hyprland: virtual desktops, and stop OOM kills from killing the session
Workspaces were pinned one-per-monitor, so switching only ever moved one screen. The virtual-desktops plugin replaces them with vdesks that span every monitor, and SUPER+1..5 now switches all screens together. Hyprland plugins are ABI-locked to the compositor, so the input's nixpkgs follows ours to compile against the hyprland we actually run. cycleworkspaces stays off: upstream says it breaks above 2 monitors, and pc has 3. The session had also been dying several times a day, which looked like a Hyprland crash and was not one. There was no swap at all, and a nixos-rebuild forks ~60 nix evaluators; stacked on a browser and a few editor sessions that exhausted all 15G, leaving page cache squeezed to 40M while 13G sat in anon and nothing was reclaimable. The kernel then OOM-killed 1Password (highest oom_score_adj), and because every GUI app shares the wayland-wm@hyprland.service cgroup, systemd's default OOMPolicy=stop tore down the whole unit -- compositor included, with uwsm's Restart=no meaning nothing came back. Two independent fixes: zram swap (50% of RAM, zstd) with vm.swappiness raised to 180, so the kernel compresses anon pages instead of evicting cache, which raises the ceiling; and OOMPolicy=continue, so that hitting it costs one app instead of the desktop. zram rather than a swapfile because root is compress=zstd, and a btrfs swapfile needs its own nodatacow, uncompressed subvolume. Also lands the pending work in this tree: hyprshot replaces grimblast for screenshots (it reads HYPRSHOT_DIR, not XDG_SCREENSHOTS_DIR, so shots were landing loose in ~/Pictures), hyprlock at boot so login lands on the lock screen and unlocks the keyring, the battery popup moves to SUPER+R, and SUPER+W/E open floating wifi and bluetooth panels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d04b17fe52
commit
ddca4e1d72
3 changed files with 233 additions and 27 deletions
|
|
@ -2,6 +2,20 @@
|
|||
# Monitor layout is host-specific; written by the flake per host.
|
||||
source = ~/.config/hypr/monitors.conf
|
||||
|
||||
# Virtual desktops plugin. A vdesk spans every monitor, so SUPER+1..5 switches
|
||||
# all screens together instead of workspaces being pinned per monitor.
|
||||
source = ~/.config/hypr/plugins.conf
|
||||
|
||||
plugin {
|
||||
virtual-desktops {
|
||||
# The startup toast would fire on every login now that mako exists.
|
||||
notifyinit = 0
|
||||
rememberlayout = size
|
||||
# Deliberately NOT enabling cycleworkspaces: upstream states it does
|
||||
# not work with more than 2 monitors, and pc has 3.
|
||||
}
|
||||
}
|
||||
|
||||
device {
|
||||
name = apple-spi-keyboard
|
||||
kb_options = caps:ctrl_modifier,grp:win_space_toggle
|
||||
|
|
@ -63,9 +77,15 @@ ecosystem {
|
|||
env = QT_QPA_PLATFORMTHEME,qt6ct
|
||||
env = QT_STYLE_OVERRIDE,Adwaita-Dark
|
||||
env = XDG_SCREENSHOTS_DIR,$HOME/Pictures/Screenshots
|
||||
# hyprshot ignores XDG_SCREENSHOTS_DIR: it reads HYPRSHOT_DIR, else XDG_PICTURES_DIR,
|
||||
# else $HOME. Without this, screenshots land loose in ~/Pictures.
|
||||
env = HYPRSHOT_DIR,$HOME/Pictures/Screenshots
|
||||
|
||||
# -- Autostart --
|
||||
exec-once = uwsm finalize
|
||||
# Boot lands on the lock screen rather than an unlocked desktop. Unlocking it
|
||||
# also unlocks the keyring (see security.pam.services.hyprlock in flake.nix).
|
||||
exec-once = hyprlock
|
||||
exec-once = swaybg -c '#16181a'
|
||||
exec-once = GDK_BACKEND=wayland eww daemon
|
||||
exec-once = systemctl --user start hyprpolkitagent
|
||||
|
|
@ -73,8 +93,8 @@ exec-once = systemctl --user start hyprpolkitagent
|
|||
# -- Core --
|
||||
bind = SUPER, Return, exec, kitty
|
||||
bind = SUPER, D, exec, fuzzel
|
||||
bind = SUPER, B, exec, eww open battery-popup
|
||||
bindr = SUPER, B, exec, eww close battery-popup
|
||||
bind = SUPER, R, exec, eww open battery-popup
|
||||
bindr = SUPER, R, exec, eww close battery-popup
|
||||
bind = SUPER, Q, killactive
|
||||
bind = SUPER, F, fullscreen, 1
|
||||
bind = SUPER SHIFT, Space, togglefloating
|
||||
|
|
@ -83,6 +103,17 @@ bind = SUPER, Escape, exec, hyprlock
|
|||
# Amphetamine-style stay-awake toggle: idle + sleep + lid. See flake.nix.
|
||||
bind = SUPER, A, exec, caffeine
|
||||
|
||||
# -- Floating panels --
|
||||
# Each gets its own app_id so the rule below floats only these and not every
|
||||
# terminal. Quit with the TUI's own `q`; no toggle wrapper.
|
||||
#
|
||||
# The float/size rule is in monitors.conf, generated per host: Hyprland 0.56
|
||||
# `size` takes ONLY absolute pixels -- percentages are silently ignored (they
|
||||
# parse fine and then do nothing), so the value depends on the resolution.
|
||||
bind = SUPER, W, exec, kitty --class panel-wifi impala
|
||||
bind = SUPER, E, exec, kitty --class panel-bt bluetui
|
||||
|
||||
|
||||
# -- Focus --
|
||||
bind = SUPER, h, movefocus, l
|
||||
bind = SUPER, j, movefocus, d
|
||||
|
|
@ -95,18 +126,18 @@ bind = SUPER SHIFT, j, movewindow, d
|
|||
bind = SUPER SHIFT, k, movewindow, u
|
||||
bind = SUPER SHIFT, l, movewindow, r
|
||||
|
||||
# -- Workspaces --
|
||||
bind = SUPER, 1, workspace, 1
|
||||
bind = SUPER, 2, workspace, 2
|
||||
bind = SUPER, 3, workspace, 3
|
||||
bind = SUPER, 4, workspace, 4
|
||||
bind = SUPER, 5, workspace, 5
|
||||
# -- Virtual desktops (plugin dispatchers, not Hyprland workspaces) --
|
||||
bind = SUPER, 1, vdesk, 1
|
||||
bind = SUPER, 2, vdesk, 2
|
||||
bind = SUPER, 3, vdesk, 3
|
||||
bind = SUPER, 4, vdesk, 4
|
||||
bind = SUPER, 5, vdesk, 5
|
||||
|
||||
bind = SUPER SHIFT, 1, movetoworkspace, 1
|
||||
bind = SUPER SHIFT, 2, movetoworkspace, 2
|
||||
bind = SUPER SHIFT, 3, movetoworkspace, 3
|
||||
bind = SUPER SHIFT, 4, movetoworkspace, 4
|
||||
bind = SUPER SHIFT, 5, movetoworkspace, 5
|
||||
bind = SUPER SHIFT, 1, movetodesk, 1
|
||||
bind = SUPER SHIFT, 2, movetodesk, 2
|
||||
bind = SUPER SHIFT, 3, movetodesk, 3
|
||||
bind = SUPER SHIFT, 4, movetodesk, 4
|
||||
bind = SUPER SHIFT, 5, movetodesk, 5
|
||||
|
||||
# -- Move current workspace to another monitor --
|
||||
bind = SUPER ALT, h, movecurrentworkspacetomonitor, l
|
||||
|
|
@ -138,10 +169,8 @@ bindl = , XF86AudioNext, exec, playerctl next
|
|||
bindl = , XF86AudioPrev, exec, playerctl previous
|
||||
|
||||
# -- Screenshots --
|
||||
# Super+S: region -> clipboard
|
||||
bind = SUPER, S, exec, grimblast --notify copy area
|
||||
# Super+Ctrl+S: full screen -> file + clipboard
|
||||
bind = SUPER CTRL, S, exec, grimblast --notify copysave screen
|
||||
# Super+Shift+S: region -> file + clipboard + open in GIMP
|
||||
bind = SUPER SHIFT, S, exec, bash -c 'mkdir -p "$HOME/Pictures/Screenshots"; f="$HOME/Pictures/Screenshots/$(date +%Y-%m-%dT%H-%M-%S).png"; grimblast --notify --freeze save area "$f" && wl-copy --type image/png < "$f" && gimp "$f"'
|
||||
# Super+Ctrl+S: focused monitor -> file + clipboard
|
||||
bind = SUPER CTRL, S, exec, hyprshot -m output
|
||||
# Super+Shift+S: region (screen frozen while selecting) -> file + clipboard
|
||||
bind = SUPER SHIFT, S, exec, hyprshot -m region -z
|
||||
|
||||
|
|
|
|||
39
flake.lock
39
flake.lock
|
|
@ -78,6 +78,43 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"hyprland-virtual-desktops": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1790338297,
|
||||
"narHash": "sha256-iJP+CyrS/fw/dqysRN9GtVB8GlJZcja4rkjBeJsXFsg=",
|
||||
"owner": "levnikmyskin",
|
||||
"repo": "hyprland-virtual-desktops",
|
||||
"rev": "74b88ee5209c4dcea74eedc4646e185b02a8f946",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "levnikmyskin",
|
||||
"repo": "hyprland-virtual-desktops",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"linear-tui-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1790503843,
|
||||
"narHash": "sha256-FS0w8a53N+XN0tnxUzpu4FNG0rYVycv/ZlnB7bPgd+I=",
|
||||
"owner": "k1-c",
|
||||
"repo": "linear-tui",
|
||||
"rev": "fa79ffed365c2deff1916022abe8652989682216",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "k1-c",
|
||||
"ref": "v0.13.0",
|
||||
"repo": "linear-tui",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1786106723,
|
||||
|
|
@ -99,6 +136,8 @@
|
|||
"apple-silicon": "apple-silicon",
|
||||
"firefox-addons": "firefox-addons",
|
||||
"home-manager": "home-manager",
|
||||
"hyprland-virtual-desktops": "hyprland-virtual-desktops",
|
||||
"linear-tui-src": "linear-tui-src",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"zen-browser": "zen-browser"
|
||||
}
|
||||
|
|
|
|||
154
flake.nix
154
flake.nix
|
|
@ -24,10 +24,46 @@
|
|||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.home-manager.follows = "home-manager";
|
||||
};
|
||||
|
||||
# linear-tui has no flake of its own, so take the source and build it below.
|
||||
# Pinned to a tag; `nix flake update` moves it and records the hash in
|
||||
# flake.lock, so there is no sha256 to maintain by hand.
|
||||
linear-tui-src = {
|
||||
url = "github:k1-c/linear-tui/v0.13.0";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, apple-silicon, home-manager, firefox-addons, zen-browser, ... }:
|
||||
# Virtual desktops: one desktop spans all monitors, replacing Hyprland's
|
||||
# workspace-pinned-to-monitor model. Its own nixpkgs pin is older than
|
||||
# ours, and Hyprland plugins are ABI-locked to the compositor, so it must
|
||||
# follow ours to compile against the hyprland we actually run.
|
||||
hyprland-virtual-desktops = {
|
||||
url = "github:levnikmyskin/hyprland-virtual-desktops";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, apple-silicon, home-manager, firefox-addons, zen-browser, linear-tui-src, hyprland-virtual-desktops, ... }:
|
||||
let
|
||||
# linear-tui, built from the pinned source input above. Upstream ships no
|
||||
# flake and it is not in nixpkgs, but it has a committed Cargo.lock, so
|
||||
# cargoLock.lockFile covers the dependency hashes with nothing to vendor.
|
||||
#
|
||||
# No OpenSSL/pkg-config needed: the crate selects reqwest's rustls
|
||||
# backend on purpose, and takes its trust roots from the OS store.
|
||||
linearTui = pkgs: pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "linear-tui";
|
||||
version = "0.13.0";
|
||||
src = linear-tui-src;
|
||||
cargoLock.lockFile = "${linear-tui-src}/Cargo.lock";
|
||||
# One test (a_worktree_belongs_to_its_repository) shells out to `git`,
|
||||
# which the build sandbox lacks -- it panics on NotFound unwrapping
|
||||
# Command::new("git"). Supplying git lets all 512 tests run.
|
||||
nativeCheckInputs = [ pkgs.git ];
|
||||
|
||||
meta.mainProgram = "linear-tui";
|
||||
};
|
||||
|
||||
# ───────────────────── shared home-manager user ─────────────────────
|
||||
home = { pkgs, osConfig, ... }: {
|
||||
imports = [ zen-browser.homeModules.beta ];
|
||||
|
|
@ -47,19 +83,76 @@
|
|||
if osConfig.networking.hostName == "mbp"
|
||||
then ''
|
||||
monitor = , preferred, auto, 2.0
|
||||
|
||||
# Floating TUI panels: 70% wide, full height. Absolute px because
|
||||
# Hyprland 0.56 ignores percentages in `size`. 1728x1080 logical.
|
||||
windowrule = match:class ^(panel-wifi|panel-bt)$, float on, size 1210 1080, center on
|
||||
''
|
||||
else ''
|
||||
monitor = desc:Lenovo Group Limited P27q-20 V909LRFR, 2560x1440@60, 0x0, 1.0
|
||||
monitor = desc:Lenovo Group Limited P27q-20 V909LPNY, 2560x1440@60, 2560x0, 1.0
|
||||
monitor = desc:Lenovo Group Limited P27q-20 V909LRDD, 2560x1440@60, 5120x0, 1.0
|
||||
workspace = 1, monitor:desc:Lenovo Group Limited P27q-20 V909LRFR, default:true
|
||||
workspace = 2, monitor:desc:Lenovo Group Limited P27q-20 V909LPNY, default:true
|
||||
workspace = 3, monitor:desc:Lenovo Group Limited P27q-20 V909LRDD, default:true
|
||||
|
||||
# Same panels, sized for 2560x1440 logical.
|
||||
windowrule = match:class ^(panel-wifi|panel-bt)$, float on, size 1792 1440, center on
|
||||
'';
|
||||
# Plugin load line, as a store path. Sourced from hyprland.conf, the
|
||||
# same pattern as monitors.conf. Upstream installs the library as
|
||||
# libvirtual-desktops.so (not lib<pname>.so).
|
||||
"hypr/plugins.conf".text = ''
|
||||
plugin = ${hyprland-virtual-desktops.packages.${pkgs.stdenv.hostPlatform.system}.virtual-desktops}/lib/libvirtual-desktops.so
|
||||
'';
|
||||
"eww".source = ./config/eww;
|
||||
"fuzzel".source = ./config/fuzzel;
|
||||
};
|
||||
|
||||
|
||||
# Launcher hygiene. These .desktop entries ship with packages we do want,
|
||||
# but they are noise in fuzzel. A NoDisplay entry in XDG_DATA_HOME
|
||||
# shadows the package's own copy, which is the only way to hide one
|
||||
# without patching the package.
|
||||
#
|
||||
# Deliberately home.file and NOT xdg.desktopEntries: that option only
|
||||
# emits anything when xdg.enable = true, which we don't set, so it
|
||||
# silently produces no files at all.
|
||||
# nvim - neovim's "Neovim wrapper" entry; we only launch it from a terminal
|
||||
# uuctl - uwsm's systemd-user-unit picker, pulled in by withUWSM
|
||||
home.file = {
|
||||
".local/share/applications/nvim.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Neovim wrapper
|
||||
NoDisplay=true
|
||||
'';
|
||||
".local/share/applications/uuctl.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=uuctl
|
||||
NoDisplay=true
|
||||
'';
|
||||
};
|
||||
|
||||
# Notification daemon. Nothing owned org.freedesktop.Notifications before
|
||||
# this, so notify-send failed outright: hyprshot's "Screenshot saved"
|
||||
# toasts went nowhere, and kitty logged a DBus ServiceUnknown error on
|
||||
# every launch. Colours match the cyberdream kitty theme above.
|
||||
services.mako = {
|
||||
enable = true;
|
||||
settings = {
|
||||
font = "JetBrainsMono Nerd Font 11";
|
||||
background-color = "#16181a";
|
||||
text-color = "#ffffff";
|
||||
border-color = "#5ea1ff";
|
||||
border-size = 2;
|
||||
border-radius = 8;
|
||||
default-timeout = 5000;
|
||||
anchor = "top-right";
|
||||
margin = "12";
|
||||
padding = "12";
|
||||
width = 380;
|
||||
};
|
||||
};
|
||||
|
||||
programs.kitty = {
|
||||
enable = true;
|
||||
settings = {
|
||||
|
|
@ -105,7 +198,7 @@
|
|||
hyprcursor.enable = true;
|
||||
};
|
||||
|
||||
# Dark GTK apps (gimp, file dialogs).
|
||||
# Dark GTK apps (file dialogs, etc).
|
||||
gtk = {
|
||||
enable = true;
|
||||
theme = { name = "Adwaita-dark"; package = pkgs.gnome-themes-extra; };
|
||||
|
|
@ -208,7 +301,7 @@
|
|||
};
|
||||
};
|
||||
|
||||
home.packages = with pkgs; [ ripgrep fd tridactyl-native lazygit rustc cargo clippy rustfmt rust-analyzer claude-code gcc bluetui impala postgresql ];
|
||||
home.packages = with pkgs; [ ripgrep fd tridactyl-native lazygit rustc cargo clippy rustfmt rust-analyzer claude-code gcc bluetui impala postgresql fastfetch (linearTui pkgs) ];
|
||||
};
|
||||
|
||||
# ───────────────────── shared system (both hosts) ─────────────────────
|
||||
|
|
@ -316,6 +409,25 @@
|
|||
|
||||
programs.hyprland = { enable = true; withUWSM = true; };
|
||||
programs.hyprlock.enable = true;
|
||||
|
||||
# Every GUI app lives in the one wayland-wm@hyprland.service cgroup, so
|
||||
# with systemd's default OOMPolicy=stop a kernel OOM-kill of ANY process
|
||||
# in it stops the whole unit -- compositor included. 1Password was always
|
||||
# the victim (highest oom_score_adj), but losing it killed the entire
|
||||
# desktop, and uwsm sets Restart=no, so nothing came back. It read as
|
||||
# "Hyprland crashed" when the compositor was never at fault.
|
||||
#
|
||||
# continue: log the kill, lose that one app, keep the session. Merges
|
||||
# into the drop-in the uwsm module already generates for this unit.
|
||||
systemd.user.services."wayland-wm@".serviceConfig.OOMPolicy = "continue";
|
||||
|
||||
# Autologin means no password reaches PAM at login, so the gnome-keyring
|
||||
# login keyring is never created or unlocked -- which is why 1Password
|
||||
# could not persist its 2FA device token and re-prompted the YubiKey
|
||||
# every unlock. Putting pam_gnome_keyring in hyprlock's stack instead
|
||||
# makes the lock screen we already have do that job, so there is still
|
||||
# exactly one password prompt and no second login screen.
|
||||
security.pam.services.hyprlock.enableGnomeKeyring = true;
|
||||
services.hypridle.enable = true;
|
||||
|
||||
# ── Amphetamine-style "stay awake" ──
|
||||
|
|
@ -354,11 +466,15 @@
|
|||
|
||||
hardware.bluetooth.enable = true;
|
||||
programs.zsh.enable = true;
|
||||
# No NixOS manual: it puts a "NixOS Manual" entry in the launcher and we
|
||||
# read the docs on the web. man pages are unaffected.
|
||||
documentation.nixos.enable = false;
|
||||
|
||||
fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
git kitty fuzzel gimp
|
||||
wl-clipboard grim slurp swaybg brightnessctl playerctl grimblast
|
||||
git kitty fuzzel
|
||||
wl-clipboard grim slurp swaybg brightnessctl playerctl hyprshot
|
||||
hyprpolkitagent xdg-utils
|
||||
|
||||
# Front-end for caffeine.service (above). Bound to SUPER+A.
|
||||
|
|
@ -455,6 +571,28 @@
|
|||
fileSystems = [ "/" ];
|
||||
};
|
||||
|
||||
# 15G of RAM and no swap at all: a `nixos-rebuild` forks ~60 nix
|
||||
# evaluators, and stacked on a browser and a few editor sessions
|
||||
# that was enough to exhaust it. The kernel then OOM-killed a
|
||||
# process inside wayland-wm@hyprland.service, and systemd's default
|
||||
# OOMPolicy=stop tore the whole graphical session down with it.
|
||||
#
|
||||
# zram rather than a swapfile, because root is compress=zstd and a
|
||||
# btrfs swapfile needs its own nodatacow, uncompressed subvolume.
|
||||
zramSwap = {
|
||||
enable = true;
|
||||
memoryPercent = 50; # 7.6G of backing store, ~a third of that in real RAM
|
||||
};
|
||||
|
||||
# Swapping to RAM is cheap, so let the kernel compress anon pages
|
||||
# rather than evict page cache. At the OOM above, cache had already
|
||||
# been squeezed to 40M while 13G sat in anon and nothing was
|
||||
# reclaimable — the default swappiness of 60 gets that backwards here.
|
||||
boot.kernel.sysctl = {
|
||||
"vm.swappiness" = 180;
|
||||
"vm.page-cluster" = 0; # zram has no seek penalty, so no readahead
|
||||
};
|
||||
|
||||
# Build x86_64 derivations (packing-station iso/deploys) via qemu.
|
||||
boot.binfmt.emulatedSystems = [ "x86_64-linux" ];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue