Skip to main content

max / alloy

8.8 KB · 233 lines History Blame Raw
1 #!/bin/sh
2 # alloy-backdrop — the desktop background: this machine's own verbs and keys.
3 #
4 # Runs as a shop surface on the wlr-layer-shell background layer, started from
5 # the sway config. A running Alloy machine otherwise ships no keybinding
6 # reference at all: the only other copy is docs/manual/05-keybindings.md, and no
7 # COPY puts docs/ into the image. So an empty workspace shows the reference
8 # instead of a flat colour.
9 #
10 # BOTH LISTS ARE DERIVED, never transcribed. The verbs come from `alloy --help`
11 # and the keys from the sway config this session actually loaded. A second copy
12 # of either would be a list to maintain, and the one that rots is the one
13 # nobody reads because it is behind their windows.
14 #
15 # A SCRIPT RATHER THAN A VERB, and that is the reason: `alloy backdrop` would
16 # appear in the verb list this panel prints. usr/bin is where the session's own
17 # helpers already live (alloy-menu, alloy-clipmenu, alloy-layer-notice).
18 #
19 # IT TAKES NO INPUT. shop's `--layer background` sets an empty input region and
20 # no keyboard interactivity, so clicks and keys pass through to whatever is
21 # actually focused. Nothing here should ever want a keypress.
22 #
23 # NO COLOUR OF ITS OWN. Only SGR bold and dim, which shop resolves out of the
24 # theme named in ~/.config/shop/config.toml. `alloy theme apply` swaps that file
25 # for its .night sibling, so the backdrop follows the desktop without knowing a
26 # palette exists (docs/TOKENS.md: no hex outside the theme files).
27 #
28 # IT MUST NOT EXIT. shop closes when the program it runs finishes, and a closed
29 # background surface is a black screen, not a flat one. So this parks after
30 # drawing, and redraws on SIGWINCH: the surface is sized to the output, and
31 # `alloy display --reconcile` runs at login, so the first size this sees is
32 # often not the one it keeps.
33
34 set -u
35
36 CONFIG="${ALLOY_BACKDROP_CONFIG:-$HOME/.config/sway/config}"
37 ALLOY="${ALLOY_BACKDROP_ALLOY:-alloy}"
38
39 # The verbs, from the binary rather than from a list here. clap indents each
40 # command by two spaces under "Commands:" and ends the block with a blank line.
41 # `help` is clap's own and is dropped: it is not something the machine does.
42 verbs() {
43 "$ALLOY" --help 2>/dev/null | awk '
44 /^Commands:/ { inside = 1; next }
45 inside && /^[[:space:]]*$/ { exit }
46 inside && /^ [a-z]/ {
47 name = $1
48 if (name == "help") next
49 $1 = ""
50 sub(/^[[:space:]]+/, "")
51 print name "\t" $0
52 }
53 '
54 }
55
56 # The keys, from the config the session loaded. Variables are resolved from the
57 # file's own `set` lines, so $mod reads as the key someone presses rather than
58 # as sway's spelling of it, and $term reads as the terminal this image ships.
59 # Reading only the main config on purpose: the drop-ins under config.d are the
60 # machine's and the user's, and a backdrop that printed someone's private binds
61 # onto the desktop would be a surprise.
62 keys() {
63 [ -r "$CONFIG" ] || return 0
64 awk '
65 # Collect `set $name value` first; sway allows use before definition,
66 # so substitution waits until the whole file is read.
67 /^set[[:space:]]+\$/ {
68 name = $2
69 $1 = ""; $2 = ""
70 sub(/^[[:space:]]+/, "")
71 vars[name] = $0
72 next
73 }
74 # A `mode "resize" { ... }` block scopes its binds to that mode. They
75 # are not reachable from the desktop the way the others are, so they
76 # carry the mode on the chord: an untagged `h` printed beside the
77 # global binds would read as Super+h, which does something else.
78 /^mode[[:space:]]+"/ {
79 mode = $2
80 gsub(/"/, "", mode)
81 next
82 }
83 mode != "" && /^}/ { mode = ""; next }
84 /^[[:space:]]*bindsym[[:space:]]/ {
85 line = $0
86 sub(/^[[:space:]]*bindsym[[:space:]]+/, "", line)
87 # `--release` and friends are modifiers on the bind, not the chord.
88 while (line ~ /^--[a-z-]+[[:space:]]/) {
89 sub(/^--[a-z-]+[[:space:]]+/, "", line)
90 }
91 split(line, part, /[[:space:]]+/)
92 chord = part[1]
93 action = line
94 sub(/^[^[:space:]]+[[:space:]]+/, "", action)
95 # `exec` is how sway spells "run a command" and is on most of these
96 # lines; printing it on every row costs width and says nothing.
97 sub(/^exec[[:space:]]+/, "", action)
98 if (mode != "") chord = mode " " chord
99 binds[++n] = chord "\t" action
100 }
101 END {
102 for (i = 1; i <= n; i++) {
103 line = binds[i]
104 for (name in vars) {
105 gsub("\\" name, vars[name], line)
106 }
107 # Mod4 is what sway calls it and Super is what is printed on
108 # the key, which is the whole point of resolving the variable.
109 gsub(/Mod4/, "Super", line)
110 gsub(/Mod1/, "Alt", line)
111 print line
112 }
113 }
114 ' "$CONFIG"
115 }
116
117 # Terminal size. `stty size` answers from the pty shop gave us; the fallback is
118 # a small sane grid rather than an error, because a backdrop that refused to
119 # draw would leave the screen black.
120 #
121 # ALLOY_BACKDROP_SIZE overrides it as "<rows> <cols>". That exists so the layout
122 # can be asserted without allocating a pty: the test that matters is that every
123 # bind reaches the panel at any width, and a test harness has no terminal.
124 size() {
125 if [ -n "${ALLOY_BACKDROP_SIZE:-}" ]; then
126 printf '%s\n' "$ALLOY_BACKDROP_SIZE"
127 return
128 fi
129 s=$(stty size 2>/dev/null) || s=""
130 case "$s" in
131 [0-9]*' '[0-9]*) printf '%s\n' "$s" ;;
132 *) printf '%s\n' "24 80" ;;
133 esac
134 }
135
136 render() {
137 set -- $(size)
138 rows=$1
139 cols=$2
140
141 printf '\033[2J\033[H'
142
143 { verbs | sed 's/^/V\t/'; keys | sed 's/^/K\t/'; } | awk \
144 -v rows="$rows" -v cols="$cols" -F '\t' '
145 function spaces(n, out) {
146 out = ""
147 while (n-- > 0) out = out " "
148 return out
149 }
150 # Flow a section into as many columns as the width allows, filling each
151 # column top to bottom so the reading order down a column is the order
152 # the source had.
153 #
154 # Padding is computed from the VISIBLE length, which is why the styled
155 # cell is assembled here rather than handed in ready-made: length()
156 # counts the SGR bytes, so padding a styled string aligns the escapes
157 # and not the glyphs.
158 function columns(key, val, count, kw, width, label, per, cn, r, c, idx, line, cell, vis, gap) {
159 if (count == 0) return
160 print ""
161 gap = 3
162 cn = int((cols - 2 + gap) / (width + gap))
163 if (cn < 1) cn = 1
164 per = int((count + cn - 1) / cn)
165 printf "\033[1m %s\033[0m\n", label
166 for (r = 0; r < per; r++) {
167 line = ""
168 for (c = 0; c < cn; c++) {
169 idx = c * per + r + 1
170 if (idx > count) continue
171 if (c > 0) line = line spaces(gap)
172 cell = "\033[1m" key[idx] "\033[0m" spaces(kw - length(key[idx])) \
173 " \033[2m" val[idx] "\033[0m"
174 vis = kw + 1 + length(val[idx])
175 line = line cell spaces(width - vis)
176 }
177 sub(/[[:space:]]+$/, "", line)
178 print " " line
179 }
180 }
181 {
182 if ($1 == "V") {
183 vn++
184 vkey[vn] = $2
185 vval[vn] = $3
186 if (length($2) > vw) vw = length($2)
187 } else {
188 kn++
189 kkey[kn] = $2
190 kval[kn] = $3
191 if (length($2) > kw) kw = length($2)
192 }
193 }
194 END {
195 for (i = 1; i <= vn; i++) {
196 vlen = vw + 1 + length(vval[i])
197 if (vlen > vwidth) vwidth = vlen
198 }
199 for (i = 1; i <= kn; i++) {
200 klen = kw + 1 + length(kval[i])
201 if (klen > kwidth) kwidth = klen
202 }
203 columns(vkey, vval, vn, vw, vwidth, "alloy")
204 columns(kkey, kval, kn, kw, kwidth, "keys")
205 }
206 '
207 }
208
209 case "${1:-}" in
210 --once)
211 # Draw and leave. What `alloy-backdrop --once | less` gives a person who
212 # wants the reference in front of them, and what lets the panel be
213 # asserted without a compositor or a pty.
214 render
215 exit 0
216 ;;
217 "") ;;
218 *)
219 echo "alloy-backdrop: expected --once or no argument" >&2
220 exit 2
221 ;;
222 esac
223
224 render
225 trap 'render' WINCH
226
227 # Park. `wait` is what makes the trap prompt rather than waiting out the sleep,
228 # so a resize redraws now instead of up to a day later.
229 while :; do
230 sleep 86400 &
231 wait $! 2>/dev/null || true
232 done
233