{ config, lib, pkgs, ... }: with lib; let cfg = config.custom.desktops.hyprland.rules; hm = config.home-manager.users.${config.custom.username}; _1password = getExe config.programs._1password-gui.package; ghostty = getExe hm.programs.ghostty.package; launch = hm.home.file.".local/bin/launch".source; libreoffice = getExe config.custom.programs.libreoffice.package; loupe = getExe pkgs.loupe; steam = getExe config.programs.steam.package; uwsm = getExe pkgs.uwsm; virt-manager = getExe pkgs.virt-manager; waydroid = getExe pkgs.waydroid; youtube-music = getExe pkgs.youtube-music; command = command: "${uwsm} app -- ${command}"; in { options.custom.desktops.hyprland.rules = { enable = mkOption {default = false;}; }; config.home-manager.users.${config.custom.username} = mkIf cfg.enable { wayland.windowManager.hyprland.settings = { # BUG: on-created-empty race condition with delayed windows # https://github.com/hyprwm/Hyprland/issues/5663 # https://wiki.hyprland.org/Configuring/Workspace-Rules #?? workspace = WORKSPACE, RULES workspace = [ "special:android, on-created-empty:${command "${launch} --workspace special:android --empty ${waydroid} app launch com.YoStarEN.Arknights"}" "special:game, on-created-empty:${command steam}" "special:music, on-created-empty:${command youtube-music}" "special:office, on-created-empty:${command "${launch} --workspace special:office --empty --tile -- ${libreoffice}"}" "special:password, on-created-empty:${command "${launch} --workspace special:password --empty ${_1password}"}" "special:terminal, on-created-empty:${command ghostty}" "special:vm, on-created-empty:${command "${launch} --workspace special:vm --empty ${virt-manager}"}" "special:wallpaper, on-created-empty:${command "${loupe} /tmp/wallpaper.png"}" ]; # https://wiki.hyprland.org/Configuring/Window-Rules #?? windowrulev2 = RULE, WINDOW windowrulev2 = with config.custom; let gaps_in = gap / 4; # HACK: Attempts to account for hypr-specific scale, gaps, borders, and bar padding ### Static geometry rules tr = num: toString (builtins.floor num); # Convert truncated float to string # Bottom center android = rec { x = tr (width / scale / 2 - (toInt w) / 2); y = tr (height / scale - (toInt h) - gap - padding + ( if ultrawide then - border # Cause unknown else 0 )); w = tr ( width / scale * ( if ultrawide then 3.0 / 8.0 # threeeighths else 1.0 # one ) + ( if ultrawide then - gaps_in * 2 else - gap * 2 ) - border * 2 ); h = tr (height / scale * 0.5); }; # Bottom center clipboard = rec { x = tr (width / scale / 2 - (toInt w) / 2); y = tr (height / scale - (toInt h) - gap - padding + ( if ultrawide then - border # Cause unknown else 0 )); w = "600"; h = tr (height / scale * 0.75); }; # Bottom center dropdown = rec { x = tr (width / scale / 2 - (toInt w) / 2); y = tr (height / scale - (toInt h) - gap - padding + ( if ultrawide then - border # Cause unknown else 0 )); w = tr ( width / scale * ( if ultrawide then 3.0 / 8.0 # threeeighths else 1.0 # one ) + ( if ultrawide then - gaps_in * 2 else - gap * 2 ) - border * 2 ); h = tr (height / scale * ( if ultrawide then 0.2 else 0.3 )); }; # Top right pip = rec { x = tr (width / scale - (toInt w) - gap - border); y = tr (gap + border); w = tr ( width / scale * ( if ultrawide then (1 - 3.0 / 8.0) / 2 # threeeighths / 2 else 1.0 / 3.0 # onethird ) - gap - gaps_in - border * 2 ); h = tr ((toInt w) * 9 / 16 + 1); # 16:9 aspect ratio }; ### Rules # Return hypr-formatted string, converting booleans into 0/1 format = field: expr: "${field}:${ toString ( if expr == true then 1 else if expr == false then 0 else expr ) }"; # Generate hypr-formatted window rules #?? merge merge = field: expr: rules: map ( rule: if isAttrs field then "${rule}, ${lib.concatStringsSep ", " (lib.mapAttrsToList (f: e: format f e) field)}" else "${rule}, ${format field expr}" ) rules; class = expr: rules: merge "class" "^${expr}$" rules; fields = fields: rules: merge fields null rules; floating = expr: rules: merge "floating" expr rules; focus = expr: rules: merge "focus" expr rules; fullscreen = expr: rules: merge "fullscreen" expr rules; pinned = expr: rules: merge "pinned" expr rules; tag = expr: rules: merge "tag" expr rules; title = expr: rules: merge "title" "^${expr}$" rules; ### Pseudo-tags # Wrap generated rules in Nix categories t = { android = rules: [ (class "[Ww]aydroid.*" rules) ]; browser = rules: [ (class "^brave-browser$" rules) (class "^chromium-browser$" rules) (class "^firefox.*$" rules) (class "^google-chrome$" rules) (class "^librewolf$" rules) (class "^vivaldi.*$" rules) (class "^zen$" rules) ]; clipboard = rules: [ (class "clipboard" rules) ]; dropdown = rules: [ (class ".*dropdown" rules) ]; editor = rules: [ (class "codium" rules) # VSCode (class "obsidian" rules) ]; files = rules: [ (class "org\\.gnome\\.Nautilus" rules) ]; game = rules: [ (class ".*\\.(exe|x86_64)" rules) # Wine (class "moe\\.launcher\\..+" (rules ++ ["size 1280 730" "plugin:hyprbars:nobar"])) # An Anime Game Launcher (class "net.retrodeck.retrodeck" rules) # Emulators (class "steam_app_.+" rules) # Proton ]; media = rules: [ (class "org\\.gnome\\.Loupe" rules) (class "totem" rules) ]; music = rules: [ (class "com\\.github\\.th_ch\\.youtube_music" rules) (class "Spotify" rules) (class "YouTube Music" rules) (title "Spotify Premium" rules) ]; office = rules: [ (class "draw\\.io" rules) (class "libreoffice.+" rules) (class "ONLYOFFICE Desktop Editors" rules) ]; password = rules: [ (class "1Password" rules) (class "Bitwarden" rules) ]; pip = rules: [ (title "Picture.in.[Pp]icture" rules) ]; social = rules: [ (class "cinny" rules) (class "discord" rules) (class "Element" rules) (class "org\\.telegram\\.desktop" rules) (class "signal" rules) ]; steam = rules: [ (class "SDL Application" rules) # Steam (class "steam" rules) ]; terminal = rules: [ (class "foot" rules) (class "com\\.mitchellh\\.ghostty" rules) (class "kitty" rules) (class "org\\.wezfurlong\\.wezterm" rules) ]; vm = rules: [ (class "(sdl-|wl|x)freerdp" (rules ++ ["nomaxsize" "tile"])) (class "looking-glass-client" (rules ++ ["plugin:hyprbars:nobar"])) (class "org\\.remmina\\.Remmina" (rules ++ ["tile"])) (class "virt-manager" rules) ]; }; in flatten [ ### Defaults (floating false ["noshadow" "plugin:hyprbars:nobar"]) (floating true ["noborder"]) (focus false ["plugin:hyprbars:bar_color rgb(073642)" "plugin:hyprbars:title_color rgb(586e75)"]) (fullscreen true ["idleinhibit focus"]) (pinned true ["bordercolor rgb(fdf6e3) rgb(fdf6e300)" "noborder 0" "noshadow"]) (tag "scroller:pinned" ["bordercolor rgb(fdf6e3) rgb(fdf6e300)"]) (t.android ["float" "idleinhibit always" "move ${android.x} ${android.y}" "size ${android.w} ${android.h}" "workspace special:android silent" "plugin:hyprbars:nobar"]) (t.browser ["workspace 1"]) (t.clipboard ["float" "move ${clipboard.x} ${clipboard.y}" "pin" "size ${clipboard.w} ${clipboard.h}" "stayfocused" "plugin:hyprbars:nobar"]) (t.dropdown ["float" "move ${dropdown.x} ${dropdown.y}" "pin" "size ${dropdown.w} ${dropdown.h}" "plugin:hyprbars:nobar"]) (t.game ["focusonactivate" "idleinhibit always" "noborder" "noshadow" "renderunfocused" "workspace name:game" "plugin:hyprbars:nobar"]) (t.media ["center" "float" "keepaspectratio" "size <90% <90%"]) (t.music ["workspace special:music silent"]) (t.office ["workspace special:office silent" "plugin:scroller:group office"]) (t.password ["workspace special:password silent" "plugin:hyprbars:nobar"]) (t.pip ["float" "keepaspectratio" "move ${pip.x} ${pip.y}" "noinitialfocus" "pin" "size ${pip.w} ${pip.h}" "plugin:hyprbars:nobar"]) (t.social ["plugin:scroller:group social" "plugin:scroller:columnwidth onequarter" "plugin:scroller:windowheight onehalf"]) (t.steam ["suppressevent activate activatefocus" "workspace special:steam silent" "plugin:hyprbars:nobar"]) (t.terminal ["plugin:scroller:group terminal"]) (t.vm ["workspace special:vm silent" "plugin:scroller:group vm"]) ### Overrides (class "org\\.gnome\\.NautilusPreviewer" ["float" "stayfocused" "plugin:hyprbars:nobar"]) # Sushi (class "steam_app_1473350" ["workspace 0"]) # (the) Gnorp Apologue (class "Tap Wizard 2\\.x86_64" ["workspace 0"]) #!! Expressions are not wrapped in ^$ (fields { class = "^discord$"; title = "^Discord Updater$"; # Update dialog } ["float" "nofocus" "plugin:hyprbars:nobar"]) (fields { class = "^org\\.gnome\\.Loupe$"; title = "^wallpaper.png$"; } ["workspace special:wallpaper silent"]) (fields { class = "^steam$"; title = "^notificationtoasts$"; # Steam notifications } ["float" "nofocus" "pin"]) ]; }; }; }