ef0fc9a5c5
- Polkit authentication agent (lxqt-policykit) as a sway-session user service — programs.sway only enables the daemon, so GUI auth dialogs (nemo mount, NM/blueman) previously failed silently. Corrected the header comment that wrongly claimed the agent was handled system-side. - kanshi for output/display management (safe internal-panel default; a documented template for docked/Cinema-Display profiles). - gammastep night-light (manual location; adjust coordinates). - inhibit_idle on fullscreen so video doesn't get blanked/locked. - logind lid policy on the laptops: suspend on battery, lock on AC. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 ../swaywm.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 ../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') ";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
}
|