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
55 changes: 55 additions & 0 deletions .config/eww/eww.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
* {
all: unset;
}

.lyrics-box {
background-color: rgba(0, 0, 0, 0.55);
border-radius: 12px;
padding: 14px 20px;
font-family: "DejaVu Sans Mono", monospace;
/* Fixed regardless of how long/short the current line's text is --
otherwise the box (not the window, which is already a fixed
620px) visibly shrinks and grows to fit whichever line is
currently longest. 580px = 620px window width minus this box's
own 20px left/right padding. */
min-width: 580px;
}

.lyrics-context, .lyrics-current {
font-size: 15px;
margin: 2px 0;
}

.lyrics-context {
color: #888888;
}

.lyrics-current {
/* Same weight as the context lines on purpose -- font-size is set
per-instance via the bound :style attribute (shrinks for long
lines, see lyrics-listen.sh's font_size_for), not here. */
color: #ffe66d;
opacity: 1;
transition: opacity 300ms ease;
}

.lyrics-current.dim {
/* The brief in-between state on a line change -- see
lyrics-listen.sh's two-phase "reveal" emission. Opacity only, so
this row's own height never changes and nothing above/below it
moves. */
opacity: 0.15;
}

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

.lyrics-close:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.15);
}
47 changes: 47 additions & 0 deletions .config/eww/eww.yuck
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
;; Lyrics scroll widget -- pinned always-on-top overlay showing the
;; current synced (.lrc) lyrics line with 1 line of context above and 2
;; below, sourced from "mpdtui -lyrics-line" via scripts/lyrics-listen.sh
;; (see that script for why this is deflisten/push rather than
;; defpoll/pull -- it needs to emit an intermediate "dimmed" frame for
;; the current line's opacity transition below to animate from).
;;
;; The current line's own opacity (not a `revealer`, which was tried
;; first -- it collapses its child to zero height while hidden, shrinking
;; then re-growing the whole box on every line change) fades between
;; `reveal`'s two states; its font-size is bound to `current_size`
;; (scripts/lyrics-listen.sh's font_size_for) so a long line shrinks
;; instead of overflowing the box's fixed width.
;;
;; The outer revealer (visible) auto-hides the whole widget when MPD
;; isn't actively playing -- collapsing it rather than "eww close"-ing
;; the window itself, which would kill the very deflisten script that's
;; supposed to bring it back (see lyrics-listen.sh's own doc comment).

(deflisten lyrics :initial "{\"visible\": false, \"reveal\": true, \"current_size\": 15, \"above\": \"\", \"current\": \"\", \"below1\": \"\", \"below2\": \"\"}"
"~/.config/eww/scripts/lyrics-listen.sh")

;; :close-window closes lyrics-window, not the daemon -- but note (see
;; lyrics-listen.sh and personal-systemd-services.md §7) this is a
;; *manual* close, not the auto-hide-on-pause path: it does kill the
;; deflisten script backing `lyrics` (eww only keeps one running while
;; some open window uses its variable), and won't come back on its own.
;; Run "eww open lyrics-window" to bring it back afterward.
(defwidget lyrics-widget []
(revealer :transition "slidedown" :duration "400ms" :reveal {lyrics.visible}
(overlay
(box :orientation "v" :space-evenly false :halign "center" :class "lyrics-box"
(label :class "lyrics-context" :text {lyrics.above} :limit-width 70)
(label :class {lyrics.reveal ? "lyrics-current" : "lyrics-current dim"}
:style "font-size: ${lyrics.current_size}px;"
:text {lyrics.current} :limit-width 70)
(label :class "lyrics-context" :text {lyrics.below1} :limit-width 70)
(label :class "lyrics-context" :text {lyrics.below2} :limit-width 70))
(button :class "lyrics-close" :halign "end" :valign "start"
:onclick "eww close lyrics-window" "✕"))))

(defwindow lyrics-window
:monitor 0
:geometry (geometry :x "0%" :y "4%" :width "620px" :height "1px" :anchor "top center")
:stacking "fg"
:windowtype "normal"
(lyrics-widget))
89 changes: 89 additions & 0 deletions .config/eww/scripts/lyrics-listen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/bash
# Streams the current synced-lyrics window to eww's `lyrics` deflisten
# variable as one JSON object per line, forever, one tick per second.
#
# Runs "mpdtui -lyrics-line" once per tick (its own 4-line window: 1 line
# of context above the current line, the current line, 2 lines below --
# see mpdtui's internal/lyricsline) and pushes the result straight
# through -- except when the *current* line (line 2) has changed since
# the previous tick: then it first pushes "reveal: false" (still showing
# the *old* current text, dimmed) and sleeps briefly before pushing
# "reveal: true" with the new text at full opacity, so eww.yuck's
# opacity transition on the current-line label has two distinct states
# to animate between. A poll-based (defpoll) variable can't do this: it
# only ever holds one value at a time, so there's no way to briefly show
# the "dimmed" state in between two polled snapshots -- deflisten's
# whole point is pushing as many intermediate frames as needed, on our
# own schedule, not eww's.
#
# "reveal" deliberately drives an *opacity* transition, not a `revealer`
# widget wrapping the label -- a `revealer` was tried first and collapses
# its child to zero height while hidden, which shrinks (then re-grows)
# the whole box every single line change. Opacity leaves the label's own
# row height untouched the entire time, so nothing above/below it moves.
#
# current_size scales the current line's own font down for long lines
# (see font_size_for) so a long line doesn't overflow the box's fixed
# width instead of needing the box to grow to fit it.
#
# Also emits "visible" (true only when MPD is actively [playing]), which
# eww.yuck uses to wrap the *entire* widget in an outer revealer -- NOT
# "eww close"/"eww open" on the window itself. Closing the window that
# way was tried first and is a trap: eww only keeps a deflisten script
# alive while some open window is actually using its variable, so a
# script that closes its own window gets killed the moment it does --
# nothing is left running to ever reopen it. Collapsing an outer
# revealer keeps the window (and this script) alive the whole time,
# avoiding that self-inflicted dead end entirely.

# font_size_for: the current line's font shrinks in three steps as its
# character count grows, so a long line stays inside the box's fixed
# 580px content width rather than needing the box to widen or the text
# to wrap. Thresholds picked for DejaVu Sans Mono at the base 15px size
# (~9px/char): 45 chars is where it starts getting close to the box's
# ~540px usable text width (580px minus the box's own 20px*2 padding).
font_size_for() {
local len=$1
if [ "$len" -gt 60 ]; then
echo 11
elif [ "$len" -gt 45 ]; then
echo 13
else
echo 15
fi
}

prev_current=""
while true; do
if mpc status 2>/dev/null | grep -q '\[playing\]'; then
playing="true"
else
playing="false"
fi

if [ "$playing" = "true" ]; then
mpdtui -lyrics-line >/tmp/mpdtui-lyrics-line.txt 2>/dev/null
above=$(sed -n '1p' /tmp/mpdtui-lyrics-line.txt)
current=$(sed -n '2p' /tmp/mpdtui-lyrics-line.txt)
below1=$(sed -n '3p' /tmp/mpdtui-lyrics-line.txt)
below2=$(sed -n '4p' /tmp/mpdtui-lyrics-line.txt)
current_size=$(font_size_for "${#current}")

if [ "$current" != "$prev_current" ] && [ -n "$prev_current" ]; then
jq -cn --argjson visible "$playing" --argjson size "$current_size" --arg above "$above" --arg current "$prev_current" --arg below1 "$below1" --arg below2 "$below2" \
'{visible: $visible, reveal: false, current_size: $size, above: $above, current: $current, below1: $below1, below2: $below2}'
sleep 0.2
fi

jq -cn --argjson visible "$playing" --argjson size "$current_size" --arg above "$above" --arg current "$current" --arg below1 "$below1" --arg below2 "$below2" \
'{visible: $visible, reveal: true, current_size: $size, above: $above, current: $current, below1: $below1, below2: $below2}'

prev_current="$current"
else
jq -cn --argjson visible "$playing" \
'{visible: $visible, reveal: true, current_size: 15, above: "", current: "", below1: "", below2: ""}'
prev_current=""
fi

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

[Service]
Type=simple
# scripts/lyrics-listen.sh shells out to "mpdtui" (installed via Homebrew
# at /home/linuxbrew/.linuxbrew/bin), which isn't on a systemd user
# service's own minimal default PATH -- unlike an interactive shell, this
# doesn't source .zshrc/.bashrc/.profile at all.
Environment=PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/eww daemon --no-daemonize
ExecStartPost=/usr/bin/sleep 2
ExecStartPost=/usr/bin/eww open lyrics-window
Restart=on-failure

[Install]
WantedBy=default.target personal-services.target
2 changes: 1 addition & 1 deletion workspace/aistuff
Loading