This repository has no description
21 kB
605 lines
1{
2 pkgs,
3 lib,
4 self,
5 ...
6}:
7let
8
9 term = "/run/current-system/sw/bin/kitty";
10
11 # Unified script: switch to workspace and move it to current output
12 gotoWorkspace = pkgs.writeShellApplication {
13 name = "goto-workspace";
14 runtimeInputs = [
15 pkgs.sway
16 pkgs.jq
17 ];
18 text = ''
19 WS="$1"
20 CURRENT_OUTPUT=$(swaymsg -r -t get_outputs | jq -r '.[] | select(.focused == true).name')
21 swaymsg workspace "$WS"
22 swaymsg move workspace to output "$CURRENT_OUTPUT"
23 '';
24 };
25
26 # Switch screens by number. mod+F<n> focuses the screen bound to slot n:
27 # the output assigned via mod+ctrl+F<n> while one exists (so a bound
28 # screen stays reachable after hot-plug renumbering), else the nth
29 # output by position (1 = primary, 2 = first secondary, ...).
30 # mod+ctrl+F<n> binds the currently focused screen to slot n.
31 screenSwitch = pkgs.writeShellApplication {
32 name = "sway-screen-switch";
33 runtimeInputs = [
34 pkgs.sway
35 pkgs.jq
36 pkgs.coreutils
37 pkgs.libnotify
38 ];
39 text = ''
40 MARKS="$HOME/.config/sway-screen-marks"
41 mkdir -p "$MARKS"
42
43 if [ "$1" = "mark" ]; then
44 SLOT="$2"
45 CURRENT=$(swaymsg -r -t get_outputs | jq -r '.[] | select(.focused == true) | .name')
46 if [ -n "$CURRENT" ]; then
47 echo "$CURRENT" > "$MARKS/$SLOT"
48 notify-send "Screen" "F$SLOT -> $CURRENT" 2>/dev/null
49 fi
50 exit 0
51 fi
52
53 MARKED=$(cat "$MARKS/$1" 2>/dev/null || true)
54
55 if [ -n "$MARKED" ]; then
56 swaymsg focus output "$MARKED"
57 exit 0
58 fi
59
60 swaymsg -r -t get_outputs | jq -r 'sort_by(.x, .y) | .[].name' | sed -n "''${1}p" | while read -r TARGET; do
61 [ -n "$TARGET" ] && swaymsg focus output "$TARGET"
62 done
63 '';
64 };
65
66 # mod+F<n> = switch to screen slot n, mod+ctrl+F<n> = bind current screen to slot n.
67 screenBindings = lib.concatStringsSep "\n" (
68 lib.map
69 (n: ''
70 bindsym $mod+F${toString n} exec ${lib.getExe screenSwitch} ${toString n}
71 bindsym $mod+ctrl+F${toString n} exec ${lib.getExe screenSwitch} mark ${toString n}
72 '')
73 [
74 1
75 2
76 3
77 4
78 5
79 6
80 7
81 8
82 9
83 ]
84 );
85
86 # Mark a window and jump to it later, per slot. mod+ctrl+<digit> marks
87 # the focused window (by container id) into that slot. mod+<digit> jumps
88 # to the marked window: if its workspace is hidden it is moved to the
89 # current screen, if it is shown on another screen focus switches there.
90 # mod+shift+<digit> always brings the marked window's workspace to the
91 # current screen. A stale mark (window closed) is dropped with a note.
92 windowMark = pkgs.writeShellApplication {
93 name = "sway-window-mark";
94 runtimeInputs = [
95 pkgs.sway
96 pkgs.jq
97 pkgs.coreutils
98 pkgs.libnotify
99 ];
100 text = ''
101 MARKS="$HOME/.config/sway-window-marks"
102 mkdir -p "$MARKS"
103
104 if [ "$1" = "mark" ]; then
105 SLOT="$2"
106 INFO=$(swaymsg -r -t get_tree | jq -r '
107 [ .. | objects | select(.type == "con" and .focused == true) ]
108 | .[0] // empty
109 | [ .id, (.name // "unknown") ] | @tsv')
110 if [ -z "$INFO" ]; then
111 notify-send "Window" "no window focused" 2>/dev/null
112 exit 0
113 fi
114 IFS=$'\t' read -r ID NAME <<< "$INFO"
115 echo "$ID" > "$MARKS/$SLOT"
116 notify-send "Window" "slot $SLOT: $NAME" 2>/dev/null
117 exit 0
118 fi
119
120 SLOT="$1"
121 ID=$(cat "$MARKS/$SLOT" 2>/dev/null || true)
122 if [ -z "$ID" ]; then
123 notify-send "Window" "no window in slot $SLOT" 2>/dev/null
124 exit 0
125 fi
126
127 # Workspace that currently contains the marked window (empty if it died).
128 WS=$(swaymsg -r -t get_tree | jq -r --argjson id "$ID" '
129 . as $t
130 | [ path(.. | objects | select(.id == $id)) ] | .[0] as $p
131 | if $p == null then empty
132 else [ range(1; ($p | length) + 1) as $i
133 | $t | getpath($p[0:$i])
134 | objects | select(.type == "workspace") | .name
135 ] | .[0] // empty
136 end')
137 if [ -z "$WS" ]; then
138 rm -f "$MARKS/$SLOT"
139 notify-send "Window" "slot $SLOT: marked window is gone" 2>/dev/null
140 exit 0
141 fi
142
143 # Screen the user is typing on, captured BEFORE the switch: switching
144 # a workspace moves seat focus to the target's output, so the
145 # focused output after the switch would be the wrong answer.
146 CUR=$(swaymsg -r -t get_outputs | jq -r '.[] | select(.focused == true) | .name')
147 # Shown on any output? A plain jump only brings the workspace when
148 # it is hidden; "bring" always moves it here.
149 SHOWN=$(swaymsg -r -t get_workspaces | jq -r --arg ws "$WS" '[.[] | select(.name == $ws) | .visible] | any')
150 MOVE=false
151 if [ $# -gt 1 ] && [ "$2" = "bring" ]; then
152 MOVE=true
153 elif [ "$SHOWN" != "true" ]; then
154 MOVE=true
155 fi
156 swaymsg workspace "$WS"
157 # Sway 1.12: nameless "move workspace to output <out>" moves the
158 # focused (just-switched) workspace. A named first argument is
159 # parsed as a container move to a workspace literally named
160 # "<ws> to output <out>".
161 if [ "$MOVE" = "true" ]; then
162 swaymsg move workspace to output "$CUR"
163 fi
164 swaymsg "[con_id=$ID] focus"
165 '';
166 };
167
168 # mod+<digit> = jump to window slot, mod+shift+<digit> = bring its
169 # workspace here, mod+ctrl+<digit> = mark focused window into slot.
170 windowBindings = lib.concatStringsSep "\n" (
171 lib.map
172 (n: ''
173 bindsym $mod+${toString n} exec ${lib.getExe windowMark} ${toString n}
174 bindsym $mod+shift+${toString n} exec ${lib.getExe windowMark} ${toString n} bring
175 bindsym $mod+ctrl+${toString n} exec ${lib.getExe windowMark} mark ${toString n}
176 '')
177 [
178 1
179 2
180 3
181 4
182 5
183 6
184 7
185 8
186 9
187 ]
188 );
189
190in
191{
192 # Make goto-workspace available system-wide for workspace switching
193 environment.systemPackages = [ gotoWorkspace ];
194
195 # Firefox as a persistent systemd user service — starts after workspace-manager
196 # so the sway rule to hide the anchor window is already registered
197 systemd.user.services.firefox = {
198 description = "Firefox Web Browser";
199 partOf = [ "sway-session.target" ];
200 wantedBy = [ "sway-session.target" ];
201 after = [
202 "sway-session.target"
203 "workspace-manager.service"
204 ];
205 # Never restart on `nixos-rebuild switch` — that would kill all open
206 # browser windows. A new Firefox build is only picked up at the next
207 # sway session start.
208 restartIfChanged = false;
209 stopIfChanged = false;
210 serviceConfig = {
211 Type = "simple";
212 ExecStart = lib.getExe self.packages.${pkgs.system}.firefox;
213 Restart = "always";
214 RestartSec = 2;
215 };
216 };
217 imports = [
218 ../lib/wayland.nix
219 ./noctalia.nix
220 ];
221 programs.sway = {
222 enable = true;
223 wrapperFeatures.gtk = true;
224 # Do NOT hardcode SWAYSOCK here: sway >=1.11 ignores an inherited value and
225 # creates its own per-pid socket (sway-ipc.$uid.$pid.sock). Exporting a
226 # fixed path baked a dead socket into the firefox/workspace-manager user
227 # services, breaking all swaymsg calls (and thus tab restore). Let sway
228 # advertise the real socket; the import-environment below propagates it.
229 };
230
231 # Enable realtime scheduling for sway
232 security.pam.loginLimits = [
233 {
234 domain = "@users";
235 item = "rtprio";
236 type = "-";
237 value = 1;
238 }
239 ];
240
241 systemd.user.services.swayidle = {
242 description = "Idle manager for Wayland";
243 documentation = [ "man:swayidle(1)" ];
244 partOf = [ "sway-session.target" ];
245 wantedBy = [ "sway-session.target" ];
246 serviceConfig = {
247 Type = "simple";
248 ExecStart = ''
249 ${pkgs.swayidle}/bin/swayidle -w \
250 timeout 120 '${lib.getExe' pkgs.systemd "systemctl"} --user start lock.target' \
251 timeout 300 '${lib.getExe' pkgs.systemd "systemctl"} suspend-then-hibernate' \
252 before-sleep '${lib.getExe' pkgs.systemd "systemctl"} --user start lock.target'
253 '';
254 Restart = "on-failure";
255 RestartSec = 1;
256 };
257 };
258
259 # Keep new windows on the workspace of the process that spawned them instead
260 # of the focused one (sway only tracks this for its own `exec`, and only for
261 # 60s).
262 systemd.user.services.sway-spawn-workspace = {
263 description = "Place new windows on their spawning process's workspace";
264 partOf = [ "sway-session.target" ];
265 wantedBy = [ "sway-session.target" ];
266 serviceConfig = {
267 Type = "simple";
268 ExecStart = lib.getExe self.packages.${pkgs.system}.sway-spawn-workspace;
269 Restart = "on-failure";
270 RestartSec = 2;
271 };
272 };
273
274 # Inhibit idle when audio is playing (prevents suspend during calls/music)
275 systemd.user.services.sway-audio-idle-inhibit = {
276 description = "Inhibit idle when audio is playing";
277 partOf = [ "sway-session.target" ];
278 wantedBy = [ "sway-session.target" ];
279 serviceConfig = {
280 Type = "simple";
281 ExecStart = "${pkgs.sway-audio-idle-inhibit}/bin/sway-audio-idle-inhibit";
282 Restart = "on-failure";
283 RestartSec = 1;
284 };
285 };
286
287 systemd.user.services.sway-urgent-rumble = {
288 description = "Trigger rumble on urgent windows";
289 partOf = [ "sway-session.target" ];
290 wantedBy = [ "sway-session.target" ];
291 serviceConfig = {
292 Type = "simple";
293 ExecStart = lib.getExe (
294 pkgs.writeShellApplication {
295 name = "sway-urgent-rumble";
296 runtimeInputs = [
297 pkgs.sway
298 pkgs.jq
299 self.packages.${pkgs.system}.gpd-rumble
300 ];
301 text = ''
302 # Find first force-feedback capable device
303 FF_DEVICE=""
304 for ev in /sys/class/input/event*; do
305 if [ -f "$ev/device/capabilities/ff" ] && [ "$(cat "$ev/device/capabilities/ff")" != "0" ]; then
306 FF_DEVICE="/dev/input/''${ev##*/}"
307 break
308 fi
309 done
310
311 if [ -z "$FF_DEVICE" ]; then
312 echo "No force-feedback device found, exiting"
313 exit 0
314 fi
315
316 echo "Using force-feedback device: $FF_DEVICE"
317
318 swaymsg -t subscribe -m '["window"]' | \
319 jq --unbuffered -r 'select(.change == "urgent" and .container.urgent == true) | "urgent"' | \
320 while read -r _; do
321 gpd-rumble 200 100 "$FF_DEVICE" &
322 done
323 '';
324 }
325 );
326 Restart = "on-failure";
327 RestartSec = 1;
328 };
329 };
330
331 environment.etc."sway/config".text = ''
332 # Default config for sway
333 #
334 # Copy this to ~/.config/sway/config and edit it to your liking.
335 #
336 # Read `man 5 sway` for a complete reference.
337
338 ### Variables
339 #
340 # Logo key. Use Mod1 for Alt.
341 set $mod Mod4
342 # Home row direction keys, like vim
343 set $left h
344 set $down j
345 set $up k
346 set $right l
347 # Your preferred terminal emulator
348 set $term ${term}
349 # Your preferred application launcher
350 # Note: pass the final command to swaymsg so that the resulting window can be opened
351 # on the original workspace that the command was run on.
352 set $menu ${pkgs.dmenu}/bin/dmenu_path | menu | xargs swaymsg exec --
353
354 ### Output configuration
355 #
356 # Default wallpaper (more resolutions are available in @datadir@/backgrounds/sway/)
357 # output * bg @datadir@/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill
358 #
359 # Example configuration:
360 #
361 # output HDMI-A-1 resolution 1920x1080 position 1920,0
362 #
363 # You can get the names of your outputs by running: swaymsg -t get_outputs
364
365 # Per-machine output overrides (scale, resolution, position) live here.
366 include /etc/sway/config.d/*.conf
367
368 ### Idle configuration
369 #
370 # Example configuration:
371 #
372 # exec swayidle -w \
373 # timeout 300 'swaylock -f -c 000000' \
374 # timeout 600 'swaymsg "output * power off"' resume 'swaymsg "output * power on"' \
375 # before-sleep 'swaylock -f -c 000000'
376 #
377 # This will lock your screen after 300 seconds of inactivity, then turn off
378 # your displays after another 300 seconds, and turn your screens back on when
379 # resumed. It will also lock your screen before your computer goes to sleep.
380
381 ### Input configuration
382 #
383 # Example configuration:
384 #
385 input "2321:21128:HTIX5288:00_0911:5288_Touchpad" {
386 tap enabled
387 }
388 input "2362:628:PIXA3854:00_093A:0274_Touchpad" {
389 tap enabled
390 }
391 #
392 # You can get the names of your inputs by running: swaymsg -t get_inputs
393 # Read `man 5 sway-input` for more information about this section.
394
395 ### Key bindings
396 #
397 # Basics:
398 #
399 # Start a terminal
400 bindsym $mod+Shift+Return exec $term
401
402 # Kill focused window
403 bindsym $mod+Shift+c kill
404
405 # Start your launcher
406 bindsym $mod+d exec $menu
407
408 # Drag floating windows by holding down $mod and left mouse button.
409 # Resize them with right mouse button + $mod.
410 # Despite the name, also works for non-floating windows.
411 # Change normal to inverse to use left mouse button for resizing and right
412 # mouse button for dragging.
413 floating_modifier $mod normal
414
415 # Reload the configuration file
416 bindsym $mod+Shift+r reload
417
418 # Exit sway (logs you out of your Wayland session)
419 bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -B 'Yes, exit sway' 'swaymsg exit'
420 #
421 # Moving around:
422 #
423 # Move your focus around
424 bindsym $mod+Left focus left
425 bindsym $mod+Down focus down
426 bindsym $mod+Up focus up
427 bindsym $mod+Right focus right
428
429 # Move the focused window with the same, but add Shift
430 bindsym $mod+Shift+Left move left
431 bindsym $mod+Shift+Down move down
432 bindsym $mod+Shift+Up move up
433 bindsym $mod+Shift+Right move right
434 # Layout stuff:
435 #
436 # Split bindings removed to avoid accidental sub-containers.
437 # Use $mod+w to flatten into tabs, $mod+e to toggle split direction.
438 bindsym $mod+n split none
439
440 # Switch the current container between different layout styles
441 bindsym $mod+s layout stacking
442 bindsym $mod+w layout tabbed
443 bindsym $mod+e layout toggle split
444
445 # Make the current focus fullscreen
446 bindsym $mod+f fullscreen
447
448 # Toggle the current focus between tiling and floating mode
449 bindsym $mod+Shift+space floating toggle
450
451 # Swap focus between the tiling area and the floating area
452 bindsym $mod+a focus mode_toggle
453
454 # Workspace manager menu
455 bindsym $mod+space exec ${lib.getExe self.packages.${pkgs.system}.workspace-menu}
456
457 # Move focus to the parent container
458 bindsym $mod+Shift+a focus parent
459 #
460 # Scratchpad:
461 #
462 # Sway has a "scratchpad", which is a bag of holding for windows.
463 # You can send windows there and get them back later.
464
465 # Move the currently focused window to the scratchpad
466 bindsym $mod+Shift+minus move scratchpad
467
468 # Show the next scratchpad window or hide the focused scratchpad window.
469 # If there are multiple scratchpad windows, this command cycles through them.
470 bindsym $mod+minus scratchpad show
471 #
472 # Resizing containers:
473 #
474 mode "resize" {
475 # left will shrink the containers width
476 # right will grow the containers width
477 # up will shrink the containers height
478 # down will grow the containers height
479 bindsym $left resize shrink width 10px
480 bindsym $down resize grow height 10px
481 bindsym $up resize shrink height 10px
482 bindsym $right resize grow width 10px
483
484 # Ditto, with arrow keys
485 bindsym Left resize shrink width 10px
486 bindsym Down resize grow height 10px
487 bindsym Up resize shrink height 10px
488 bindsym Right resize grow width 10px
489
490 # Return to default mode
491 bindsym Return mode "default"
492 bindsym Escape mode "default"
493 }
494 bindsym $mod+r mode "resize"
495
496 default_border pixel 1
497 default_floating_border none
498 smart_borders on
499
500 bindsym $mod+q exec ${pkgs.writers.writeDash "goto_workspace" ''
501 set -efux
502 WS=$(swaymsg -r -t get_workspaces |
503 jq -r '.[].name' |
504 menu -p 'Workspace name: '
505 )
506 if [ -n "$WS" ]; then
507 ${lib.getExe gotoWorkspace} "$WS"
508 fi
509 ''}
510
511 bindsym $mod+Shift+q exec ${pkgs.writers.writeDash "moveto_workspace" ''
512 set -efux
513 WS=$(swaymsg -r -t get_workspaces |
514 jq -r '.[].name' |
515 menu -p 'Workspace name: '
516 )
517 if [ -n "$WS" ]; then
518 swaymsg move container to workspace "$WS"
519 fi
520 ''}
521
522 bindsym $mod+y exec /run/current-system/sw/bin/switch-theme toggle
523
524 bindsym $mod+Tab exec ${pkgs.sway-overfocus}/bin/sway-overfocus split-rw group-rw split-dw group-dw
525 bindsym $mod+Escape workspace back_and_forth
526
527 # Screen switching: mod+F<n> jumps to screen n (1 = primary, 2 = first
528 # secondary, ...), mod+ctrl+F<n> marks the current screen as the target.
529 # While a mark exists, any mod+F<n> jumps to the marked screen.
530 ${screenBindings}
531
532 # Window marks: mod+<digit> jumps to the window marked into that slot,
533 # mod+shift+<digit> brings its workspace to the current screen,
534 # mod+ctrl+<digit> marks the focused window into that slot.
535 ${windowBindings}
536
537 # screenlock (random live video/image from ~/wallpaper via swaylock-plugin)
538 bindsym $mod+F11 exec ${lib.getExe' pkgs.systemd "systemctl"} --user start lock.target
539 bindsym $mod+l exec ${lib.getExe' pkgs.systemd "systemctl"} --user start lock.target
540
541 # kill window (xkill-like)
542 mode "kill" {
543 bindsym --whole-window button1 kill, mode "default"
544 bindsym --whole-window button2 mode "default"
545 bindsym --whole-window button3 mode "default"
546 bindsym Escape mode "default"
547 }
548 bindsym $mod+x mode "kill"
549
550 # media buttons
551 bindsym XF86AudioMute exec ${pkgs.pulseaudio.out}/bin/pactl -- set-sink-mute @DEFAULT_SINK@ toggle
552 bindsym XF86AudioRaiseVolume exec ${pkgs.pulseaudio.out}/bin/pactl -- set-sink-volume @DEFAULT_SINK@ +4%
553 bindsym XF86AudioLowerVolume exec ${pkgs.pulseaudio.out}/bin/pactl -- set-sink-volume @DEFAULT_SINK@ -4%
554
555 # brightness keys
556 bindsym --locked XF86MonBrightnessDown exec brightnessctl set 5%-
557 bindsym --locked XF86MonBrightnessUp exec brightnessctl set 5%+
558
559 # background programs
560 exec ${pkgs.copyq}/bin/copyq --start-server
561 exec ydotoold
562 exec_always pkill kanshi; exec ${pkgs.kanshi}/bin/kanshi
563
564
565 # theme and env specific stuff
566 exec_always ${pkgs.writers.writeDash "dbus-sway-environment" ''
567 set -efux
568 # XDG_SESSION_TYPE=wayland must be exported here: with DISPLAY set but
569 # XDG_SESSION_TYPE unset, Firefox (and other apps) started from the systemd
570 # user manager pick the X11/XWayland screencast path, which captures a black
571 # screen under sway. Setting it explicitly makes them use the PipeWire portal.
572 dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP=sway XDG_SESSION_TYPE=wayland
573 systemctl --user set-environment XDG_SESSION_TYPE=wayland
574 systemctl --user import-environment DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
575 systemctl --user start --no-block sway-session.target
576 systemctl --user stop xdg-desktop-portal xdg-desktop-portal-wlr
577 systemctl --user start xdg-desktop-portal xdg-desktop-portal-wlr
578 ''}
579
580 exec_always ${pkgs.writers.writeDash "gsettings" ''
581 set -efux
582 export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}:$XDG_DATA_DIRS
583 gnome_schema=org.gnome.desktop.interface
584 gsettings set $gnome_schema gtk-theme 'adw-gtk3-dark'
585 ''}
586
587 # keyboard
588 input type:keyboard {
589 xkb_layout us
590 xkb_variant altgr-intl
591 xkb_options caps:hyper
592 }
593
594 # Hide the workspace-manager Firefox anchor window. Registered statically
595 # here (not just by the daemon at runtime) so the rule exists before the
596 # firefox.service starts, otherwise the anchor flashes onto the current
597 # workspace and gets captured as a phantom tab.
598 for_window [title="^workspace-anchor"] move scratchpad
599
600 # flameshot
601 for_window [app_id="flameshot"] border pixel 0, floating enable, fullscreen disable, move absolute position 0 0
602 exec ${pkgs.flameshot}/bin/flameshot
603 '';
604
605}