Separate user identity (data) from the reusable modules, and let a host declare any number of users instead of exactly one. - users/registry.nix: per-user identity (name, email, groups, authorized and signing keys) as the single source of identity; no user data is hardcoded in the modules. - mkHost takes a `users` set keyed by username; per-user identity is injected into each home config via the `identity` module arg (extraSpecialArgs is per-host, so it cannot carry per-user data). - modules/users.nix builds accounts from the registry; modules/ssh.nix no longer defines authorized keys (the registry owns them); home/git.nix and home/desktop.nix read `identity`; users/emmathorpe/work.nix drops its now-redundant git identity override. - Restructure the tree: users/, home/, modules/, hosts/, lib/ replace the former lyrathorpe/ and system/ layout. - Add standalone homeConfigurations (the portable subset: shell, git, editor, claude) and an exported homeModules output for use on machines not managed by this flake, or as an input to other flakes. Behaviour-preserving for existing hosts: lyrathorpe-mbp and emmathorpe-edaas evaluate to identical derivations; lyrathorpe-t400, lyrathorpe-macpro31 and lyrathorpe-rpi5 differ only by de-duplicating a repeated authorized_keys entry. Fixes the SSH authorized-key leak (one user's key was applied to every account), the hardcoded default git identity, and the hardcoded EDaaS linger setting.
527 lines
19 KiB
Nix
527 lines
19 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 the polkit *daemon* come from the system-level
|
|
# programs.sway (see ../modules/sway.nix); package = null below reuses it instead of
|
|
# pulling a second Sway. The polkit authentication *agent* (the thing that draws
|
|
# the GUI auth dialog) is a user service started here. home-manager owns the user
|
|
# config (~/.config/sway) and wires the systemd user session (sway-session.target),
|
|
# which is what lets the agent/swayidle/dunst/kanshi 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 ../lib/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";
|
|
}
|
|
# Don't let swayidle blank/lock during fullscreen video. Two rules cover
|
|
# native Wayland (app_id) and XWayland (class) clients.
|
|
{
|
|
criteria.app_id = ".*";
|
|
command = "inhibit_idle fullscreen";
|
|
}
|
|
{
|
|
criteria.class = ".*";
|
|
command = "inhibit_idle fullscreen";
|
|
}
|
|
];
|
|
|
|
# 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 = {
|
|
# Nerd Font: monospace plus the powerline/Nerd glyphs the tmux
|
|
# statusline uses (otherwise they render as blank/"?").
|
|
font = "JetBrainsMono Nerd Font:size=11";
|
|
# 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;
|
|
|
|
# Polkit authentication agent. programs.sway (system) enables the polkit
|
|
# daemon but no agent, so GUI privilege prompts (nemo mounting a disk,
|
|
# NetworkManager/blueman editing a system resource) would otherwise fail
|
|
# silently. lxqt-policykit is a small, toolkit-light agent; bind it to the
|
|
# Sway session so it starts and stops with the desktop.
|
|
systemd.user.services.polkit-lxqt = {
|
|
Unit = {
|
|
Description = "lxqt-policykit polkit authentication agent";
|
|
PartOf = [ "graphical-session.target" ];
|
|
After = [ "graphical-session.target" ];
|
|
};
|
|
Service = {
|
|
ExecStart = "${pkgs.lxqt.lxqt-policykit}/bin/lxqt-policykit-agent";
|
|
Restart = "on-failure";
|
|
};
|
|
Install.WantedBy = [ "sway-session.target" ];
|
|
};
|
|
|
|
# Output/display management. Reacts to hotplug and applies per-display
|
|
# mode/scale/position. Profiles are hardware-specific: the safe default below
|
|
# just enables the internal laptop panel; add docked/desktop profiles with the
|
|
# real identifiers from `swaymsg -t get_outputs` (e.g. the Mac Pro's Apple
|
|
# Cinema Display with its scale, or a docked laptop + external monitor).
|
|
services.kanshi = {
|
|
enable = true;
|
|
settings = [
|
|
{
|
|
profile.name = "undocked";
|
|
profile.outputs = [
|
|
{
|
|
criteria = "eDP-1";
|
|
status = "enable";
|
|
}
|
|
];
|
|
}
|
|
# Example to copy per host (fill in real criteria/mode/scale/position):
|
|
# {
|
|
# profile.name = "desktop";
|
|
# profile.outputs = [
|
|
# { criteria = "Apple Computer Inc Cinema HD ..."; mode = "2560x1600"; scale = 1.0; position = "0,0"; status = "enable"; }
|
|
# ];
|
|
# }
|
|
];
|
|
};
|
|
|
|
# Night light. Manual location (no geoclue dependency); adjust the coordinates
|
|
# to taste. Warmer at night, neutral by day.
|
|
services.gammastep = {
|
|
enable = true;
|
|
provider = "manual";
|
|
latitude = 51.5;
|
|
longitude = -0.13; # London-ish; set to your actual location
|
|
temperature = {
|
|
day = 6500;
|
|
night = 3700;
|
|
};
|
|
};
|
|
|
|
# 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') ";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
}
|