Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .config/eww/assets/player-expand.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions .config/eww/assets/player-pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions .config/eww/assets/player-play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions .config/eww/eww.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,115 @@
color: #ffffff;
background-color: rgba(255, 255, 255, 0.15);
}

/* Collapsed state. Both of these wrap an SVG from assets/ (see
eww.yuck) and must contribute *no* visuals of their own -- no
background, border, padding or min-size -- so the SVG's own shape is
the entire widget. Styling the button instead (border-radius +
border) is what produced a rounded rectangle rather than the
concentric-circle design. */
.player-shape-btn {
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
padding: 0;
margin: 0;
}

.player-shape-btn:hover {
/* GTK's default button hover would paint a rectangle behind the
SVG, reintroducing exactly the shape this design avoids. */
background-color: transparent;
background-image: none;
}

/* Defines the collapsed state's overall footprint (it is the overlay's
first, size-determining child and draws nothing itself). Slightly
larger than the 64px circle, which sits bottom-right within it, so
the expand triangle can occupy the empty upper-left corner just
outside the circle's arc. Sized so the two nearly touch on the
diagonal: the circle's own outline reaches to ~20px from this box's
top-left, and the triangle's lower tip ends at ~18px. */
.player-collapsed-sizer {
background-color: transparent;
min-width: 72px;
min-height: 72px;
}

/* The separate expand triangle -- no negative margin: it is positioned
by the sizer above instead. A negative margin here pushed it outside
the auto-sized window's own bounds and got it clipped. */
.player-expand-arrow {
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
padding: 0;
margin: 0;
}

.player-expand-arrow:hover {
background-color: transparent;
background-image: none;
}

/* Expanded state: a taller card. */
.player-card {
background-color: rgba(0, 0, 0, 0.75);
border-radius: 16px;
padding: 20px 24px;
font-family: "DejaVu Sans Mono", monospace;
min-width: 220px;
}

.player-albumart {
border-radius: 10px;
margin-bottom: 10px;
}

.player-title {
color: #ffffff;
font-size: 15px;
font-weight: bold;
margin-bottom: 2px;
}

.player-artist {
color: #aaaaaa;
font-size: 13px;
margin-bottom: 10px;
}

.player-controls {
margin-top: 4px;
}

.player-card-actions {
margin: 6px 6px 0 0;
}

.mpd-btn {
color: #ffffff;
font-size: 16px;
padding: 2px 8px;
margin: 0 2px;
border-radius: 6px;
}

.mpd-btn:hover {
background-color: rgba(255, 255, 255, 0.15);
}

.mpd-close {
color: #888888;
font-size: 12px;
padding: 2px 6px;
margin: 0 0 0 6px;
border-radius: 6px;
}

.mpd-close:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.15);
}
90 changes: 90 additions & 0 deletions .config/eww/eww.yuck
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,93 @@
:stacking "fg"
:windowtype "normal"
(lyrics-widget))

;; Collapsible player, bottom-right. "mpc toggle" flips play<->pause in
;; one command (matches every real media player's own single toggle
;; button rather than separate always-visible Play/Pause buttons, which
;; would be redundant given the state is already known).
(deflisten mpd_status :initial "{\"state\": \"stop\", \"artist\": \"\", \"title\": \"\", \"albumart\": \"/tmp/eww-mpd-albumart-placeholder.png\"}"
"~/.config/eww/scripts/mpd-player-status.sh")

;; Shared across both monitor instances (not per-window/[win-id]-scoped
;; like the close button below has to be) -- expanding on one screen
;; expands on both. Simplest option and matches mpd_status itself
;; already being one shared value across both instances; ask if you'd
;; rather each screen's card open/close independently, which needs a
;; separate defvar per win-id instead of one shared one.
(defvar player_expanded false)

;; Absolute, because (image :path ...) goes straight to GTK/gdk-pixbuf,
;; which does no "~" expansion (unlike the deflisten commands above,
;; where a shell expands it). Kept in one variable so there's a single
;; place to change if this config ever moves off this machine.
(defvar player_assets "/home/susamn/.config/eww/assets")

;; win-id is this specific window instance's own --id (see eww.service's
;; two "eww open --id ... --arg win-id=..." calls, one per monitor) --
;; needed because "eww close mpd-player-window" (the bare defwindow
;; name) wouldn't match either open instance; both were opened under
;; distinct --id values instead, so the close button has to close by
;; that same id, and each instance needs to know its own.
(defwidget mpd-player-widget [win-id]
;; halign/valign "end" so whichever branch is showing hugs the
;; window's own bottom-right anchor instead of centering in it.
(box :orientation "v" :halign "end" :valign "end" :space-evenly false

;; Each branch is gated on :visible *and* wrapped in a revealer, not
;; a revealer alone. A hidden revealer only collapses the axis its
;; transition animates -- it still reserves the child's full size on
;; the other axis (and crossfade animates neither), so the hidden
;; card below was silently reserving its own ~220px width even while
;; collapsed, pushing the little circle that far off the corner.
;; :visible false drops the widget from allocation entirely; the
;; revealer inside it still provides the reveal animation.
(box :visible {!player_expanded}
(revealer :transition "crossfade" :duration "250ms" :reveal {!player_expanded}
;; The sizer is the overlay's first (size-determining) child:
;; a bit larger than the 64px circle, so the arrow can sit in
;; the empty upper-left corner *outside* the circle's arc
;; without a negative margin (which would push it past the
;; window's own auto-sized bounds and get clipped).
(overlay
(box :class "player-collapsed-sizer")
(button :class "player-shape-btn" :halign "end" :valign "end"
:onclick "mpc toggle"
(image :path {mpd_status.state == "playing"
? "${player_assets}/player-pause.svg"
: "${player_assets}/player-play.svg"}
:image-width 64 :image-height 64))
(button :class "player-expand-arrow" :halign "start" :valign "start"
:onclick "eww update player_expanded=true"
(image :path "${player_assets}/player-expand.svg"
:image-width 20 :image-height 20)))))

;; Expanded: a taller card -- album art, title, artist, the same
;; toggle/stop pair, plus collapse (back to the circle) and close
;; (this instance only, see win-id above) in the corner.
(box :visible {player_expanded}
(revealer :transition "crossfade" :duration "300ms" :reveal {player_expanded}
(overlay
(box :orientation "v" :space-evenly false :halign "center" :class "player-card"
(image :class "player-albumart" :path {mpd_status.albumart} :image-width 120 :image-height 120)
(label :class "player-title" :text {mpd_status.title == "" ? "Nothing playing" : mpd_status.title} :limit-width 28)
(label :class "player-artist" :text {mpd_status.artist} :limit-width 28)
(box :orientation "h" :space-evenly false :halign "center" :class "player-controls"
(button :class "mpd-btn" :onclick "mpc toggle" {mpd_status.state == "playing" ? "⏸" : "▶"})
(button :class "mpd-btn" :onclick "mpc stop" "⏹")))
(box :orientation "h" :halign "end" :valign "start" :class "player-card-actions"
(button :class "mpd-close" :onclick "eww update player_expanded=false" "⌄")
(button :class "mpd-close" :onclick "eww close ${win-id}" "✕")))))))

(defwindow mpd-player-window
[win-id]
:monitor 0
;; Both width and height auto-size to content (not just height, like
;; lyrics-window) -- the collapsed circle and expanded card are very
;; different widths too, not just heights, and a fixed width would
;; leave the small circle floating in the middle of reserved space
;; instead of flush against the corner.
:geometry (geometry :x "5px" :y "15px" :width "1px" :height "1px" :anchor "bottom right")
:stacking "fg"
:windowtype "normal"
(mpd-player-widget :win-id win-id))
59 changes: 59 additions & 0 deletions .config/eww/scripts/mpd-player-status.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash
# Streams MPD's current playback state, track (artist/title), and album
# art path for the mpd player widget's `mpd_status` deflisten variable.
#
# Album art extraction mirrors ~/.config/conky/get-mpd-info.sh's own
# ffmpeg+convert technique exactly, but with its own cache file
# (ALBUMART_PATH/CACHE_FILE below) rather than reusing that script's
# /tmp/conky-albumart.jpg -- this widget shouldn't silently break if
# that conky instance ever stops running.
CACHE_FILE="/tmp/eww-mpd-player-track.txt"
ALBUMART_PATH="/tmp/eww-mpd-albumart.jpg"
# eww's (image) widget hard-errors ("Failed to open file """) on an
# empty :path rather than rendering a blank/placeholder -- so this is
# always emitted instead of "" when there's no real art (nothing
# playing, extraction failed, or the track has no embedded art), never
# left for eww.yuck to handle as a missing-value case.
PLACEHOLDER_PATH="/tmp/eww-mpd-albumart-placeholder.png"
MUSIC_DIR=$(grep -E "^music_directory" ~/.config/mpd/mpd.conf 2>/dev/null | sed 's/music_directory[[:space:]]*"\(.*\)"/\1/' | sed "s|~|$HOME|")
[ -z "$MUSIC_DIR" ] && MUSIC_DIR="$HOME/Music"
[ -f "$PLACEHOLDER_PATH" ] || convert -size 120x120 xc:'#2a2a2a' "$PLACEHOLDER_PATH" 2>/dev/null

prev=""
while true; do
state=$(mpc status 2>/dev/null | grep -oP '\[\K[^\]]+' || echo "stop")
artist=$(mpc current -f '%artist%' 2>/dev/null)
title=$(mpc current -f '%title%' 2>/dev/null)
file=$(mpc current -f '%file%' 2>/dev/null)

if [ -n "$file" ]; then
# Only re-extract when the track actually changed -- ffmpeg+convert
# on every 1s tick would be wasteful and pointless (the art
# can't have changed if the track hasn't).
if [ ! -f "$CACHE_FILE" ] || [ "$(cat "$CACHE_FILE" 2>/dev/null)" != "$file" ]; then
rm -f "$ALBUMART_PATH"
full_path="$MUSIC_DIR/$file"
if [ -f "$full_path" ]; then
ffmpeg -i "$full_path" -an -c:v copy "$ALBUMART_PATH" -y 2>/dev/null
if [ -s "$ALBUMART_PATH" ]; then
convert "$ALBUMART_PATH" -resize 120x120! "$ALBUMART_PATH" 2>/dev/null
fi
fi
echo "$file" > "$CACHE_FILE"
fi
else
rm -f "$ALBUMART_PATH" "$CACHE_FILE"
fi

albumart="$PLACEHOLDER_PATH"
[ -s "$ALBUMART_PATH" ] && albumart="$ALBUMART_PATH"

line="${state}|${artist}|${title}|${file}"
if [ "$line" != "$prev" ]; then
jq -cn --arg state "$state" --arg artist "$artist" --arg title "$title" --arg albumart "$albumart" \
'{state: $state, artist: $artist, title: $title, albumart: $albumart}'
prev="$line"
fi

sleep 1
done
11 changes: 10 additions & 1 deletion .config/systemd/user/eww.service
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Unit]
Description=Eww Widget Daemon (lyrics scroll widget)
Description=Eww Widget Daemon (lyrics scroll + mpd player widgets)
After=graphical-session.target

[Service]
Expand All @@ -12,6 +12,15 @@ Environment=PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/
ExecStart=/usr/bin/eww daemon --no-daemonize
ExecStartPost=/usr/bin/sleep 2
ExecStartPost=/usr/bin/eww open lyrics-window
# One instance per monitor -- "--id" lets the same mpd-player-window
# definition be opened more than once simultaneously (normally opening
# a window name that's already open just no-ops/replaces it); "--screen"
# overrides the defwindow's own hardcoded ":monitor 0" per instance.
# "--arg win-id=..." matches each instance's own --id so its close
# button (eww.yuck) can close itself specifically, not the other
# monitor's copy or the (unmatched) bare "mpd-player-window" name.
ExecStartPost=/usr/bin/eww open --id mpd-player-screen0 --screen 0 --arg win-id=mpd-player-screen0 mpd-player-window
ExecStartPost=/usr/bin/eww open --id mpd-player-screen1 --screen 1 --arg win-id=mpd-player-screen1 mpd-player-window
Restart=on-failure

[Install]
Expand Down
5 changes: 5 additions & 0 deletions workspace/.alias_descriptions
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ mtx=MPD Tui player - enlarged with vim motions
mt=MPD Tui mini player - small, inline and only what is needed
mtp=Search playlists and play with MPD Tui player
mtt=Search tracks and play with MPD Tui player
ewwr=Restart the eww widget daemon (picks up yuck/scss/unit file changes)
ewwreload=Reload eww's yuck/scss without restarting the daemon
ewwlog=Tail the eww widget daemon's systemd journal
ewwst=Show the eww widget daemon's systemd service status
mpdwidget=Open or close the mpd player widget on both monitors (mpdwidget open|close)

29 changes: 29 additions & 0 deletions workspace/.aliases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,35 @@ alias pfm="$TOOLS_PATH/performance-manager/quick-start.sh"
alias mtrm="$TOOLS_PATH/media-trimmer/quick-start.sh"
alias att="$TOOLS_PATH/api-testing-tool/quick-start.sh"

# eww widgets (lyrics scroll + mpd player, see ~/.config/eww/) --
# restart picks up ~/.config/systemd/user/eww.service, eww.yuck, or
# eww.scss changes; reload is lighter (yuck/scss only, no daemon
# restart, but won't pick up a changed unit file).
alias ewwr="systemctl --user restart eww.service"
alias ewwreload="eww reload"
alias ewwlog="journalctl --user -u eww.service -f"
alias ewwst="systemctl --user status eww.service"

# Reopen the mpd player widget on both monitors after a manual "eww
# close" -- matches eww.service's own ExecStartPost invocations exactly
# (see that file for why win-id is required: each instance's close
# button targets itself by that id, not the shared "mpd-player-window"
# name neither open instance actually uses).
mpdwidget() {
case "$1" in
open)
eww open --id mpd-player-screen0 --screen 0 --arg win-id=mpd-player-screen0 mpd-player-window
eww open --id mpd-player-screen1 --screen 1 --arg win-id=mpd-player-screen1 mpd-player-window
;;
close)
eww close mpd-player-screen0 mpd-player-screen1
;;
*)
echo "Usage: mpdwidget open|close"
;;
esac
}

pnv() {
if [ "$1" = "activate" ]; then
if [ -z "$2" ]; then
Expand Down
Loading