Skip to main content

max / alloy

18.5 KB · 373 lines History Blame Raw
1 @{! variants = default, night }
2 # Alloy Sway configuration
3 #
4 # i3-style tiling Wayland session. Translated from the former niri
5 # config on the 2026-07-17 pivot away from the scrolling-tiler model.
6 # Palette on chrome per docs/TOKENS.md.
7 #
8 # HANDOFF (scaffold — refine in Helix):
9 # - Env vars: GTK/QT/MOZ backend hints live in
10 # ~/.config/environment.d/alloy.conf; verify they reach sway's
11 # children under greetd (see the live-verification GO task).
12 # - Verify on real hardware: swayosd Fn binds, grim screenshots,
13 # per-machine output in /etc/sway/config.d/.
14
15 set $mod Mod4
16 # shop, Alloy's own terminal, built from source into the image rather than
17 # installed from a repo. The name is the binary name, with none of the
18 # package-name-versus-binary-name trap rio had.
19 #
20 # Get this wrong and an installed machine has no way to open a terminal at
21 # all, and no way to edit this file to fix it, short of a VT: with no
22 # launcher bound below, $mod+Return is the only route from the session to a
23 # shell. The Containerfile asserts the binary exists for that reason.
24 set $term shop
25
26 # -------------------------------------------------------------------
27 # Input
28 # -------------------------------------------------------------------
29 input type:keyboard {
30 xkb_layout us
31 repeat_delay 400
32 repeat_rate 30
33 }
34
35 input type:touchpad {
36 tap enabled
37 natural_scroll enabled
38 pointer_accel 0.2
39 }
40
41 input type:pointer {
42 accel_profile flat
43 }
44
45 # niri warped the cursor to the focused window; sway's nearest is
46 # warping to the focused container center on focus change.
47 mouse_warping container
48 focus_follows_mouse no
49
50 # Cursor theme + size (was niri's XCURSOR_* environment block).
51 seat seat0 xcursor_theme Bibata-Modern-Classic 24
52
53 # -------------------------------------------------------------------
54 # Output — per-machine display config lives in a drop-in so this base
55 # stays generic. Ship an example, commented, like the niri config did.
56 # -------------------------------------------------------------------
57 # output eDP-1 {
58 # mode 1920x1200@60Hz
59 # scale 1.0
60 # }
61 output * bg @{surface.page} solid_color
62 # The backdrop, and the flat colour above is its fallback. Both lines stay:
63 # shop exits when the compositor has no wlr-layer-shell, and a desktop with no
64 # background at all is worse than a plain one.
65 #
66 # `--layer background` is sized to the output, anchored to all four edges and
67 # takes no input, so it never steals a click or a keypress from the window in
68 # front of it. The panel prints this machine's own `alloy` verbs and the binds
69 # from this file, which is the only keybinding reference an installed machine
70 # carries: docs/ is not copied into the image.
71 #
72 # alloy-drift runs the surface and alloy-backdrop still writes the panel: the
73 # reference stays derived by the script that derives it, and what the binary
74 # adds is the frame clock and the automata that run once the panel is put away
75 # ($mod+F1, below). The panel is what it shows on login, every login.
76 #
77 # It reads its colours from ~/.config/shop/config.toml like any other shop, so
78 # `alloy theme apply` moves the backdrop and the terminal together.
79 exec $term --layer background -e /usr/bin/alloy-drift
80 include /etc/sway/config.d/*
81 # `alloy display` owns 50-display.conf in here, and this include is what makes
82 # the console's writes reach sway at the next login. Ordered after the system
83 # drop-ins deliberately: sway merges every stanza that matches an output, later
84 # ones winning on a property both name, so what the user set beats what the
85 # image shipped. Anything else in this directory is theirs and is left alone.
86 include ~/.config/sway/config.d/*
87
88 # -------------------------------------------------------------------
89 # Layout / chrome
90 # -------------------------------------------------------------------
91 gaps inner 8
92 gaps outer 0
93 # niri ran border off + a 2px focus-ring; pixel border with no titlebar
94 # is the sway equivalent (also covers the old prefer-no-csd intent).
95 default_border pixel 2
96 default_floating_border pixel 2
97 font pango:Quasi Mono 10
98
99 # class border background text indicator child_border
100 client.focused @{border.strong} @{border.strong} @{content.primary} @{border.strong} @{border.strong}
101 client.focused_inactive @{border.subtle} @{border.subtle} @{content.muted} @{border.subtle} @{border.subtle}
102 client.unfocused @{border.subtle} @{border.subtle} @{content.muted} @{border.subtle} @{border.subtle}
103 client.urgent @{status.danger} @{status.danger} @{readable_on(status.danger)} @{status.danger} @{status.danger}
104
105 # satty floats; everything else tiles (anyrun rule dropped with anyrun).
106 for_window [app_id="satty"] floating enable
107 # The picker floats centered rather than tiling: it is a transient prompt, and
108 # tiling it would reflow every window on screen for as long as it is open.
109 for_window [app_id="alloy-menu"] floating enable, resize set 800 500
110
111 # -------------------------------------------------------------------
112 # Startup
113 # -------------------------------------------------------------------
114 # Systemd user units DO run in this session, so nothing that ships a user unit
115 # needs an `exec` line here. The mechanism is the sway-systemd package:
116 # /etc/sway/config.d/10-systemd-session.conf ends by exec'ing
117 # /usr/libexec/sway-systemd/session.sh, which starts sway-session.target, and
118 # that target BindsTo=graphical-session.target and wants
119 # graphical-session-pre.target. None of it is Alloy's config, which is why it
120 # is invisible from this file and was twice assumed absent. Proven on
121 # 2026-07-21 by booting an install in QEMU and reading the uid 1000 journal:
122 # all three targets reached, xdg-user-dirs.service finished, pipewire and
123 # wireplumber running as the user, and reproduced on the fw12 install on
124 # 2026-07-29. So `exec xdg-user-dirs-update` came back out, and ~/Documents and
125 # friends are created by xdg-user-dirs.service.
126 #
127 # The package arrives as a weak dependency (sway -> sway-config-upstream
128 # recommends sway-systemd), not as an explicit install line, so a build that
129 # turns weak deps off would take the whole user session with it. Rationale for
130 # the other half, the presets, is in etc/systemd/user-preset/50-alloy.preset.
131 exec mkdir -p ~/Pictures/Screenshots
132 # Clipboard history: two watchers per docs/STACK.md#clipboard-history.
133 #
134 # Through alloy-clipstore rather than `cliphist store` directly, since
135 # 2026-08-17. Both are the same one line apart: store the selection, unless
136 # alloy-secret-copy marked it as a password on its way to a paste. Before that
137 # these two lines archived every password gopass copied, and $mod+Shift+v read
138 # them back (GO alloy problem e7a9e38c).
139 exec wl-paste --type text --watch alloy-clipstore
140 exec wl-paste --type image --watch alloy-clipstore
141 # swayosd-server backs the volume/brightness/caps overlays, and this line is
142 # the only thing that starts it. Checked rather than assumed, because the note
143 # above retires the sibling `exec`: SwayOSD-0.3.2 ships three binaries and
144 # exactly one unit, swayosd-libinput-backend.service, which is a SYSTEM unit
145 # for reading the lock keys. There is no user unit for the server in the
146 # package at all, and the one D-Bus activation file it ships
147 # (org.erikreider.swayosd on the system bus) names the backend, not the server.
148 # So the server has nothing to start it but the compositor.
149 exec swayosd-server
150
151 # Monitors follow their settings between ports.
152 #
153 # `~/.config/sway/config.d/50-display.conf` keys every stanza by connector name,
154 # so a monitor moved from DP-1 to DP-2 arrives with whatever was written for the
155 # port rather than for the screen. This reads the EDID of what is attached,
156 # matches it against Alloy's own table, and moves the settings across. See
157 # `crates/alloy/src/monitors.rs` for why the connector is what gets written.
158 #
159 # Here rather than in `usr/bin/alloy-session`, which is where `alloy theme
160 # apply` runs and would be the obvious home: this needs a compositor to ask, and
161 # the wrapper runs before there is one. `exec` fires once sway is up, which is
162 # the earliest moment `swaymsg -t get_outputs` can answer.
163 #
164 # Draws nothing and prints only when something moved.
165 exec alloy display --reconcile
166
167 # The first-boot offer: a mesh for the machines, a sync for the files
168 # (docs/CONTINUITY.md). --if-first-boot is what makes this a one-time event:
169 # the console records that it asked, and every later login runs this line and
170 # exits without drawing anything. Skipping is fine and reversible; `alloy mesh`
171 # and `alloy sync` carry the same enrollments afterwards.
172 exec $term -e alloy setup --if-first-boot
173
174 # Idle: warn at 4.5 minutes, lock at 5, displays off at 10, and always lock
175 # before suspend so a closed lid never resumes into a live session.
176 # -w makes swayidle wait for the lock to come up before sleeping.
177 # Lock styling comes from ~/.config/swaylock/config (it sets daemonize).
178 #
179 # The 270-second stage is the warning, and it is why this block has four
180 # timeouts instead of three. Until it existed the comment here read "dim to
181 # lock at 5 minutes" and nothing dimmed: full brightness to swaylock with no
182 # intermediate state, so the lock could only ever arrive as an ambush. Thirty
183 # seconds of dimmed screen is enough to notice and move the mouse, and the
184 # `resume` on that stage is what puts the brightness back — including after an
185 # unlock, since typing a password is input and fires it.
186 #
187 # It is also the nearest thing to a grace period on offer: upstream swaylock has
188 # no --grace, and taking the swaylock-effects fork for one flag is the wrong
189 # trade for a distro that prefers canonical packages.
190 exec swayidle -w \
191 timeout 270 'alloy-dim down' \
192 resume 'alloy-dim up' \
193 timeout 300 'swaylock -f' \
194 timeout 600 'swaymsg "output * power off"' \
195 resume 'swaymsg "output * power on"' \
196 before-sleep 'swaylock -f'
197
198 # Do not lock over a video.
199 #
200 # sway implements the Wayland idle-inhibit protocol and swayidle honours it, so
201 # an application that registers an inhibitor while it plays already suppresses
202 # the block above without help. These rules cover the case where that does not
203 # happen: mpv only inhibits while its stop-screensaver option is on, and a
204 # browser that loses the inhibitor mid-session gives no sign of it. Fullscreen
205 # is the condition rather than mere focus, because a windowed video is usually
206 # something being glanced at rather than watched.
207 #
208 # Deliberately a named few and not `[app_id=".*"]`. A blanket rule would make
209 # any fullscreen window hold the lock off, and on a distro whose primary surface
210 # is a terminal that means walking away from a fullscreen shell leaves the
211 # machine unlocked indefinitely. The lock is a security control; the exemption
212 # belongs to the handful of things that are actually playing something. Add an
213 # app here when a real one is found to need it, and use `class` rather than
214 # `app_id` if it turns out to be running through XWayland.
215 #
216 # The browser is matched by a pattern rather than a literal, and stays a pattern
217 # now that Firefox is the only one the image carries. A user who installs their
218 # own is the case it is for: a Flatpak Chromium arrives with its own app_id and
219 # a fullscreen video in it should hold the lock off too. Add one here when a
220 # real one is found, and use `class` rather than `app_id` if it turns out to be
221 # running through XWayland. Not yet confirmed against a running session: if a
222 # fullscreen video stops holding the lock off, this is the line to check first.
223 for_window [app_id="mpv"] inhibit_idle fullscreen
224 for_window [app_id="^(firefox|chromium)"] inhibit_idle fullscreen
225 for_window [app_id="imv"] inhibit_idle fullscreen
226
227 # -------------------------------------------------------------------
228 # Binds — session
229 # -------------------------------------------------------------------
230 bindsym $mod+Return exec $term
231 # No multiplexer bind. Tabs, splits and panes are sway's job, and a
232 # multiplexer inside a tiling compositor is the same feature twice; see
233 # docs/STACK.md#terminal.
234 # Launcher: a fuzzy picker in a terminal, not a graphical surface
235 # (docs/STACK.md#launcher). $mod+Return is for typing commands; this is for
236 # the graphical apps whose binary name nobody remembers.
237 bindsym $mod+d exec $term --app-id=alloy-menu -e /usr/bin/alloy-menu
238 # Clipboard history, read half. The two wl-paste watchers above have filled it
239 # since the stack landed; this is what gets anything back out.
240 bindsym $mod+Shift+v exec $term --app-id=alloy-menu -e /usr/bin/alloy-clipmenu
241 bindsym $mod+Ctrl+l exec swaylock
242 # Reload. Sway's own default, and absent here until now: without it the only
243 # way to apply a config edit is `swaymsg reload` from a terminal, which is
244 # circular in the one case that matters: a config that broke the terminal
245 # bind. That is not hypothetical; it is how this file's $term was found wrong.
246 bindsym $mod+Shift+c reload
247 bindsym $mod+Shift+e exit
248 # The keybinding reference, on the desktop behind everything. It is what the
249 # background shows by default and this puts it away and brings it back; behind
250 # it are automata that draw nothing anybody authored (crates/backdrop).
251 #
252 # A signal rather than a key the backdrop reads, because the background layer
253 # has an empty input region and never receives one -- that is what stops a click
254 # on the desktop stealing focus, and it is worth more than a keypress. `-x` so
255 # this cannot match anything but the backdrop itself.
256 #
257 # This line is why the bind is discoverable: the panel derives its key list from
258 # this file, so the way to dismiss the reference is printed on the reference.
259 bindsym $mod+F1 exec pkill -USR1 -x alloy-drift
260
261 # Window ops
262 bindsym $mod+q kill
263 bindsym $mod+f fullscreen toggle
264 bindsym $mod+Shift+f fullscreen toggle global
265
266 # Focus (i3 hjkl)
267 bindsym $mod+h focus left
268 bindsym $mod+j focus down
269 bindsym $mod+k focus up
270 bindsym $mod+l focus right
271
272 # Move
273 bindsym $mod+Shift+h move left
274 bindsym $mod+Shift+j move down
275 bindsym $mod+Shift+k move up
276 bindsym $mod+Shift+l move right
277
278 # Layout — sway-native (no niri equivalent; core i3 UX)
279 bindsym $mod+b splith
280 bindsym $mod+v splitv
281 bindsym $mod+w layout tabbed
282 bindsym $mod+s layout stacking
283 bindsym $mod+e layout toggle split
284 bindsym $mod+Shift+space floating toggle
285 bindsym $mod+space focus mode_toggle
286
287 # Resize (was niri's column-width presets)
288 bindsym $mod+r mode "resize"
289 bindsym $mod+minus resize shrink width 10 ppt
290 bindsym $mod+equal resize grow width 10 ppt
291 mode "resize" {
292 bindsym h resize shrink width 10 ppt
293 bindsym l resize grow width 10 ppt
294 bindsym k resize shrink height 10 ppt
295 bindsym j resize grow height 10 ppt
296 bindsym Return mode "default"
297 bindsym Escape mode "default"
298 }
299
300 # Workspaces (kept the named notes/main from niri; media too)
301 bindsym $mod+1 workspace number 1
302 bindsym $mod+2 workspace number 2
303 bindsym $mod+3 workspace number 3
304 bindsym $mod+4 workspace number 4
305 bindsym $mod+n workspace notes
306 bindsym $mod+m workspace main
307 bindsym $mod+Shift+1 move container to workspace number 1
308 bindsym $mod+Shift+2 move container to workspace number 2
309 bindsym $mod+Shift+3 move container to workspace number 3
310 bindsym $mod+Shift+4 move container to workspace number 4
311
312 # -------------------------------------------------------------------
313 # Binds — screenshots (grim; sway has no built-in capture)
314 # -------------------------------------------------------------------
315 # All four go through alloy-shot, which captures and then says so. They were
316 # four inline grim calls until 2026-07-29, when Print Screen was reported as a
317 # dead keybind: it worked the whole time and told nobody, which is the same
318 # thing from the keyboard. The script's header has the rest, including why a
319 # `&& notify-send` appended here would have named the wrong file.
320 #
321 # Active window was deferred once on the belief that jq had to be installed for
322 # it; jq is a declared dependency of the image, so the deferral cost nothing but
323 # the bind. Ctrl+Print was a second copy of the region-select line until then.
324 # Whole output.
325 bindsym Print exec alloy-shot output
326 # Region select. Escape cancels without an error.
327 bindsym Shift+Print exec alloy-shot region
328 # Active window.
329 bindsym Ctrl+Print exec alloy-shot window
330 # Annotate the most recent screenshot with satty.
331 bindsym $mod+Print exec alloy-shot annotate
332
333 # -------------------------------------------------------------------
334 # Binds — volume / brightness / media (swayosd + playerctl)
335 # -------------------------------------------------------------------
336 bindsym XF86AudioRaiseVolume exec swayosd-client --output-volume raise
337 bindsym XF86AudioLowerVolume exec swayosd-client --output-volume lower
338 bindsym XF86AudioMute exec swayosd-client --output-volume mute-toggle
339 bindsym XF86AudioMicMute exec swayosd-client --input-volume mute-toggle
340 bindsym XF86MonBrightnessUp exec swayosd-client --brightness raise
341 bindsym XF86MonBrightnessDown exec swayosd-client --brightness lower
342 bindsym XF86AudioPlay exec playerctl play-pause
343 bindsym XF86AudioPause exec playerctl play-pause
344 bindsym XF86AudioNext exec playerctl next
345 bindsym XF86AudioPrev exec playerctl previous
346 # Caps-lock indicator (needs swayosd-libinput-backend running).
347 bindsym --release Caps_Lock exec swayosd-client --caps-lock
348
349 # -------------------------------------------------------------------
350 # Bar — swaybar, with `alloy status --bar` as the status line.
351 # -------------------------------------------------------------------
352 # This was `while date; do sleep 20; done` until 2026-07-30: a clock and
353 # nothing else, on a distro whose one validated machine is a laptop. Asking
354 # whether the machine was charging meant reading /sys by hand.
355 #
356 # The status line is a console verb rather than a script in usr/bin/ because
357 # `alloy audio` and `alloy net` already parse pactl and nmcli into types, and a
358 # script would have been a second parse of the same two contracts. It emits
359 # swaybar's JSON protocol, so each block carries its own color from the theme;
360 # the `colors` block below still sets the bar's own surfaces and the workspace
361 # buttons, which are swaybar's to draw and not the status line's.
362 bar {
363 position top
364 font pango:Quasi Mono 10
365 status_command alloy status --bar
366 colors {
367 background @{surface.page}
368 statusline @{content.primary}
369 focused_workspace @{border.strong} @{border.strong} @{content.primary}
370 inactive_workspace @{border.subtle} @{border.subtle} @{content.muted}
371 }
372 }
373