From 0873f56c288766deaa5483c69e7683f9679fcd36 Mon Sep 17 00:00:00 2001 From: ooks-io Date: Wed, 15 Jan 2025 23:03:36 +1100 Subject: [PATCH] hyprland: workspace/rules module init still some work too be done, the regex types are cursed. --- .../workstation/hyprland/settings/default.nix | 2 + .../hyprland/settings/options/default.nix | 140 +++++++++++ .../hyprland/settings/options/rules.nix | 237 ++++++++++++++++++ .../workstation/hyprland/settings/rules.nix | 53 ++-- .../hyprland/settings/workspaces.nix | 37 +++ 5 files changed, 447 insertions(+), 22 deletions(-) create mode 100644 modules/home/workstation/hyprland/settings/options/default.nix create mode 100644 modules/home/workstation/hyprland/settings/options/rules.nix create mode 100644 modules/home/workstation/hyprland/settings/workspaces.nix diff --git a/modules/home/workstation/hyprland/settings/default.nix b/modules/home/workstation/hyprland/settings/default.nix index 7645edd..02c8304 100644 --- a/modules/home/workstation/hyprland/settings/default.nix +++ b/modules/home/workstation/hyprland/settings/default.nix @@ -8,5 +8,7 @@ ./monitor.nix ./gestures.nix ./appearance.nix + ./workspaces.nix + ./options ]; } diff --git a/modules/home/workstation/hyprland/settings/options/default.nix b/modules/home/workstation/hyprland/settings/options/default.nix new file mode 100644 index 0000000..a7d5605 --- /dev/null +++ b/modules/home/workstation/hyprland/settings/options/default.nix @@ -0,0 +1,140 @@ +{ + lib, + config, + ... +}: let + inherit + (lib) + flatten + attrValues + concatStringsSep + filterAttrs + mapAttrsToList + boolToString + mkOption + isBool + ; + inherit + (lib.types) + listOf + attrsOf + submodule + nullOr + str + int + bool + oneOf + ; + + # our cursed regex types + hyprland = import ./rules.nix {inherit lib;}; + + _toString = type: val: + if isBool val + then + if type == "windowrule" + then + if val + then "1" + else "0" + else boolToString val # for workspace rules + else toString val; + + # format rules for hyprland + formatRules = type: rule: + concatStringsSep "," ( + mapAttrsToList (name: value: "${name}:${_toString type value}") + (filterAttrs (_: v: v != null) rule) + ); + + # workspace handling + mkWorkspaces = mapAttrsToList ( + selector: rules: "${selector},${formatRules "workspacerule" rules}" + ); + + # rule options + mkRuleOption = type: description: + mkOption { + type = nullOr type; + default = null; + inherit description; + }; + + # window rule matchers + windowRuleMatchers = submodule { + options = { + class = mkRuleOption str "Window class matcher"; + title = mkRuleOption str "Window title matcher"; + initialClass = mkRuleOption str "Initial window class matcher"; + initialTitle = mkRuleOption str "Initial window title matcher"; + xwayland = mkRuleOption bool "Match XWayland windows"; + floating = mkRuleOption bool "Match floating windows"; + fullscreen = mkRuleOption bool "Match fullscreen windows"; + workspace = mkRuleOption str "Match windows on specific workspace"; + }; + }; + + # Workspace rules submodule + workspaceRules = submodule { + options = { + name = mkRuleOption str "Default name of workspace"; + monitor = mkRuleOption str "Binds workspace to monitor"; + default = mkRuleOption bool "Set as default workspace for monitor"; + gapsin = mkRuleOption int "Gaps between windows"; + gapsout = mkRuleOption int "Gaps between windows and monitor edges"; + bordersize = mkRuleOption int "Border size around windows"; + border = mkRuleOption bool "Draw borders"; + shadow = mkRuleOption bool "Draw shadows"; + rounding = mkRuleOption bool "Draw rounded corners"; + decorate = mkRuleOption bool "Draw window decorations"; + persistent = mkRuleOption bool "Keep workspace alive when empty"; + on-created-empty = mkRuleOption str "Command to run when workspace created empty"; + }; + }; + + # Window rules type using our validated types from rules.nix + windowRuleType = listOf (oneOf (attrValues hyprland.types)); + + cfg = config.wayland.windowManager.hyprland; +in { + options.wayland.windowManager.hyprland = { + # Workspace configuration + workspaces = mkOption { + type = attrsOf workspaceRules; + default = {}; + description = "Workspace-specific configurations"; + }; + + # Window rules configuration + windowRules = mkOption { + type = listOf (submodule { + options = { + matches = mkOption { + type = windowRuleMatchers; + description = "Window matching criteria"; + }; + rules = mkOption { + type = windowRuleType; + description = "Rules to apply to matching windows"; + }; + }; + }); + default = []; + description = "Window-specific rules"; + }; + }; + + config.wayland.windowManager.hyprland.settings = { + workspace = mkWorkspaces cfg.workspaces; + windowrulev2 = let + # Convert rules to Hyprland format + formatWindowRule = rule: let + matches = formatRules "windowrule" rule.matches; + rules = map (r: "${r},${matches}") rule.rules; + in + rules; + in + flatten (map formatWindowRule cfg.windowRules); + }; +} + diff --git a/modules/home/workstation/hyprland/settings/options/rules.nix b/modules/home/workstation/hyprland/settings/options/rules.nix new file mode 100644 index 0000000..24040a1 --- /dev/null +++ b/modules/home/workstation/hyprland/settings/options/rules.nix @@ -0,0 +1,237 @@ +{lib, ...}: let + inherit (lib) concatStringsSep attrValues; + inherit (lib.types) enum strMatching; + + # helper function for constructing regex patterns for hyprland rules + mkRegexTarget = { + name, + regex ? [], + extraRegex ? "", + optional ? false, + }: let + regexPart = concatStringsSep "|" regex; + parameterPart = + if optional + then "( (${regexPart}))?" + else " (${regexPart})"; + in + strMatching "${name}${parameterPart}${extraRegex}"; + + # basic windowrules that can be validated with simple enum + + basicWindowRules = enum [ + "float" + "tile" + "fullscreen" + "maximize" + "pseudo" + "noinitialfocus" + "pin" + "unset" + "nomaxsize" + "stayfocused" + ]; + + # toggleable options + toggleableRules = [ + # window decoration + "decorate" # window decorations + "noborder" # borders + "noshadow" # shadows + "norounding" # corner rounding + + # Visual effects + "noblur" # blur + "noanim" # animations + "nodim" # dimming + "opaque" # opacity enforcement + "forcergbx" # RGB handling + "xray" # blur xray mode + "immediate" # tearing mode + + # Behavior + "dimaround" # dim around window + "focusonactivate" # focus on activation request + "nofocus" # disable focus + "stayfocused" # keep focus + "keepaspectratio" # maintain aspect ratio + "nearestneighbor" # nearest neighbor filtering + "nomaxsize" # disable max size + "noshortcutsinhibit" # shortcut inhibiting + "allowsinput" # XWayland input forcing + "renderunfocused" # unfocused rendering + "syncfullscreen" # fullscreen sync + ]; + + # reusable regex pattens to be used in constructing our types + patterns = { + # toggles + onOpt = "1|true|on|yes|0|salse|off|no|toggle|unset"; + + # numbers + float = "[+-]?([0-9]*[.])?[0-9]+"; + int = "[0-9]+"; + alpha = ''(0|0?\.[[:digit:]]+|1|1\.0)''; + percentage = "[0-9]+(%)?"; + + # position + anchor = "100%-w?-[0-9]+"; + coordinate = "${patterns.percentage}|${patterns.anchor}"; + deg = "(0-360)"; + + # identification + numericId = "[1-9][0-9]*"; + sign = "[+-]"; + relative = "${patterns.sign}${patterns.numericId}"; + monitorPrefix = "(m|r)"; + monitorRelative = "${patterns.monitorPrefix}(${patterns.sign}${patterns.numericId}|~${patterns.numericId})"; + emptyModifiers = "[mn]*"; + empty = "empty${patterns.emptyModifiers}"; + mode = "(always|focus|fullscreen|none)"; + ident = "[A-Za-z0-9_][A-Za-z0-9_ -]*"; + name = "name:${patterns.ident}"; + special = "special(:[a-zA-Z0-9_-]+)?"; + previous = "previous(_per_monitor)?"; + openWorkspace = "e(${patterns.sign}${patterns.numericId}|~${patterns.numericId})"; + + # colors + rgbValue = ''([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])''; + rgbaHex = ''rgba\([[:xdigit:]]{8}\)''; + rgbaDecimal = ''rgba\(${patterns.rgbValue}, *${patterns.rgbValue}, *${patterns.rgbValue}, *${patterns.alpha}\)''; + rgbHex = ''rgb\([[:xdigit:]]{3}([[:xdigit:]]{3})?\)''; + rgbDecimal = ''rgb\(${patterns.rgbValue}, *${patterns.rgbValue}, *${patterns.rgbValue}\)''; + legacy = ''0x[[:xdigit:]]{8}''; + color = concatStringsSep "|" [ + patterns.rgbaHex + patterns.rgbaDecimal + patterns.rgbHex + patterns.rgbDecimal + patterns.legacy + ]; + }; + + mkToggleableRules = name: + mkRegexTarget { + inherit name; + regex = [patterns.onOpt]; + optional = true; + }; + + toggleTargets = builtins.listToAttrs (map (name: { + inherit name; + value = mkToggleableRules name; + }) + toggleableRules); + + # regex patterns to pass to the workspace rule + + types = + { + inherit basicWindowRules; + workspace = mkRegexTarget { + name = "workspace"; + regex = attrValues { + inherit (patterns) numericId relative monitorRelative empty name special previous openWorkspace; + }; + extraRegex = "( +silent)?"; + }; + + monitor = mkRegexTarget { + name = "monitor"; + regex = attrValues { + inherit (patterns) numericId ident; + }; + }; + + group = mkRegexTarget { + name = "group"; + regex = ["set|new|lock|barred|deny|invade|override|unset"]; + }; + + size = mkRegexTarget { + name = "size"; + regex = [ + "([0-9]+|[<>][0-9]+)(%|px)?( ([0-9]+|[<>][0-9]+)(%|px)?)?" + ]; + }; + + move = mkRegexTarget { + name = "move"; + regex = [ + "(${patterns.coordinate}) (${patterns.coordinate})" + "cursor (${patterns.percentage}) (${patterns.percentage})" + "onscreen cursor (${patterns.percentage}) (${patterns.percentage})" + ]; + }; + + bordercolor = mkRegexTarget { + name = "bordercolor"; + regex = [patterns.color]; + }; + + idleinhibit = mkRegexTarget { + name = "idleinhibit"; + regex = [patterns.mode]; + }; + + opacity = mkRegexTarget { + name = "opacity"; + regex = let + opacityValue = "${patterns.alpha}( override)?"; + in [ + opacityValue + "${opacityValue} ${opacityValue}" + "${opacityValue} ${opacityValue} ${opacityValue}" + ]; + }; + + center = mkRegexTarget { + name = "center"; + regex = ["[0-1]"]; + optional = true; + }; + + roundingpower = mkRegexTarget { + name = "roundingpower"; + regex = [patterns.float]; + }; + + bordersize = mkRegexTarget { + name = "bordersize"; + regex = [patterns.int]; + }; + + rounding = mkRegexTarget { + name = "rounding"; + regex = [patterns.int]; + }; + + scrollmouse = mkRegexTarget { + name = "scrollmouse"; + regex = [patterns.float]; + }; + + scrolltouchpad = mkRegexTarget { + name = "scrolltouchpad"; + regex = [patterns.float]; + }; + + tag = mkRegexTarget { + name = "tag"; + regex = [''[+-]?[[:alnum:]_]+\*?'']; + }; + + maxsize = mkRegexTarget { + name = "maxsize"; + regex = ["${patterns.int} ${patterns.int}"]; + }; + + minsize = mkRegexTarget { + name = "minsize"; + regex = ["${patterns.int} ${patterns.int}"]; + }; + } + // toggleTargets; +in { + inherit types; +} diff --git a/modules/home/workstation/hyprland/settings/rules.nix b/modules/home/workstation/hyprland/settings/rules.nix index cc79331..0edfa89 100644 --- a/modules/home/workstation/hyprland/settings/rules.nix +++ b/modules/home/workstation/hyprland/settings/rules.nix @@ -1,24 +1,33 @@ { - wayland.windowManager.hyprland.settings = { - windowrulev2 = [ - "float,move 191 15,size 924 396,class:^(1Password)$" - - "float, title:^(Picture-in-Picture)$" - "pin, title:^(Picture-in-Picture)$" - - "float,move 237 175, size 1200 720,title:^(File Upload)$" - - "workspace 4, title:^(Vesktop)$" - - # Floating BTOP - "float,title:^(BTOP)$" - "size 85%,title:^(BTOP)$" - "pin,title:^(BTOP)$" - "center,title:^(BTOP)$" - "stayfocused,title:^(BTOP)$" - - # Tearing - "immediate, title:^(TEKKEN™8)$" - ]; - }; + wayland.windowManager.hyprland.windowRules = [ + # TODO tag games for immediate + { + matches = {title = "TEKKEN™8";}; + rules = ["immediate"]; + } + { + matches = {class = "firefox";}; + rules = ["idleinhibit fullscreen"]; + } + { + matches = {class = "1Password";}; + rules = ["center 1" "float" "size 50%"]; + } + { + matches = {title = "BTOP";}; + rules = ["float" "size 85%" "pin" "center 1" "stayfocused" "dimaround"]; + } + { + matches = {class = "vesktop";}; + rules = ["workspace 4 silent"]; + } + { + matches = {title = "^(Picture-in-Picture)$";}; + rules = ["float" "pin"]; + } + { + matches = {title = "^(Open Files)$";}; + rules = ["center 1" "float" "size 50%"]; + } + ]; } diff --git a/modules/home/workstation/hyprland/settings/workspaces.nix b/modules/home/workstation/hyprland/settings/workspaces.nix new file mode 100644 index 0000000..952091b --- /dev/null +++ b/modules/home/workstation/hyprland/settings/workspaces.nix @@ -0,0 +1,37 @@ +{osConfig, ...}: let + inherit (osConfig.ooknet) hardware; + multiMonitor = builtins.length hardware.monitors > 1; + primary = hardware.primaryMonitor; + secondary = + if multiMonitor + then (builtins.elemAt hardware.monitors 1).name + else primary; +in { + wayland.windowManager.hyprland.workspaces = { + "1" = { + name = "terminal"; + monitor = primary; + default = true; + }; + "2" = { + name = "browser"; + monitor = primary; + }; + "3" = { + name = "media"; + monitor = secondary; + default = true; + }; + "4" = { + name = "discord"; + monitor = secondary; + }; + "5" = { + name = "gaming"; + monitor = primary; + }; + "r[6-9]" = { + monitor = primary; + }; + }; +}