diff --git a/flake.lock b/flake.lock index 90d8c338..195bae66 100644 --- a/flake.lock +++ b/flake.lock @@ -375,6 +375,62 @@ "type": "github" } }, + "niri": { + "inputs": { + "niri-stable": "niri-stable", + "niri-unstable": "niri-unstable", + "nixpkgs": "nixpkgs_6", + "nixpkgs-stable": "nixpkgs-stable", + "xwayland-satellite-stable": "xwayland-satellite-stable", + "xwayland-satellite-unstable": "xwayland-satellite-unstable" + }, + "locked": { + "lastModified": 1783526091, + "narHash": "sha256-Zkkil0e8Wg5BcBmJkWJwBXlETefYQbYKw72xJUIBPLE=", + "owner": "sodiboo", + "repo": "niri-flake", + "rev": "a24d4c0dce53fd8dbeb9ad20411e282c7b9875f1", + "type": "github" + }, + "original": { + "owner": "sodiboo", + "repo": "niri-flake", + "type": "github" + } + }, + "niri-stable": { + "flake": false, + "locked": { + "lastModified": 1756556321, + "narHash": "sha256-RLD89dfjN0RVO86C/Mot0T7aduCygPGaYbog566F0Qo=", + "owner": "YaLTeR", + "repo": "niri", + "rev": "01be0e65f4eb91a9cd624ac0b76aaeab765c7294", + "type": "github" + }, + "original": { + "owner": "YaLTeR", + "ref": "v25.08", + "repo": "niri", + "type": "github" + } + }, + "niri-unstable": { + "flake": false, + "locked": { + "lastModified": 1783522755, + "narHash": "sha256-dI0HkX1djETia7cD/Y64h8BNIsSOfTRMzfNum2J6UhE=", + "owner": "YaLTeR", + "repo": "niri", + "rev": "0777769e719b7c9b7c980d4ea66288bfbb4da5b3", + "type": "github" + }, + "original": { + "owner": "YaLTeR", + "repo": "niri", + "type": "github" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -439,7 +495,7 @@ }, "nixos-hardware": { "inputs": { - "nixpkgs": "nixpkgs_6" + "nixpkgs": "nixpkgs_7" }, "locked": { "lastModified": 1783370751, @@ -487,6 +543,22 @@ "type": "github" } }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1782847189, + "narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b6018f87da91d19d0ab4cf979885689b469cdd41", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-unstable": { "locked": { "lastModified": 1783279667, @@ -568,6 +640,22 @@ } }, "nixpkgs_6": { + "locked": { + "lastModified": 1783224372, + "narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d407951447dcd00442e97087bf374aad70c04cea", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_7": { "locked": { "lastModified": 1767892417, "narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=", @@ -580,7 +668,7 @@ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, - "nixpkgs_7": { + "nixpkgs_8": { "locked": { "lastModified": 1783389287, "narHash": "sha256-0xIy4dVLqq47rA+mRy0hXDfjhQd4E5PoIns/RmB7nR4=", @@ -621,29 +709,6 @@ "type": "github" } }, - "plasma-manager": { - "inputs": { - "home-manager": [ - "home-manager" - ], - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1783534709, - "narHash": "sha256-XyWsinAFbRgA+p6oAJ5ejdT43aCmepNpKGkU7UKEMps=", - "owner": "nix-community", - "repo": "plasma-manager", - "rev": "a8c78043e8d8601a41c140ce4e2758121bf947e7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "plasma-manager", - "type": "github" - } - }, "pre-commit-hooks": { "inputs": { "flake-compat": "flake-compat_2", @@ -672,13 +737,13 @@ "glide": "glide", "home-manager": "home-manager", "mac-app-util": "mac-app-util", + "niri": "niri", "nix-darwin": "nix-darwin", "nix-tresorit": "nix-tresorit", "nixos-avf": "nixos-avf", "nixos-hardware": "nixos-hardware", - "nixpkgs": "nixpkgs_7", + "nixpkgs": "nixpkgs_8", "nixpkgs-unstable": "nixpkgs-unstable", - "plasma-manager": "plasma-manager", "pre-commit-hooks": "pre-commit-hooks", "spicetify-nix": "spicetify-nix", "steam-config-nix": "steam-config-nix", @@ -972,6 +1037,39 @@ "repo": "treefmt-nix", "type": "github" } + }, + "xwayland-satellite-stable": { + "flake": false, + "locked": { + "lastModified": 1755491097, + "narHash": "sha256-m+9tUfsmBeF2Gn4HWa6vSITZ4Gz1eA1F5Kh62B0N4oE=", + "owner": "Supreeeme", + "repo": "xwayland-satellite", + "rev": "388d291e82ffbc73be18169d39470f340707edaa", + "type": "github" + }, + "original": { + "owner": "Supreeeme", + "ref": "v0.7", + "repo": "xwayland-satellite", + "type": "github" + } + }, + "xwayland-satellite-unstable": { + "flake": false, + "locked": { + "lastModified": 1781226823, + "narHash": "sha256-28696iIw8uE0ZUyFTtzhEM8xMh85clCYypMxkvUi+sc=", + "owner": "Supreeeme", + "repo": "xwayland-satellite", + "rev": "8575d0ef55d70f9b4c46b6bffb3accf912217e1e", + "type": "github" + }, + "original": { + "owner": "Supreeeme", + "repo": "xwayland-satellite", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 5fde4eed..e87debd0 100644 --- a/flake.nix +++ b/flake.nix @@ -37,11 +37,6 @@ url = "github:different-name/steam-config-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; - plasma-manager = { - url = "github:nix-community/plasma-manager"; - inputs.nixpkgs.follows = "nixpkgs"; - inputs.home-manager.follows = "home-manager"; - }; nixos-hardware.url = "github:NixOS/nixos-hardware/master"; nixos-avf = { url = "github:nix-community/nixos-avf"; @@ -56,6 +51,9 @@ inputs.nixpkgs.follows = "nixpkgs"; inputs.home-manager.follows = "home-manager"; }; + # niri compositor: NixOS + home-manager modules with build-time config + # validation. Keeps its own nixpkgs pin so its binary cache stays hit. + niri.url = "github:sodiboo/niri-flake"; mac-app-util = { # Pinned to nixos-25.11 (sbcl 2.5.10) instead of following our nixos-26.05: # 26.05's sbcl 2.6.4 breaks cl-nix-lite's fare-quasiquote build diff --git a/home/common/glide/default.nix b/home/common/glide/default.nix index 0620341e..e072d536 100644 --- a/home/common/glide/default.nix +++ b/home/common/glide/default.nix @@ -3,6 +3,19 @@ programs.glide-browser = { enable = true; + + policies.SearchEngines = { + Default = "Kagi"; + Remove = [ "Google" "Bing" "DuckDuckGo" "Amazon.com" "eBay" "Wikipedia (en)" "Perplexity" ]; + Add = [{ + Name = "Kagi"; + URLTemplate = "https://kagi.com/search?q={searchTerms}"; + Method = "GET"; + IconURL = "https://kagi.com/favicon.ico"; + SuggestURLTemplate = "https://kagi.com/api/autosuggest?q={searchTerms}"; + Alias = "kagi"; + }]; + }; }; home.file.".config/glide" = { diff --git a/home/common/stylix.nix b/home/common/stylix.nix index 3187e4fb..25828955 100644 --- a/home/common/stylix.nix +++ b/home/common/stylix.nix @@ -10,6 +10,14 @@ in autoEnable = lib.mkDefault true; image = lib.mkDefault null; + # Themed cursor. niri-flake's stylix target reads this to set niri's xcursor + # theme + size; also themes GTK/other apps' cursors. (Global, not niri-only.) + cursor = { + name = lib.mkDefault "Bibata-Modern-Classic"; + package = lib.mkDefault pkgs.bibata-cursors; + size = lib.mkDefault 24; + }; + fonts = { monospace = { name = lib.mkOptionDefault "MonoLisa Nerd Font"; diff --git a/home/linux/default.nix b/home/linux/default.nix index b7fed5c6..ed8d3216 100644 --- a/home/linux/default.nix +++ b/home/linux/default.nix @@ -1,4 +1,4 @@ -{ features, hostName, lib, ... }: +{ features, lib, ... }: { imports = [ ./theme.nix @@ -7,11 +7,7 @@ ./mpv ] ++ lib.optionals features.gaming [ ./games - ] ++ lib.optionals (features.desktopEnvironment == "plasma") [ - ./plasma - ] ++ lib.optionals (features.desktopEnvironment == "sway") [ - ./sway - ] ++ lib.optionals (features.desktopEnvironment != null && features.desktopEnvironment != "sway") [ - ./${features.desktopEnvironment}/host/${hostName}.nix + ] ++ lib.optionals (features.desktopEnvironment == "niri") [ + ./niri ]; } diff --git a/home/linux/desktop.nix b/home/linux/desktop.nix index acfa316f..c003c51a 100644 --- a/home/linux/desktop.nix +++ b/home/linux/desktop.nix @@ -14,6 +14,7 @@ in filezilla flashprint crosspipe + kdePackages.dolphin kdePackages.kdenlive glaxnimate krita diff --git a/home/linux/niri/default.nix b/home/linux/niri/default.nix new file mode 100644 index 00000000..2f2e0d03 --- /dev/null +++ b/home/linux/niri/default.nix @@ -0,0 +1,255 @@ +{ config, lib, pkgs, hostName, features, ... }: +let + isPocket = hostName == "pocket"; + + # Waybar's right-side modules; laptop-only extras added on pocket. + rightModules = + [ "tray" "pulseaudio" "network" ] + ++ lib.optionals isPocket [ "custom/cellular" "backlight" "battery" ] + ++ [ "clock" ]; +in +{ + imports = [ ./host/${hostName}.nix ]; + + # `programs.niri` here comes from niri-flake's NixOS module, which injects its + # home-manager module automatically (via home-manager.sharedModules) — so we + # must NOT import homeModules.niri again. The same auto-wiring pulls in + # niri-flake's stylix target (homeModules.stylix, active because stylix is a + # NixOS module here); it owns the window border + cursor colors, so we don't + # set focus-ring/border/cursor below. + + home.packages = with pkgs; [ + swaylock + brightnessctl + playerctl + ] ++ lib.optionals features.gui [ + # Desktop chrome — skipped on gui = false hosts (eink). + pavucontrol + networkmanagerapplet # nm-connection-editor for the bar's on-click + xwayland-satellite # X11 apps; niri finds it on PATH and spawns it on demand + ]; + + programs = { + # ---- niri compositor config (typed settings DSL) ------------------ + # niri-flake validates this at build time and generates the KDL. We use + # `settings` (not raw `config`) because niri-flake's stylix target only + # writes into `settings` — setting `config` would override it wholesale. + # niri reloads on rebuild-switch; no restart needed. + niri.settings = { + input = { + keyboard.xkb.layout = "us"; + touchpad = { + tap = true; + natural-scroll = true; + }; + focus-follows-mouse = { + enable = true; + max-scroll-amount = "0%"; + }; + warp-mouse-to-focus.enable = true; + }; + + layout = { + gaps = 8; + center-focused-column = "never"; + preset-column-widths = [ + { proportion = 0.33333; } + { proportion = 0.5; } + { proportion = 0.66667; } + { proportion = 1.0; } + ]; + # Open full width — no half-empty screen with a lone window. Mod+R + # cycles down to ⅓/½/⅔ when tiling two side by side. + default-column-width.proportion = 1.0; + # focus-ring / border colors come from stylix (niri-flake stylix target). + # Empty-workspace + overview backdrop have no stylix target, so with + # image = null they'd fall back to niri's gray — wire them by hand. + background-color = "#${config.lib.stylix.colors.base00}"; + }; + + overview.backdrop-color = "#${config.lib.stylix.colors.base01}"; + + # niri keeps each column at its opened width; niri-even-widths (scripts/) + # rebalances landscape workspaces to 1 / min(n,3) so a lone window is full + # width, two split ½, three+ hold at ⅓ and scroll. Skipped on headless. + spawn-at-startup = lib.optionals features.gui [{ argv = [ "niri-even-widths" ]; }]; + + prefer-no-csd = true; + screenshot-path = "~/Pictures/Screenshots/Screenshot-%Y-%m-%d-%H-%M-%S.png"; + hotkey-overlay.skip-at-startup = true; + + window-rules = [{ + geometry-corner-radius = + let r = 6.0; in { top-left = r; top-right = r; bottom-left = r; bottom-right = r; }; + clip-to-geometry = true; + }]; + + binds = with config.lib.niri.actions; { + "Mod+Shift+Slash".action = show-hotkey-overlay; + + "Mod+Return".action = spawn "ghostty"; + "Mod+D".action = spawn "fuzzel"; + "Mod+Q".action = close-window; + "Mod+Alt+L".action = spawn "swaylock"; + + # Focus (vim keys) + "Mod+H".action = focus-column-left; + "Mod+L".action = focus-column-right; + "Mod+J".action = focus-window-down; + "Mod+K".action = focus-window-up; + + # Move + "Mod+Shift+H".action = move-column-left; + "Mod+Shift+L".action = move-column-right; + "Mod+Shift+J".action = move-window-down; + "Mod+Shift+K".action = move-window-up; + + # Monitors (desktop is dual-head) + "Mod+Ctrl+H".action = focus-monitor-left; + "Mod+Ctrl+L".action = focus-monitor-right; + "Mod+Shift+Ctrl+H".action = move-window-to-monitor-left; + "Mod+Shift+Ctrl+L".action = move-window-to-monitor-right; + + "Mod+Home".action = focus-column-first; + "Mod+End".action = focus-column-last; + + # Workspaces + "Mod+1".action = focus-workspace 1; + "Mod+2".action = focus-workspace 2; + "Mod+3".action = focus-workspace 3; + "Mod+4".action = focus-workspace 4; + "Mod+5".action = focus-workspace 5; + "Mod+6".action = focus-workspace 6; + "Mod+7".action = focus-workspace 7; + "Mod+8".action = focus-workspace 8; + "Mod+9".action = focus-workspace 9; + # move-column-to-workspace isn't in config.lib.niri.actions as a bare + # builder (only the -down/-up variants are), so use the path form. + "Mod+Shift+1".action.move-column-to-workspace = 1; + "Mod+Shift+2".action.move-column-to-workspace = 2; + "Mod+Shift+3".action.move-column-to-workspace = 3; + "Mod+Shift+4".action.move-column-to-workspace = 4; + "Mod+Shift+5".action.move-column-to-workspace = 5; + "Mod+Shift+6".action.move-column-to-workspace = 6; + "Mod+Shift+7".action.move-column-to-workspace = 7; + "Mod+Shift+8".action.move-column-to-workspace = 8; + "Mod+Shift+9".action.move-column-to-workspace = 9; + "Mod+U".action = focus-workspace-down; + "Mod+I".action = focus-workspace-up; + + # Sizing / layout + "Mod+F".action = maximize-column; + "Mod+Shift+F".action = fullscreen-window; + "Mod+C".action = center-column; + "Mod+R".action = switch-preset-column-width; + "Mod+Minus".action = set-column-width "-10%"; + "Mod+Equal".action = set-column-width "+10%"; + "Mod+Comma".action = consume-window-into-column; + "Mod+Period".action = expel-window-from-column; + "Mod+Space".action = switch-focus-between-floating-and-tiling; + "Mod+Shift+Space".action = toggle-window-floating; + + # Screenshots (niri built-in). Not bare builders in config.lib.niri.actions; + # use the path form (empty attrset = default props). + "Print".action.screenshot = { }; + "Mod+Print".action.screenshot-screen = { }; + "Mod+Shift+Print".action.screenshot-window = { }; + + # Media / volume / brightness + "XF86AudioRaiseVolume" = { allow-when-locked = true; action = spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.05+"; }; + "XF86AudioLowerVolume" = { allow-when-locked = true; action = spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.05-"; }; + "XF86AudioMute" = { allow-when-locked = true; action = spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }; + "XF86AudioMicMute" = { allow-when-locked = true; action = spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }; + "XF86MonBrightnessUp" = { allow-when-locked = true; action = spawn "brightnessctl" "set" "5%+"; }; + "XF86MonBrightnessDown" = { allow-when-locked = true; action = spawn "brightnessctl" "set" "5%-"; }; + "XF86AudioPlay" = { allow-when-locked = true; action = spawn "playerctl" "play-pause"; }; + "XF86AudioNext" = { allow-when-locked = true; action = spawn "playerctl" "next"; }; + "XF86AudioPrev" = { allow-when-locked = true; action = spawn "playerctl" "previous"; }; + + "Mod+Shift+E".action = quit; + "Mod+Shift+P".action = power-off-monitors; + }; + }; + + # ---- Waybar: KDE-style bottom panel ------------------------------- + waybar = { + enable = features.gui; + systemd.enable = true; + settings.main = { + layer = "top"; + position = "bottom"; + height = 32; + spacing = 6; + modules-left = [ "niri/workspaces" ]; + modules-center = [ "niri/window" ]; + modules-right = rightModules; + + "niri/workspaces".format = "{index}"; + "niri/window" = { + format = "{}"; + max-length = 80; + }; + clock = { + format = "{:%a %d %b %H:%M}"; + tooltip-format = "{:%Y %B}\n{calendar}"; + }; + pulseaudio = { + format = "{icon} {volume}%"; + format-muted = "󰝟 muted"; + format-icons.default = [ "󰕿" "󰖀" "󰕾" ]; + on-click = "pavucontrol"; + }; + network = { + format-wifi = "󰤨 {essid}"; + format-ethernet = "󰈀 {ifname}"; + format-disconnected = "󰤭 off"; + tooltip-format = "{ifname}: {ipaddr}"; + on-click = "nm-connection-editor"; + }; + # Cellular (pocket): waybar's `network` module has no modem support, so + # poll ModemManager directly. `-m any` grabs the first modem. + "custom/cellular" = { + exec = pkgs.writeShellScript "waybar-cellular" '' + kv=$(${pkgs.modemmanager}/bin/mmcli -m any --output-keyvalue 2>/dev/null) \ + || { echo '{"text":""}'; exit 0; } + get() { printf '%s\n' "$kv" | ${pkgs.gnused}/bin/sed -n "s/^$1 *: //p"; } + state=$(get modem.generic.state) + if [ "$state" != connected ]; then + printf '{"text":"󰣲","tooltip":"cellular: %s"}\n' "''${state:-off}"; exit 0 + fi + printf '{"text":"󰄋 %s%%","tooltip":"%s · %s"}\n' \ + "$(get modem.generic.signal-quality.value)" \ + "$(get modem.3gpp.operator-name)" \ + "$(get modem.generic.access-technologies.value)" + ''; + return-type = "json"; + interval = 30; + on-click = "nm-connection-editor"; + }; + tray = { + icon-size = 16; + spacing = 8; + }; + battery = { + format = "{icon} {capacity}%"; + format-charging = "󰂄 {capacity}%"; + format-icons = [ "󰁺" "󰁻" "󰁼" "󰁽" "󰁾" "󰁿" "󰂀" "󰂁" "󰂂" "󰁹" ]; + }; + backlight = { + format = "󰃟 {percent}%"; + on-scroll-up = "brightnessctl set 5%+"; + on-scroll-down = "brightnessctl set 5%-"; + }; + }; + # No `style`: stylix.targets.waybar owns the CSS (colors + font), themed + # to the active base16 scheme. Module layout/config stays ours above. + }; + + fuzzel.enable = true; + }; + + services.mako.enable = features.gui; + # Network management is the waybar `network` module (on-click + # nm-connection-editor) + blueman for bluetooth; no separate nm-applet tray. + # The polkit agent comes from niri-flake (niri-flake-polkit / polkit-kde). +} diff --git a/home/linux/niri/host/desktop.nix b/home/linux/niri/host/desktop.nix new file mode 100644 index 00000000..26b04274 --- /dev/null +++ b/home/linux/niri/host/desktop.nix @@ -0,0 +1,29 @@ +# niri config for the desktop. Monitor layout mirrors the old KDE setup +# (via kscreen-doctor): +# DP-1 2560x1440 landscape, main +# DP-3 rotated 270° portrait panel to its right (logical 1440x2560) +# If a monitor comes up upside-down, swap rotation 270 <-> 90. +_: +{ + programs.niri.settings = { + # niri-vert-scroll (scripts/niri-vert-scroll.clj) makes portrait outputs + # one vertical scrolling column; only this host has one (DP-3). + spawn-at-startup = [{ argv = [ "niri-vert-scroll" ]; }]; + + outputs = { + "DP-1" = { + mode = { width = 2560; height = 1440; }; + scale = 1.0; + position = { x = 0; y = 282; }; + }; + + # Portrait output; niri-vert-scroll (above) drives its vertical layout. + "DP-3" = { + mode = { width = 2560; height = 1440; }; + scale = 1.0; + transform.rotation = 270; + position = { x = 2560; y = 0; }; + }; + }; + }; +} diff --git a/home/linux/niri/host/eink.nix b/home/linux/niri/host/eink.nix new file mode 100644 index 00000000..b2d22908 --- /dev/null +++ b/home/linux/niri/host/eink.nix @@ -0,0 +1,44 @@ +# niri extras for the eink box (e-ink monitor), ported from the old sway +# setup. No outputs block: connector name unconfirmed, niri auto-configures; +# pin one down with `niri msg outputs` if needed. +{ config, lib, pkgs, theme, ... }: +{ + home.packages = with pkgs; [ + foliate + zathura + ]; + + programs.niri.settings = { + # E-ink: animations just smear into ghosting. + animations.enable = false; + cursor.hide-after-inactive-ms = 3000; + # White backdrop + zero gaps (old sway: `bg #ffffff solid_color`, gaps 0) + # — niri's default dark backdrop is a permanent dark field on the panel. + layout = { + background-color = "#ffffff"; + gaps = lib.mkForce 0; + }; + # Don't drag the cursor across the panel on keyboard focus changes. + input.warp-mouse-to-focus.enable = lib.mkForce false; + + spawn-at-startup = [{ argv = [ "ghostty" ]; }]; + + binds = with config.lib.niri.actions; { + "Mod+N".action = spawn "ghostty" "-e" "nvim"; + "Mod+B".action = spawn "foliate"; + }; + }; + + # Big legible launcher (ported from the old sway host file); stylix's + # fuzzel target otherwise lands at ~10pt sans on a 20pt-baseline panel. + programs.fuzzel.settings = { + main = { + font = lib.mkForce "MonoLisa Nerd Font:size=${toString (theme.baseFontSize + 2)}"; + lines = 10; + width = 40; + horizontal-pad = 20; + vertical-pad = 10; + }; + border.width = 2; + }; +} diff --git a/home/linux/niri/host/pocket.nix b/home/linux/niri/host/pocket.nix new file mode 100644 index 00000000..2c6d822b --- /dev/null +++ b/home/linux/niri/host/pocket.nix @@ -0,0 +1,17 @@ +# niri outputs for the GPD Pocket. The internal panel is mounted portrait, so +# the primary output is rotated to landscape. CONFIRM the connector name + +# rotation on first boot: +# niri msg outputs +# If the name isn't "DSI-1" (some units report "eDP-1"), rename below. If the +# image is sideways the wrong way, swap rotation 270 <-> 90. +_: +{ + programs.niri.settings.outputs = { + "DSI-1" = { + mode = { width = 1200; height = 1920; }; + scale = 1.5; + transform.rotation = 270; + position = { x = 0; y = 0; }; + }; + }; +} diff --git a/home/linux/plasma/default.nix b/home/linux/plasma/default.nix deleted file mode 100644 index f779a97e..00000000 --- a/home/linux/plasma/default.nix +++ /dev/null @@ -1,70 +0,0 @@ -{ pkgs, inputs, theme, ... }: -let - isLight = theme.appearance == "light"; -in -{ - imports = [ inputs.plasma-manager.homeModules.plasma-manager ]; - home.packages = with pkgs; [ - exfatprogs - hfsprogs - kdePackages.ark - kdePackages.dolphin - kdePackages.filelight - kdePackages.gwenview - kdePackages.kcalc - kdePackages.konsole - kdePackages.okular - kdePackages.partitionmanager - kdePackages.yakuake - ]; - - programs.plasma = { - enable = true; - - workspace = { - theme = if isLight then "default" else "breeze-dark"; - colorScheme = if isLight then "BreezeLight" else "BreezeDark"; - cursor.theme = "breeze_cursors"; - iconTheme = if isLight then "breeze" else "breeze-dark"; - }; - - panels = [ - { - location = "bottom"; - height = 44; - } - ]; - - shortcuts = { - "kwin"."Show Desktop" = "Meta+D"; - "kwin"."Switch to Desktop 1" = "Meta+1"; - "kwin"."Switch to Desktop 2" = "Meta+2"; - "kwin"."Switch to Desktop 3" = "Meta+3"; - "kwin"."Switch to Desktop 4" = "Meta+4"; - "kwin"."Switch to Desktop 5" = "Meta+5"; - "kwin"."Switch to Desktop 6" = "Meta+6"; - "kwin"."Switch to Desktop 7" = "Meta+7"; - "kwin"."Switch to Desktop 8" = "Meta+8"; - "kwin"."Switch to Desktop 9" = "Meta+9"; - "kwin"."Window Close" = "Meta+Q"; - "kwin"."Window Maximize" = "Meta+M"; - "kwin"."Window Minimize" = "Meta+N"; - "kwin"."Window to Desktop 1" = "Meta+Shift+1"; - "kwin"."Window to Desktop 2" = "Meta+Shift+2"; - "kwin"."Window to Desktop 3" = "Meta+Shift+3"; - "kwin"."Window to Desktop 4" = "Meta+Shift+4"; - "kwin"."Window to Desktop 5" = "Meta+Shift+5"; - "kwin"."Window to Desktop 6" = "Meta+Shift+6"; - "kwin"."Window to Desktop 7" = "Meta+Shift+7"; - "kwin"."Window to Desktop 8" = "Meta+Shift+8"; - "kwin"."Window to Desktop 9" = "Meta+Shift+9"; - "org.kde.dolphin.desktop"."_launch" = "Meta+E"; - "org.kde.konsole.desktop"."_launch" = "Meta+T"; - "org.kde.krunner.desktop"."_launch" = "Meta+Space"; - }; - - configFile = { - "kdeglobals"."KDE"."SingleClick" = false; - }; - }; -} diff --git a/home/linux/plasma/host/desktop.nix b/home/linux/plasma/host/desktop.nix deleted file mode 100644 index 41e2705a..00000000 --- a/home/linux/plasma/host/desktop.nix +++ /dev/null @@ -1,7 +0,0 @@ -_: { - # Host-specific Plasma configuration for desktop - # Add desktop-specific Plasma settings here - programs.plasma = { - # Example: desktop-specific panel configuration or shortcuts - }; -} diff --git a/home/linux/plasma/host/eink.nix b/home/linux/plasma/host/eink.nix deleted file mode 100644 index 41e2705a..00000000 --- a/home/linux/plasma/host/eink.nix +++ /dev/null @@ -1,7 +0,0 @@ -_: { - # Host-specific Plasma configuration for desktop - # Add desktop-specific Plasma settings here - programs.plasma = { - # Example: desktop-specific panel configuration or shortcuts - }; -} diff --git a/home/linux/plasma/host/pocket.nix b/home/linux/plasma/host/pocket.nix deleted file mode 100644 index 2692a1f5..00000000 --- a/home/linux/plasma/host/pocket.nix +++ /dev/null @@ -1,7 +0,0 @@ -_: { - # Host-specific Plasma configuration for pocket - # Add pocket-specific Plasma settings here - programs.plasma = { - # Example: pocket-specific panel configuration or shortcuts - }; -} diff --git a/home/linux/sway/default.nix b/home/linux/sway/default.nix deleted file mode 100644 index 0e8f36b1..00000000 --- a/home/linux/sway/default.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ lib, hostName, ... }: -{ - imports = [ - ./host/${hostName}.nix - ]; - - wayland.windowManager.sway = { - enable = true; - systemd.enable = true; - - config = { - modifier = "Mod4"; # Super key - terminal = "ghostty"; - - # No title bars, simple borders - window = { - titlebar = false; - border = 2; - }; - - # Simple gaps - gaps = { - inner = 0; - outer = 0; - }; - - # Default to fullscreen for focused apps - defaultWorkspace = "workspace number 1"; - - # Minimal keybindings - keybindings = - let - mod = "Mod4"; - in - lib.mkOptionDefault { - "${mod}+Return" = "exec ghostty"; - "${mod}+d" = "exec fuzzel"; - "${mod}+q" = "kill"; - "${mod}+Shift+e" = "exec swaymsg exit"; - - # Focus - "${mod}+h" = "focus left"; - "${mod}+j" = "focus down"; - "${mod}+k" = "focus up"; - "${mod}+l" = "focus right"; - - # Move - "${mod}+Shift+h" = "move left"; - "${mod}+Shift+j" = "move down"; - "${mod}+Shift+k" = "move up"; - "${mod}+Shift+l" = "move right"; - - # Fullscreen - "${mod}+f" = "fullscreen toggle"; - }; - - bars = [ ]; # No bar by default, can be overridden per-host - }; - }; - - # Fuzzel launcher - minimal and fast - programs.fuzzel = { - enable = true; - }; -} diff --git a/home/linux/sway/host/eink.nix b/home/linux/sway/host/eink.nix deleted file mode 100644 index 3de041bf..00000000 --- a/home/linux/sway/host/eink.nix +++ /dev/null @@ -1,56 +0,0 @@ -{ pkgs, lib, theme, ... }: -{ - wayland.windowManager.sway = { - config = { - fonts = { - names = lib.mkForce [ "MonoLisa Nerd Font" ]; - size = lib.mkForce (theme.baseFontSize * 1.0); - }; - - keybindings = - let - mod = "Mod4"; - in - lib.mkOptionDefault { - "${mod}+n" = "exec ghostty -e nvim"; - "${mod}+b" = "exec foliate"; - }; - - startup = [ - { command = "ghostty"; } - ]; - - output = { - "*" = { - bg = "#ffffff solid_color"; - }; - }; - }; - - extraConfig = '' - seat * hide_cursor 3000 - for_window [class=".*"] opacity 1 - for_window [app_id=".*"] opacity 1 - ''; - }; - - programs.fuzzel = { - settings = { - main = { - font = lib.mkForce "MonoLisa Nerd Font:size=${toString (theme.baseFontSize + 2)}"; - lines = 10; - width = 40; - horizontal-pad = 20; - vertical-pad = 10; - }; - border = { - width = 2; - }; - }; - }; - - home.packages = with pkgs; [ - foliate - zathura - ]; -} diff --git a/hosts/pocket/configuration.nix b/hosts/pocket/configuration.nix index 7e7f0158..49793329 100644 --- a/hosts/pocket/configuration.nix +++ b/hosts/pocket/configuration.nix @@ -10,10 +10,9 @@ variant = ""; }; + # grim/slurp/mako now come from the niri modules (screenshots are built-in; + # mako runs as a home-manager user service). environment.systemPackages = with pkgs; [ - grim - slurp - mako crosspipe ghostty ]; diff --git a/lib/default.nix b/lib/default.nix index 7a96bdeb..f9c9e5c6 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -31,7 +31,7 @@ let }; in assert builtins.elem t.appearance [ "light" "dark" ]; - assert builtins.elem f.desktopEnvironment [ null "plasma" "sway" "macos" ]; + assert builtins.elem f.desktopEnvironment [ null "niri" "macos" ]; { inherit inputs username system versions; theme = t; diff --git a/outputs/hosts.nix b/outputs/hosts.nix index 4caeb00c..c96f922e 100644 --- a/outputs/hosts.nix +++ b/outputs/hosts.nix @@ -18,7 +18,7 @@ let system = "x86_64-linux"; username = "drakeb"; format = "nixos"; - features.desktopEnvironment = "plasma"; + features.desktopEnvironment = "niri"; theme.baseFontSize = 12; extraSystemModules = chaletArgs; }; @@ -38,7 +38,7 @@ in system = "x86_64-linux"; username = "drakeb"; format = "nixos"; - features = { desktopEnvironment = "sway"; gui = false; }; + features = { desktopEnvironment = "niri"; gui = false; }; theme = { appearance = "light"; baseFontSize = 20; scheme = "primer-light"; }; autologin = true; extraSystemModules = chaletArgs; diff --git a/scripts/niri-even-widths.clj b/scripts/niri-even-widths.clj new file mode 100644 index 00000000..6f58124d --- /dev/null +++ b/scripts/niri-even-widths.clj @@ -0,0 +1,76 @@ +;; Even column widths on landscape workspaces — niri has no native +;; auto-rebalance (columns keep whatever width they opened at). One column +;; fills the screen; a 2nd splits to ½; a 3rd (and beyond) holds at ⅓ so the +;; row overflows and scrolls. Sibling of niri-vert-scroll.clj, which does the +;; same even-split for *height* on portrait outputs. + +(require '[cheshire.core :as json] + '[babashka.process :as p] + '[clojure.java.io :as io]) + +(def max-visible 3) + +;; nil on failure — niri msg errors transiently; a throw would kill the daemon. +(defn niri-json [& args] + (try (-> (apply p/shell {:out :string} "niri" "msg" "--json" args) + :out (json/parse-string true)) + (catch Exception _ nil))) + +(defn niri-action [& args] + (try (apply p/shell {:out :string :err :string} "niri" "msg" "action" args) + (catch Exception _ nil))) + +(defn landscape-ws-ids [] + (let [landscape (->> (niri-json "outputs") vals + (filter #(>= (get-in % [:logical :width] 0) + (get-in % [:logical :height] 0))) + (map :name) set)] + (->> (niri-json "workspaces") + (filter #(contains? landscape (:output %))) + (map :id) set))) + +(defn column-of [w] (get-in w [:layout :pos_in_scrolling_layout 0])) + +;; Rebalance any workspace whose column count changed since we last saw it. +;; Keyed on count so our own set-window-width (which re-fires +;; WindowOpenedOrChanged) is a no-op — it never changes the count, so no loop. +;; set-window-width takes --id, so we resize each column by its first window's +;; id without focusing it (no stolen focus / warped mouse). One windows fetch +;; covers every workspace. +(defn reconcile-all [ws-ids counts] + (let [by-ws (->> (niri-json "windows") + (filter #(and (contains? ws-ids (:workspace_id %)) + (not (:is_floating %)))) + (group-by :workspace_id))] + (reduce + (fn [acc ws-id] + (let [cols (vals (group-by column-of (get by-ws ws-id []))) + n (count cols)] + (if (= n (get counts ws-id)) + acc + (do (when (pos? n) + (let [pct (str (quot 100 (min n max-visible)) "%")] + (doseq [col cols] + (niri-action "set-window-width" pct "--id" + (str (:id (first col))))))) + (assoc acc ws-id n))))) + counts + ws-ids))) + +(defn parse-line [s] + (try (json/parse-string s true) (catch Exception _ nil))) + +(defn -main [] + (let [proc (p/process ["niri" "msg" "--json" "event-stream"] {:out :stream})] + (with-open [rdr (io/reader (:out proc))] + (loop [ws-ids (landscape-ws-ids) + counts {}] + (when-let [line (.readLine rdr)] + (let [[ev-type _] (some-> line parse-line first)] + (case ev-type + :WorkspacesChanged (recur (landscape-ws-ids) counts) + :WindowClosed (recur ws-ids (reconcile-all ws-ids counts)) + :WindowOpenedOrChanged (recur ws-ids (reconcile-all ws-ids counts)) + (recur ws-ids counts)))))))) + +(-main) diff --git a/scripts/niri-vert-scroll.clj b/scripts/niri-vert-scroll.clj new file mode 100644 index 00000000..93896d17 --- /dev/null +++ b/scripts/niri-vert-scroll.clj @@ -0,0 +1,77 @@ +;; One vertical, scrolling column on a portrait monitor — niri has no native +;; vertical layout (YaLTeR/niri#1071). Pulls each window into a single column, +;; then even-splits height up to :max-visible (full, ½, ⅓ …) and holds at +;; 1/:max-visible beyond that so the column overflows and scrolls. + +(require '[cheshire.core :as json] + '[babashka.process :as p] + '[clojure.java.io :as io]) + +(def max-visible 3) + +;; nil on failure — niri msg errors transiently; a throw would kill the daemon. +(defn niri-json [& args] + (try (-> (apply p/shell {:out :string} "niri" "msg" "--json" args) + :out (json/parse-string true)) + (catch Exception _ nil))) + +(defn niri-action [& args] + (try (apply p/shell {:out :string :err :string} "niri" "msg" "action" args) + (catch Exception _ nil))) + +(defn portrait-ws-ids [] + (let [portrait (->> (niri-json "outputs") vals + (filter #(< (get-in % [:logical :width] 0) + (get-in % [:logical :height] 0))) + (map :name) set)] + (->> (niri-json "workspaces") + (filter #(contains? portrait (:output %))) + (map :id) set))) + +(defn column-of [w] (get-in w [:layout :pos_in_scrolling_layout 0])) + +(defn apply-heights! [ws-id] + (let [wins (filter #(= (:workspace_id %) ws-id) (niri-json "windows")) + overflow (> (count wins) max-visible) + pct (str (quot 100 max-visible) "%")] + (doseq [w wins] + (if overflow + (niri-action "set-window-height" pct "--id" (str (:id w))) + (niri-action "reset-window-height" "--id" (str (:id w))))))) + +;; The pull re-fires an event, so multi-column layouts converge one step a time. +(defn consolidate! [w] + (when (> (or (column-of w) 1) 1) + (niri-action "consume-or-expel-window-left" "--id" (str (:id w))) + (apply-heights! (:workspace_id w)))) + +;; The typed niri config can't set a per-output default-column-width, so we +;; widen the portrait column to full width here. set-column-width has no --id; +;; it acts on the *focused* column, so only fire it when this window is focused +;; (else we'd resize whatever column is focused on the landscape monitor). +;; ponytail: a background-opened portrait window stays at default width until +;; it's focused-and-changed — fine, the common open-here case is focused. +(defn widen! [w] + (when (:is_focused w) + (niri-action "set-column-width" "100%"))) + +(defn parse-line [s] + (try (json/parse-string s true) (catch Exception _ nil))) + +(defn -main [] + (let [proc (p/process ["niri" "msg" "--json" "event-stream"] {:out :stream})] + (with-open [rdr (io/reader (:out proc))] + (loop [ws-ids (portrait-ws-ids)] + (when-let [line (.readLine rdr)] + (let [[ev-type payload] (some-> line parse-line first)] + (case ev-type + :WorkspacesChanged (recur (portrait-ws-ids)) + :WindowClosed (do (run! apply-heights! ws-ids) (recur ws-ids)) + :WindowOpenedOrChanged (let [w (:window payload)] + (when (contains? ws-ids (:workspace_id w)) + (consolidate! w) + (widen! w)) + (recur ws-ids)) + (recur ws-ids)))))))) + +(-main) diff --git a/system/nixOS/bluetooth.nix b/system/nixOS/bluetooth.nix index ff2f8f91..a3e425ef 100644 --- a/system/nixOS/bluetooth.nix +++ b/system/nixOS/bluetooth.nix @@ -1,9 +1,11 @@ -_: +{ features, ... }: { hardware.bluetooth = { enable = true; powerOnBoot = true; }; - # Bluedevil is included with Plasma 6 by default for KDE Bluetooth integration + # Bluetooth tray applet + manager (KDE's Bluedevil is gone with Plasma). + # GUI hosts only — headless hosts have no session to show the applet. + services.blueman.enable = features.gui; } diff --git a/system/nixOS/default.nix b/system/nixOS/default.nix index 68e2b2c4..62b79b9d 100644 --- a/system/nixOS/default.nix +++ b/system/nixOS/default.nix @@ -11,15 +11,10 @@ ./mullvad.nix ./printing.nix ./stylix.nix - ] ++ lib.optionals (features.desktopEnvironment != null) [ - ./greetd.nix ] ++ lib.optionals features.gaming [ ./gaming.nix - ] ++ ( - if features.desktopEnvironment == "plasma" - then [ ./plasma ] - else if features.desktopEnvironment == "sway" - then [ ./sway ] - else [ ] - ); + ] ++ lib.optionals (features.desktopEnvironment == "niri") [ + ./greetd.nix + ./niri + ]; } diff --git a/system/nixOS/greetd.nix b/system/nixOS/greetd.nix index c52c81f1..2a210c0f 100644 --- a/system/nixOS/greetd.nix +++ b/system/nixOS/greetd.nix @@ -1,18 +1,14 @@ -{ lib, pkgs, username, features, autologin, ... }: -let - de = features.desktopEnvironment; - sessionCommand = - if de == "plasma" then "${pkgs.kdePackages.plasma-workspace}/bin/startplasma-wayland" - else if de == "sway" then "${pkgs.sway}/bin/sway" - else throw "greetd: unsupported desktopEnvironment: ${de}"; -in +{ config, lib, username, features, autologin, ... }: +# Only imported for niri hosts (system/nixOS/default.nix); fail loud if a new +# DE ever reaches here without its own session command. +assert features.desktopEnvironment == "niri"; { services.greetd.enable = true; programs.regreet.enable = !autologin; services.greetd.settings.default_session = lib.mkIf autologin { - command = sessionCommand; + command = "${config.programs.niri.package}/bin/niri-session"; user = username; }; } diff --git a/system/nixOS/niri/default.nix b/system/nixOS/niri/default.nix new file mode 100644 index 00000000..40c71eb5 --- /dev/null +++ b/system/nixOS/niri/default.nix @@ -0,0 +1,13 @@ +{ inputs, pkgs, ... }: +{ + # niri-flake's module wires everything: niri-session, xdg portals, a polkit + # agent, the swaylock PAM service, and the niri.cachix.org substituter. + imports = [ inputs.niri.nixosModules.niri ]; + + programs.niri = { + enable = true; + # git-main niri. niri-flake doesn't apply its overlay globally, so reference + # the flake output directly (this is what niri.cachix.org caches). + package = inputs.niri.packages.${pkgs.system}.niri-unstable; + }; +} diff --git a/system/nixOS/plasma/default.nix b/system/nixOS/plasma/default.nix deleted file mode 100644 index 4b6e9b39..00000000 --- a/system/nixOS/plasma/default.nix +++ /dev/null @@ -1,27 +0,0 @@ -{ pkgs, ... }: -{ - services.xserver.enable = true; - services.desktopManager.plasma6.enable = true; - - environment.plasma6.excludePackages = with pkgs.kdePackages; [ - oxygen - ]; - - programs.dconf.enable = true; - - environment.systemPackages = with pkgs; [ - filezilla - kdePackages.kdeconnect-kde - kdePackages.kio-extras - kdePackages.kdeplasma-addons - kdePackages.kwalletmanager - kdePackages.partitionmanager - kdePackages.plasma-browser-integration - kdePackages.plasma-disks - kdePackages.plasma-nm - kdePackages.plasma-pa - kdePackages.plasma-thunderbolt - kdePackages.powerdevil - kdePackages.spectacle - ]; -} diff --git a/system/nixOS/sway/default.nix b/system/nixOS/sway/default.nix deleted file mode 100644 index 34ca2ed2..00000000 --- a/system/nixOS/sway/default.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ pkgs, ... }: -{ - programs.sway = { - enable = true; - wrapperFeatures.gtk = true; - }; - - programs.dconf.enable = true; - - xdg.portal = { - enable = true; - wlr.enable = true; - extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; - }; - - environment.systemPackages = with pkgs; [ - wl-clipboard - grim - slurp - ]; -}