From cf8892152c8d4154145f1d07c7fab98ccc6ce8ae Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 10 Feb 2026 22:25:58 -0800 Subject: [PATCH] feat: add hyprscratch scratchpad management with rule reapply fork - Add hyprscratch from colonelpanic8/reapply-rules-on-toggle fork that reapplies size/position/float rules on every toggle (not just spawn) - Configure 9 scratchpads: htop, volume, spotify, element, slack, transmission, dropdown, gmail, messages - Use clean mode (hide on workspace change) and auto-dismiss (only dropdown persists) - Fix pavucontrol class (org.pulseaudio.pavucontrol) and dropdown position (offset by taffybar height) - Add kanshi-sni, dbus-menu, dbus-hslogger flake inputs and follows Co-Authored-By: Claude Opus 4.6 --- dotfiles/config/hypr/hyprland.conf | 106 +++++--------------------- dotfiles/config/hypr/hyprscratch.conf | 1 + nixos/flake.lock | 63 +++++++++++++-- nixos/flake.nix | 36 +++++++++ nixos/hyprland.nix | 69 ++++++++++++++++- 5 files changed, 180 insertions(+), 95 deletions(-) create mode 120000 dotfiles/config/hypr/hyprscratch.conf diff --git a/dotfiles/config/hypr/hyprland.conf b/dotfiles/config/hypr/hyprland.conf index a13eeab4..77e387db 100644 --- a/dotfiles/config/hypr/hyprland.conf +++ b/dotfiles/config/hypr/hyprland.conf @@ -201,61 +201,14 @@ binds { # WINDOW RULES # ============================================================================= # Float dialogs -windowrulev2 = float, class:^()$,title:^()$ -windowrulev2 = float, title:^(Picture-in-Picture)$ -windowrulev2 = float, title:^(Open File)$ -windowrulev2 = float, title:^(Save File)$ -windowrulev2 = float, title:^(Confirm)$ +windowrule = match:class ^()$, match:title ^()$, float on +windowrule = match:title ^(Picture-in-Picture)$, float on +windowrule = match:title ^(Open File)$, float on +windowrule = match:title ^(Save File)$, float on +windowrule = match:title ^(Confirm)$, float on -# Scratchpad windows - float and size -windowrulev2 = workspace special:htop silent, class:^(htop-scratch)$ -windowrulev2 = float, class:^(htop-scratch)$ -windowrulev2 = size 90% 90%, class:^(htop-scratch)$ -windowrulev2 = center, class:^(htop-scratch)$ - -windowrulev2 = workspace special:volume silent, class:^(?i)(pavucontrol|pavucontrol-qt|org\.pulseaudio\.pavucontrol)$ -windowrulev2 = float, class:^(?i)(pavucontrol|pavucontrol-qt|org\.pulseaudio\.pavucontrol)$ -windowrulev2 = size 90% 90%, class:^(?i)(pavucontrol|pavucontrol-qt|org\.pulseaudio\.pavucontrol)$ -windowrulev2 = center, class:^(?i)(pavucontrol|pavucontrol-qt|org\.pulseaudio\.pavucontrol)$ - -windowrulev2 = workspace special:spotify silent, class:^(spotify)$ -windowrulev2 = float, class:^(spotify)$ -windowrulev2 = size 90% 90%, class:^(spotify)$ -windowrulev2 = center, class:^(spotify)$ - -windowrulev2 = workspace special:element silent, class:^(Element)$ -windowrulev2 = float, class:^(Element)$ -windowrulev2 = size 90% 90%, class:^(Element)$ -windowrulev2 = center, class:^(Element)$ - -windowrulev2 = workspace special:slack silent, class:^(Slack)$ -windowrulev2 = float, class:^(Slack)$ -windowrulev2 = size 90% 90%, class:^(Slack)$ -windowrulev2 = center, class:^(Slack)$ - -windowrulev2 = workspace special:transmission silent, class:^(transmission-gtk)$ -windowrulev2 = float, class:^(transmission-gtk)$ -windowrulev2 = size 90% 90%, class:^(transmission-gtk)$ -windowrulev2 = center, class:^(transmission-gtk)$ - -windowrulev2 = workspace special:dropdown silent, class:^(com\.mitchellh\.ghostty\.dropdown)$ -windowrulev2 = float, class:^(com\.mitchellh\.ghostty\.dropdown)$ -windowrulev2 = size 100% 50%, class:^(com\.mitchellh\.ghostty\.dropdown)$ -windowrulev2 = move 0 0, class:^(com\.mitchellh\.ghostty\.dropdown)$ -windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty\.dropdown)$ -windowrulev2 = noshadow, class:^(com\.mitchellh\.ghostty\.dropdown)$ -windowrulev2 = animation slide, class:^(com\.mitchellh\.ghostty\.dropdown)$ - -# Gmail and Messages (Chrome windows) -windowrulev2 = workspace special:gmail silent, class:^(google-chrome)$,title:^(.*@gmail.com.*Gmail.*)$ -windowrulev2 = float, class:^(google-chrome)$,title:^(.*@gmail.com.*Gmail.*)$ -windowrulev2 = size 90% 90%, class:^(google-chrome)$,title:^(.*@gmail.com.*Gmail.*)$ -windowrulev2 = center, class:^(google-chrome)$,title:^(.*@gmail.com.*Gmail.*)$ - -windowrulev2 = workspace special:messages silent, class:^(google-chrome)$,title:^(Messages.*)$ -windowrulev2 = float, class:^(google-chrome)$,title:^(Messages.*)$ -windowrulev2 = size 90% 90%, class:^(google-chrome)$,title:^(Messages.*)$ -windowrulev2 = center, class:^(google-chrome)$,title:^(Messages.*)$ +# Scratchpad sizing handled by hyprscratch exec rules (see hyprland.nix) +# Using hyprscratch rules instead of windowrule to avoid affecting child windows (e.g. Slack meets) # ============================================================================= # KEY BINDINGS @@ -288,28 +241,17 @@ bind = $mainMod, V, exec, wl-paste | xdotool type --file - bind = $modAlt, C, exec, ~/.config/hypr/scripts/raise-or-run.sh google-chrome google-chrome-stable # ----------------------------------------------------------------------------- -# SCRATCHPADS (Special Workspaces) +# SCRATCHPADS (managed by hyprscratch daemon with auto-dismiss) # ----------------------------------------------------------------------------- -# Toggle scratchpads -bind = $modAlt, E, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh element '^Element$' - element-desktop -bind = $modAlt, G, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh gmail '^google-chrome$' '.*@gmail.com.*Gmail.*' google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox -bind = $modAlt, H, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh htop '^htop-scratch$' - alacritty --class htop-scratch --title htop -e htop -bind = $modAlt, M, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh messages '^google-chrome$' '^Messages.*' google-chrome-stable --new-window https://messages.google.com/web/conversations -bind = $modAlt, K, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh slack '^Slack$' - slack -bind = $modAlt, S, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh spotify '^spotify$' - spotify -bind = $modAlt, T, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh transmission '^transmission-gtk$' - transmission-gtk -bind = $modAlt, V, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh volume '^(pavucontrol|pavucontrol-qt|org\.pulseaudio\.pavucontrol)$' - pavucontrol -bind = $modAlt, grave, exec, ~/.config/hypr/scripts/toggle-scratchpad.sh dropdown '^com\.mitchellh\.ghostty\.dropdown$' - ghostty --config-file=/home/imalison/.config/ghostty/dropdown - -# Move windows to scratchpads -bind = $modAlt SHIFT, E, movetoworkspace, special:element -bind = $modAlt SHIFT, G, movetoworkspace, special:gmail -bind = $modAlt SHIFT, H, movetoworkspace, special:htop -bind = $modAlt SHIFT, M, movetoworkspace, special:messages -bind = $modAlt SHIFT, K, movetoworkspace, special:slack -bind = $modAlt SHIFT, S, movetoworkspace, special:spotify -bind = $modAlt SHIFT, T, movetoworkspace, special:transmission -bind = $modAlt SHIFT, V, movetoworkspace, special:volume +bind = $modAlt, E, exec, hyprscratch toggle element +bind = $modAlt, G, exec, hyprscratch toggle gmail +bind = $modAlt, H, exec, hyprscratch toggle htop +bind = $modAlt, M, exec, hyprscratch toggle messages +bind = $modAlt, K, exec, hyprscratch toggle slack +bind = $modAlt, S, exec, hyprscratch toggle spotify +bind = $modAlt, T, exec, hyprscratch toggle transmission +bind = $modAlt, V, exec, hyprscratch toggle volume +bind = $modAlt, grave, exec, hyprscratch toggle dropdown # Hidden workspace (like XMonad's NSP) bind = $mainMod, X, movetoworkspace, special:NSP @@ -579,22 +521,14 @@ bind = $mainMod, mouse_up, exec, ~/.config/hypr/scripts/workspace-scroll.sh -1 # AUTOSTART # ============================================================================= -# Start Hyprland session target for systemd user services (e.g., waybar) +# Wire Hyprland into Home Manager's standard user-session targets. +# `graphical-session.target` pulls in most tray/SNI applets (which in turn pull in `tray.target`). exec-once = systemctl --user set-environment IMALISON_SESSION_TYPE=wayland exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE +exec-once = systemctl --user start graphical-session.target exec-once = systemctl --user start hyprland-session.target exec-once = ~/.config/hypr/scripts/workspace-history.sh -# Scratchpad applications (spawn on demand via keybinds) -# exec-once = [workspace special:element silent] element-desktop -# exec-once = [workspace special:gmail silent] google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox -# exec-once = [workspace special:htop silent] ghostty --class=htop-scratch --title=htop -e htop -# exec-once = [workspace special:messages silent] google-chrome-stable --new-window https://messages.google.com/web/conversations -# exec-once = [workspace special:slack silent] slack -# exec-once = [workspace special:spotify silent] spotify -# exec-once = [workspace special:transmission silent] transmission-gtk -# exec-once = [workspace special:volume silent] pavucontrol - # Clipboard history daemon exec-once = wl-paste --type text --watch cliphist store exec-once = wl-paste --type image --watch cliphist store diff --git a/dotfiles/config/hypr/hyprscratch.conf b/dotfiles/config/hypr/hyprscratch.conf new file mode 120000 index 00000000..f2c2b372 --- /dev/null +++ b/dotfiles/config/hypr/hyprscratch.conf @@ -0,0 +1 @@ +/nix/store/nax7ksh59bapd3swghaj2ky60krnnzmg-home-manager-files/.config/hypr/hyprscratch.conf \ No newline at end of file diff --git a/nixos/flake.lock b/nixos/flake.lock index b0698c88..26021d11 100644 --- a/nixos/flake.lock +++ b/nixos/flake.lock @@ -475,6 +475,27 @@ "type": "github" } }, + "git-ignore-nix_2": { + "inputs": { + "nixpkgs": [ + "kanshi-sni", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "narHash": "sha256-XmjITeZNMTQXGhhww6ed/Wacy2KzD6svioyCX7pkUu4=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "git-sync-rs": { "inputs": { "flake-utils": [ @@ -484,11 +505,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1770784385, - "narHash": "sha256-EOKx6nqDMY0jRA65NVgYarlzOxfYNWM3Mso4oNJ6RA0=", + "lastModified": 1770787421, + "narHash": "sha256-rkFoV37meFqOImTwH/0dNrBf1r4ZkoU8g85NKwLLAMY=", "owner": "colonelpanic8", "repo": "git-sync-rs", - "rev": "670b127a9e29b463e27ac5bac002b7ac612df93b", + "rev": "14032471eb80ae69b17c85066208a000062d0fe9", "type": "github" }, "original": { @@ -915,15 +936,16 @@ ] }, "locked": { - "lastModified": 1767812182, - "narHash": "sha256-w91iSY7sor8DmBaUv2zYByf0lPWtWLqVn4LitmxZV9E=", - "owner": "sashetophizika", + "lastModified": 1770789464, + "narHash": "sha256-ZP9yyJERehoCl18QwaSHq2eh2UgBr7ZjnnEXk5z2WaA=", + "owner": "colonelpanic8", "repo": "hyprscratch", - "rev": "28fd0f9774b4be6034301a5484b00800eb6f3747", + "rev": "8a20dd005e80935b5cdd21568605cf7023d58293", "type": "github" }, "original": { - "owner": "sashetophizika", + "owner": "colonelpanic8", + "ref": "reapply-rules-on-toggle", "repo": "hyprscratch", "type": "github" } @@ -1099,6 +1121,30 @@ }, "parent": [] }, + "kanshi-sni": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "git-ignore-nix": "git-ignore-nix_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770750664, + "narHash": "sha256-rwDki2UKyZ90LefJLCGPBTZSMNgPGdhmVq0Swf7Pa+8=", + "owner": "taffybar", + "repo": "kanshi-sni", + "rev": "d2c49b844f6f4d1988996214fa5bdf5c3dd0748a", + "type": "github" + }, + "original": { + "owner": "taffybar", + "repo": "kanshi-sni", + "type": "github" + } + }, "mova": { "flake": false, "locked": { @@ -1608,6 +1654,7 @@ "hyprland-plugins": "hyprland-plugins", "hyprscratch": "hyprscratch", "imalison-taffybar": "imalison-taffybar", + "kanshi-sni": "kanshi-sni", "nix": "nix", "nixified-ai": "nixified-ai", "nixos-hardware": "nixos-hardware", diff --git a/nixos/flake.nix b/nixos/flake.nix index ecec0e22..47c15145 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -55,6 +55,14 @@ }; }; + kanshi-sni = { + url = "github:taffybar/kanshi-sni"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + # Hyprland and plugins from official flakes for proper plugin compatibility hyprland = { url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/tags/v0.53.0"; @@ -70,6 +78,11 @@ inputs.hyprland.follows = "hyprland"; }; + hyprscratch = { + url = "github:colonelpanic8/hyprscratch/reapply-rules-on-toggle"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + railbird-secrets = { url = "git+ssh://gitea@dev.railbird.ai:1123/railbird/secrets-flake.git"; inputs = { @@ -112,6 +125,8 @@ status-notifier-item.follows = "status-notifier-item"; xmonad.follows = "xmonad"; xmonad-contrib.follows = "xmonad-contrib"; + dbus-menu.follows = "dbus-menu"; + dbus-hslogger.follows = "dbus-hslogger"; weeder-nix.inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -123,6 +138,11 @@ flake-utils.follows = "flake-utils"; xmonad.follows = "xmonad"; taffybar.follows = "taffybar"; + dbus-menu.follows = "dbus-menu"; + dbus-hslogger.follows = "dbus-hslogger"; + gtk-sni-tray.follows = "gtk-sni-tray"; + gtk-strut.follows = "gtk-strut"; + status-notifier-item.follows = "status-notifier-item"; }; }; @@ -142,6 +162,8 @@ flake-utils.follows = "flake-utils"; git-ignore-nix.follows = "git-ignore-nix"; status-notifier-item.follows = "status-notifier-item"; + gtk-strut.follows = "gtk-strut"; + dbus-menu.follows = "dbus-menu"; }; }; @@ -163,6 +185,20 @@ }; }; + dbus-menu = { + url = "github:taffybar/dbus-menu"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + git-ignore-nix.follows = "git-ignore-nix"; + }; + }; + + dbus-hslogger = { + url = "github:IvanMalison/dbus-hslogger"; + flake = false; + }; + vscode-server = { url = "github:nix-community/nixos-vscode-server"; inputs = { diff --git a/nixos/hyprland.nix b/nixos/hyprland.nix index bf6dbbf2..cc94f9bd 100644 --- a/nixos/hyprland.nix +++ b/nixos/hyprland.nix @@ -10,10 +10,77 @@ makeEnable config "myModules.hyprland" true { withUWSM = true; }; + home-manager.sharedModules = [ + inputs.hyprscratch.homeModules.default + { + services.kanshi = { + enable = true; + systemdTarget = "hyprland-session.target"; + }; + + programs.hyprscratch = { + enable = true; + settings = { + daemon_options = "clean"; + global_options = ""; + global_rules = "float;size monitor_w*0.95 monitor_h*0.95;center"; + + htop = { + command = "alacritty --class htop-scratch --title htop -e htop"; + class = "htop-scratch"; + }; + + volume = { + command = "pavucontrol"; + class = "org.pulseaudio.pavucontrol"; + }; + + spotify = { + command = "spotify"; + class = "spotify"; + }; + + element = { + command = "element-desktop"; + class = "Element"; + }; + + slack = { + command = "slack"; + class = "Slack"; + }; + + transmission = { + command = "transmission-gtk"; + class = "transmission-gtk"; + }; + + dropdown = { + command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown"; + class = "com.mitchellh.ghostty.dropdown"; + options = "persist"; + rules = "float;size monitor_w monitor_h*0.5;move 0 60;noborder;noshadow;animation slide"; + }; + + gmail = { + command = "google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox"; + class = "google-chrome"; + title = "Gmail"; + }; + + messages = { + command = "google-chrome-stable --new-window https://messages.google.com/web/conversations"; + class = "google-chrome"; + title = "Messages"; + }; + }; + }; + } + ]; + # Hyprland-specific packages environment.systemPackages = with pkgs; [ # Hyprland utilities - kanshi # Monitor hotplug management hyprpaper # Wallpaper hypridle # Idle daemon hyprlock # Screen locker