hyprland: workspace/rules module init

still some work too be done, the regex types are cursed.
This commit is contained in:
ooks-io 2025-01-15 23:03:36 +11:00
parent 0b8a730519
commit 0873f56c28
5 changed files with 447 additions and 22 deletions

View file

@ -8,5 +8,7 @@
./monitor.nix
./gestures.nix
./appearance.nix
./workspaces.nix
./options
];
}

View file

@ -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);
};
}

View file

@ -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;
}

View file

@ -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:^(TEKKEN8)$"
];
};
wayland.windowManager.hyprland.windowRules = [
# TODO tag games for immediate
{
matches = {title = "TEKKEN8";};
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%"];
}
];
}

View file

@ -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;
};
};
}