Files
nixfiles/lyrathorpe/home/sway.nix
T
Emma Thorpe b04391809e feat(sway): port keybindings, modes and tweaks from dotfiles
Adapted from emmaisadev/dotfiles (sway/config.d) into the Nix config:

- Screenshots to swappy: Print = drag a region, Shift+Print = focused
  window (a writeShellScript reusing the grimshot.sh tree/jq logic).
  Replaces the old plain full-screen grim->file.
- Workspace cycle Mod+z / Mod+x (prev/next).
- Media keys (playerctl) and mic mute (wpctl source).
- Re-home `focus mode_toggle` onto Mod+Alt+space (Mod+Space is the
  launcher now).
- Clipboard history: services.clipman stores copies; Mod+c picks one
  through a Catppuccin-themed fuzzel (programs.fuzzel).
- Binding modes: a layout submenu (Mod+y -> s/w/e, which also restores
  split-toggle that Mod+e gave up to nemo) and a power menu
  (Mod+Shift+x -> lock/exit/sleep/reboot/shutdown). Mod+l still locks
  immediately.
- Touchpad tap + natural scroll (laptops; inert on desktop).
- Solid Catppuccin base as the wallpaper (output * bg, no image).
- foot: term=xterm-256color and scrollback 100000 (colours unchanged).
- swaywm.nix: add slurp/swappy/jq/playerctl to the session packages.

Skipped from the dotfiles: named workspaces + app auto-assign, the foot
--server/footclient setup, and pactl/MX-Master/lxqt device-specific bits.
All in the shared files, so every Sway host gets it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:49:36 +01:00

454 lines
16 KiB
Nix

# Declarative Sway window manager, status bar, lock, idle and notifications.
# Imported via ./desktop.nix, so only graphical hosts get it.
#
# The compositor binary, PAM and polkit integration come from the system-level
# programs.sway (see ../swaywm.nix); package = null below reuses it instead of
# pulling a second Sway. home-manager owns the user config (~/.config/sway) and
# wires the systemd user session (sway-session.target), which is what lets the
# swayidle/dunst user services start with the desktop.
{
pkgs,
lib,
# Threaded from mkHost (flake.nix). Desktop hosts set this false to drop
# mobile components (battery block, screen-brightness keys).
portable ? true,
...
}:
let
# Catppuccin Mocha (shared with the ReGreet greeter). Raw hex; prefix "#"
# where a consumer needs it -- Sway/i3status/dunst want "#", foot/swaylock do
# not.
ctp = import ../catppuccin-mocha.nix;
# Focused-window screenshot -> swappy editor (the dotfiles' grimshot.sh logic).
# Full store paths so it needs nothing on PATH.
screenshotWindow = pkgs.writeShellScript "screenshot-window" ''
${pkgs.grim}/bin/grim -g "$(${pkgs.sway}/bin/swaymsg -t get_tree \
| ${pkgs.jq}/bin/jq -r '.. | select(.focused?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"')" \
- | ${pkgs.swappy}/bin/swappy -f -
'';
# Binding-mode names. The string is both the `modes` attr key and what the
# bar's mode indicator shows, so the keys are spelled out in the label.
layoutMode = "layout: [s]tacking [w]tabbed [e]split";
systemMode = "system: [l]ock [e]xit [s]leep [r]eboot [Shift+s]shutdown";
in
{
wayland.windowManager.sway = {
enable = true;
package = null;
# `sway --validate` needs a real package; skip it since package = null.
checkConfig = false;
config = rec {
modifier = "Mod4";
terminal = "${pkgs.foot}/bin/foot";
# Launcher: sway-launcher-desktop running inside a floating foot window.
menu = "${pkgs.foot}/bin/foot --app-id=launcher ${pkgs.sway-launcher-desktop}/bin/sway-launcher-desktop";
# Dvorak is a variant of the "us" layout, not a standalone layout --
# `xkb_layout = "dvorak"` fails to compile (no symbols/dvorak) and wlroots
# silently falls back to QWERTY. Use the variant.
input."type:keyboard" = {
xkb_layout = "us";
xkb_variant = "dvorak";
};
# Touchpads (laptops): tap-to-click and natural scrolling. Inert on the
# desktop hosts, which have no touchpad.
input."type:touchpad" = {
tap = "enabled";
natural_scroll = "enabled";
};
# Solid Catppuccin Mocha base as the wallpaper (no image dependency).
output."*".bg = "#${ctp.base} solid_color";
# Window borders -- Catppuccin Mocha (blue accent on the focused window).
colors = {
focused = {
border = "#${ctp.blue}";
background = "#${ctp.base}";
text = "#${ctp.text}";
indicator = "#${ctp.blue}";
childBorder = "#${ctp.blue}";
};
focusedInactive = {
border = "#${ctp.surface0}";
background = "#${ctp.base}";
text = "#${ctp.subtext0}";
indicator = "#${ctp.surface0}";
childBorder = "#${ctp.surface0}";
};
unfocused = {
border = "#${ctp.surface0}";
background = "#${ctp.base}";
text = "#${ctp.subtext0}";
indicator = "#${ctp.surface0}";
childBorder = "#${ctp.surface0}";
};
urgent = {
border = "#${ctp.red}";
background = "#${ctp.base}";
text = "#${ctp.text}";
indicator = "#${ctp.red}";
childBorder = "#${ctp.red}";
};
};
window.commands = [
{
criteria.app_id = "launcher";
command = "floating enable, resize set 800 500";
}
];
# Binding modes (submenus). Entered from keybindings below; each action
# returns to the default mode. mkOptionDefault-merged with the module's
# built-in "resize" mode.
modes = {
# Layout submenu (Mod+y). Mirrors Sway's default s/w/e layout keys --
# notably it restores split-toggle, which moved off Mod+e when that
# became the nemo launcher.
${layoutMode} = {
"s" = "layout stacking, mode default";
"w" = "layout tabbed, mode default";
"e" = "layout toggle split, mode default";
"Return" = "mode default";
"Escape" = "mode default";
};
# Power menu (Mod+Shift+x). Lock reuses the themed swaylock; the rest go
# through systemd/logind (allowed for the active local session).
${systemMode} = {
"l" = "exec ${pkgs.swaylock}/bin/swaylock -f, mode default";
"e" = "exec ${pkgs.sway}/bin/swaymsg exit, mode default";
"s" = "exec systemctl suspend, mode default";
"r" = "exec systemctl reboot, mode default";
"Shift+s" = "exec systemctl poweroff, mode default";
"Return" = "mode default";
"Escape" = "mode default";
};
};
bars = [
{
position = "top";
statusCommand = "${pkgs.i3status-rust}/bin/i3status-rs ~/.config/i3status-rust/config-default.toml";
fonts = {
names = [
"Noto Sans"
"Font Awesome 6 Free"
];
size = 11.0;
};
# Bar background + workspace buttons -- Catppuccin Mocha. The mantle
# background matches i3status-rust's idle_bg below for a uniform strip.
colors = {
background = "#${ctp.mantle}";
statusline = "#${ctp.text}";
separator = "#${ctp.surface0}";
focusedWorkspace = {
border = "#${ctp.blue}";
background = "#${ctp.blue}";
text = "#${ctp.base}";
};
activeWorkspace = {
border = "#${ctp.surface0}";
background = "#${ctp.surface0}";
text = "#${ctp.text}";
};
inactiveWorkspace = {
border = "#${ctp.mantle}";
background = "#${ctp.mantle}";
text = "#${ctp.subtext0}";
};
urgentWorkspace = {
border = "#${ctp.red}";
background = "#${ctp.red}";
text = "#${ctp.base}";
};
};
}
];
# NB: this whole set is wrapped in mkOptionDefault so it MERGES with the
# home-manager module's default keybindings (same priority) rather than
# replacing them. Do not wrap it in mkMerge with a normal-priority attr --
# that makes the normal-priority def win and silently drops every default
# bind (terminal, movement, workspaces, ...).
keybindings = lib.mkOptionDefault (
{
# Launcher on Mod+Space. mkForce overrides the module's own default
# Mod+Space (focus mode_toggle); a plain value would conflict with it
# at equal priority. Mod+d also still runs the launcher (module default).
"${modifier}+space" = lib.mkForce "exec ${menu}";
# File manager. mkForce overrides the module default (layout toggle split).
"${modifier}+e" = lib.mkForce "exec ${pkgs.nemo}/bin/nemo";
"${modifier}+l" = "exec ${pkgs.swaylock}/bin/swaylock -f";
# Cycle workspaces.
"${modifier}+z" = "workspace prev";
"${modifier}+x" = "workspace next";
# focus mode_toggle (tiling <-> floating focus) -- re-homed off
# Mod+Space, which is now the launcher.
"${modifier}+Mod1+space" = "focus mode_toggle";
# Enter the binding-mode submenus defined above.
"${modifier}+y" = "mode \"${layoutMode}\"";
"${modifier}+Shift+x" = "mode \"${systemMode}\"";
# Clipboard history: pick a past entry through fuzzel (clipman stores
# it -- see services.clipman below).
"${modifier}+c" =
"exec ${pkgs.clipman}/bin/clipman pick -t CUSTOM --tool-args=\"${pkgs.fuzzel}/bin/fuzzel --dmenu\"";
# Screenshots -> swappy editor: Print = drag a region, Shift+Print =
# the focused window.
"Print" =
"exec ${pkgs.grim}/bin/grim -g \"$(${pkgs.slurp}/bin/slurp)\" - | ${pkgs.swappy}/bin/swappy -f -";
"Shift+Print" = "exec ${screenshotWindow}";
"XF86AudioRaiseVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+";
"XF86AudioLowerVolume" = "exec ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-";
"XF86AudioMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
"XF86AudioMicMute" = "exec ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle";
# Media keys (MPRIS via playerctl).
"XF86AudioPlay" = "exec ${pkgs.playerctl}/bin/playerctl play-pause";
"XF86AudioNext" = "exec ${pkgs.playerctl}/bin/playerctl next";
"XF86AudioPrev" = "exec ${pkgs.playerctl}/bin/playerctl previous";
}
# Screen backlight: laptops only (no internal backlight on a desktop).
// lib.optionalAttrs portable {
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%+";
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
}
);
};
};
# Terminal: Catppuccin Mocha. foot reads ~/.config/foot/foot.ini; the Sway
# `terminal` above still launches the same binary, now themed.
programs.foot = {
enable = true;
# foot 1.27: the bare [colors] section is deprecated in favour of
# [colors-dark] (the default theme), and the cursor colour moved out of
# [cursor] (where `color` is now rejected) into a `cursor` key here, written
# "<text> <cursor>" (man foot.ini(5): "ff0000 00ff00" => green cursor, red
# text). Only colors-dark is needed; we never set initial-color-theme=light.
settings = {
main = {
# Advertise as xterm-256color so remote hosts without foot's terminfo
# still behave (tmux re-adds foot's RGB/sync/etc. features -- see
# shell.nix). The [main] section is the unheadered top of foot.ini.
term = "xterm-256color";
};
scrollback.lines = 100000;
"colors-dark" = {
background = ctp.base;
foreground = ctp.text;
regular0 = ctp.surface1;
regular1 = ctp.red;
regular2 = ctp.green;
regular3 = ctp.yellow;
regular4 = ctp.blue;
regular5 = ctp.pink;
regular6 = ctp.teal;
regular7 = ctp.subtext1;
bright0 = ctp.surface2;
bright1 = ctp.red;
bright2 = ctp.green;
bright3 = ctp.yellow;
bright4 = ctp.blue;
bright5 = ctp.pink;
bright6 = ctp.teal;
bright7 = ctp.subtext0;
"selection-foreground" = ctp.base;
"selection-background" = ctp.rosewater;
cursor = "${ctp.base} ${ctp.rosewater}";
};
};
};
# Clipboard history: a user service runs `wl-paste --watch clipman store`,
# bound to the Wayland session, so copies persist and Mod+c (above) can pick
# an old entry through fuzzel.
services.clipman.enable = true;
# fuzzel: the dmenu picker used by clipman, themed Catppuccin Mocha to match
# (fuzzel colours are RRGGBBAA -- 8 hex digits).
programs.fuzzel = {
enable = true;
settings = {
main = {
font = "Noto Sans:size=12";
prompt = "\"clipboard \"";
};
border = {
width = 2;
radius = 8;
};
colors = {
background = "${ctp.base}f0";
text = "${ctp.text}ff";
prompt = "${ctp.subtext0}ff";
input = "${ctp.text}ff";
match = "${ctp.blue}ff";
selection = "${ctp.surface1}ff";
selection-text = "${ctp.text}ff";
selection-match = "${ctp.blue}ff";
border = "${ctp.blue}ff";
};
};
};
programs.swaylock = {
enable = true;
# Catppuccin Mocha (swaylock colours are hex without "#").
settings = {
color = ctp.base;
indicator-radius = 100;
indicator-thickness = 7;
show-failed-attempts = true;
font = "Noto Sans";
inside-color = ctp.base;
inside-clear-color = ctp.base;
inside-ver-color = ctp.base;
inside-wrong-color = ctp.base;
ring-color = ctp.surface1;
ring-clear-color = ctp.yellow;
ring-ver-color = ctp.blue;
ring-wrong-color = ctp.red;
key-hl-color = ctp.blue;
bs-hl-color = ctp.red;
line-color = ctp.base;
line-clear-color = ctp.base;
line-ver-color = ctp.base;
line-wrong-color = ctp.base;
separator-color = "00000000";
text-color = ctp.text;
text-clear-color = ctp.text;
text-ver-color = ctp.text;
text-wrong-color = ctp.text;
};
};
# Lock on idle, turn screens off shortly after, and lock before sleep.
services.swayidle = {
enable = true;
timeouts = [
{
timeout = 300;
command = "${pkgs.swaylock}/bin/swaylock -f";
}
{
timeout = 600;
command = "${pkgs.sway}/bin/swaymsg 'output * power off'";
resumeCommand = "${pkgs.sway}/bin/swaymsg 'output * power on'";
}
];
events = {
before-sleep = "${pkgs.swaylock}/bin/swaylock -f";
lock = "${pkgs.swaylock}/bin/swaylock -f";
};
};
services.dunst = {
enable = true;
# Catppuccin Mocha notifications (dunst colours need a leading "#").
settings = {
global = {
font = "Noto Sans 11";
frame_color = "#${ctp.blue}";
frame_width = 2;
separator_color = "frame";
offset = "10x10";
corner_radius = 5;
};
urgency_low = {
background = "#${ctp.base}";
foreground = "#${ctp.text}";
frame_color = "#${ctp.surface1}";
};
urgency_normal = {
background = "#${ctp.base}";
foreground = "#${ctp.text}";
frame_color = "#${ctp.blue}";
};
urgency_critical = {
background = "#${ctp.base}";
foreground = "#${ctp.text}";
frame_color = "#${ctp.peach}";
timeout = 0;
};
};
};
programs.i3status-rust = {
enable = true;
bars.default = {
# Catppuccin Mocha: a flat "plain" base recoloured via overrides. Idle
# blocks sit on mantle (matching the Sway bar background) with light text;
# only warning/critical states get a loud tinted background. The `theme`
# bar option is shallow-merged away by `settings.theme`, so set the base
# theme and its overrides together here.
icons = "awesome6";
settings.theme = {
theme = "plain";
overrides = {
idle_bg = "#${ctp.mantle}";
idle_fg = "#${ctp.text}";
info_bg = "#${ctp.mantle}";
info_fg = "#${ctp.blue}";
good_bg = "#${ctp.mantle}";
good_fg = "#${ctp.green}";
warning_bg = "#${ctp.peach}";
warning_fg = "#${ctp.base}";
critical_bg = "#${ctp.red}";
critical_fg = "#${ctp.base}";
separator_bg = "#${ctp.mantle}";
separator_fg = "#${ctp.surface1}";
};
};
blocks = [
{
block = "disk_space";
path = "/";
format = " $icon $available ";
}
{
block = "memory";
format = " $icon $mem_used_percents ";
}
{
block = "cpu";
interval = 2;
}
]
# Desktop-only: CPU temperature and wired network throughput, in place
# of the laptop's battery readout.
++ lib.optionals (!portable) [
{
block = "temperature";
interval = 5;
format = " $icon $average avg, $max max ";
}
{ block = "net"; }
]
++ [ { block = "sound"; } ]
++ lib.optional portable { block = "battery"; }
++ [
{
block = "time";
interval = 5;
format = " $timestamp.datetime(f:'%a %d/%m %R') ";
}
];
};
};
}