diff --git a/.config/eww/assets/player-expand.svg b/.config/eww/assets/player-expand.svg
new file mode 100644
index 0000000..be89569
--- /dev/null
+++ b/.config/eww/assets/player-expand.svg
@@ -0,0 +1,8 @@
+
diff --git a/.config/eww/assets/player-pause.svg b/.config/eww/assets/player-pause.svg
new file mode 100644
index 0000000..fad66c1
--- /dev/null
+++ b/.config/eww/assets/player-pause.svg
@@ -0,0 +1,11 @@
+
diff --git a/.config/eww/assets/player-play.svg b/.config/eww/assets/player-play.svg
new file mode 100644
index 0000000..653f172
--- /dev/null
+++ b/.config/eww/assets/player-play.svg
@@ -0,0 +1,12 @@
+
diff --git a/.config/eww/eww.scss b/.config/eww/eww.scss
index 53893e5..e360298 100644
--- a/.config/eww/eww.scss
+++ b/.config/eww/eww.scss
@@ -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);
+}
diff --git a/.config/eww/eww.yuck b/.config/eww/eww.yuck
index 22c871d..18bb3ed 100644
--- a/.config/eww/eww.yuck
+++ b/.config/eww/eww.yuck
@@ -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))
diff --git a/.config/eww/scripts/mpd-player-status.sh b/.config/eww/scripts/mpd-player-status.sh
new file mode 100755
index 0000000..c92620f
--- /dev/null
+++ b/.config/eww/scripts/mpd-player-status.sh
@@ -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
diff --git a/.config/systemd/user/eww.service b/.config/systemd/user/eww.service
index d717bea..00cb5fc 100644
--- a/.config/systemd/user/eww.service
+++ b/.config/systemd/user/eww.service
@@ -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]
@@ -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]
diff --git a/workspace/.alias_descriptions b/workspace/.alias_descriptions
index e271af8..158da7a 100644
--- a/workspace/.alias_descriptions
+++ b/workspace/.alias_descriptions
@@ -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)
diff --git a/workspace/.aliases.sh b/workspace/.aliases.sh
index 652a78d..0c49127 100644
--- a/workspace/.aliases.sh
+++ b/workspace/.aliases.sh
@@ -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