diff --git a/modules/services/aerospace/default.nix b/modules/services/aerospace/default.nix index 531e7b220..56315fa66 100644 --- a/modules/services/aerospace/default.nix +++ b/modules/services/aerospace/default.nix @@ -3,13 +3,34 @@ lib, pkgs, ... -}: - -let +}: let cfg = config.services.aerospace; - format = pkgs.formats.toml { }; - configFile = format.generate "aerospace.toml" cfg.settings; + filterAttrsRecursive = pred: set: + lib.listToAttrs ( + lib.concatMap ( + name: let + v = set.${name}; + in + if pred v + then [ + (lib.nameValuePair name ( + if lib.isAttrs v + then filterAttrsRecursive pred v + else if lib.isList v + then + (map (i: + if lib.isAttrs i + then filterAttrsRecursive pred i + else i) (lib.filter pred v)) + else v + )) + ] + else [] + ) (lib.attrNames set) + ); + filterNulls = filterAttrsRecursive (v: v != null); + configFile = format.generate "aerospace.toml" (filterNulls cfg.settings); in { @@ -17,7 +38,7 @@ in services.aerospace = with lib.types; { enable = lib.mkEnableOption "AeroSpace window manager"; - package = lib.mkPackageOption pkgs "aerospace" { }; + package = lib.mkPackageOption pkgs "aerospace" {}; settings = lib.mkOption { type = submodule { @@ -30,14 +51,14 @@ in }; after-login-command = lib.mkOption { type = listOf str; - default = [ ]; + default = []; description = "Do not use AeroSpace to run commands after login. (Managed by launchd instead)"; }; after-startup-command = lib.mkOption { type = listOf str; - default = [ ]; + default = []; description = "Add commands that run after AeroSpace startup"; - example = [ "layout tiles" ]; + example = ["layout tiles"]; }; enable-normalization-flatten-containers = lib.mkOption { type = bool; @@ -72,23 +93,100 @@ in description = "Default orientation for the root container."; }; on-window-detected = lib.mkOption { - type = listOf str; - default = [ ]; - description = "Commands to run every time a new window is detected."; + type = types.listOf (types.attrsOf types.anything); + default = []; + type = listOf (submodule { + options = { + "if" = lib.mkOption { + type = submodule { + options = { + app-id = lib.mkOption { + type = nullOr str; + default = null; + description = "The application ID to match (optional)."; + }; + workspace = lib.mkOption { + type = nullOr str; + default = null; + description = "The workspace name to match (optional)."; + }; + window-title-regex-substring = lib.mkOption { + type = nullOr str; + default = null; + description = "Substring to match in the window title (optional)."; + }; + app-name-regex-substring = lib.mkOption { + type = nullOr str; + default = null; + description = "Regex substring to match the app name (optional)."; + }; + during-aerospace-startup = lib.mkOption { + type = nullOr bool; + default = null; + description = "Whether to match during aerospace startup (optional)."; + }; + }; + }; + default = {}; + description = "Conditions for detecting a window."; + }; + check-further-callbacks = lib.mkOption { + type = nullOr bool; + default = null; + description = "Whether to check further callbacks after this rule (optional)."; + }; + run = lib.mkOption { + type = oneOf [str (listOf str)]; + example = ["move-node-to-workspace m" "resize-node"]; + description = "Commands to execute when the conditions match (required)."; + }; + }; + }); + default = []; + example = [ + { + "if" = { + app-id = "Another.Cool.App"; + workspace = "cool-workspace"; + window-title-regex-substring = "Title"; + app-name-regex-substring = "CoolApp"; + during-aerospace-startup = false; + }; + check-further-callbacks = false; + run = ["move-node-to-workspace m" "resize-node"]; + } + ]; + description = "Commands to run every time a new window is detected with optional conditions."; + }; + workspace-to-monitor-force-assignment = lib.mkOption { + type = attrsOf (oneOf [int str (listOf str)]); + default = { }; + description = '' + Map workspaces to specific monitors. + Left-hand side is the workspace name, and right-hand side is the monitor pattern. + ''; + example = { + "1" = 1; # First monitor from left to right. + "2" = "main"; # Main monitor. + "3" = "secondary"; # Secondary monitor (non-main). + "4" = "built-in"; # Built-in display. + "5" = "^built-in retina display$"; # Regex for the built-in retina display. + "6" = ["secondary" "dell"]; # Match first pattern in the list. + }; }; on-focus-changed = lib.mkOption { type = listOf str; - default = [ ]; + default = []; description = "Commands to run every time focused window or workspace changes."; }; on-focused-monitor-changed = lib.mkOption { type = listOf str; - default = [ "move-mouse monitor-lazy-center" ]; + default = ["move-mouse monitor-lazy-center"]; description = "Commands to run every time focused monitor changes."; }; exec-on-workspace-change = lib.mkOption { type = listOf str; - default = [ ]; + default = []; example = [ "/bin/bash" "-c" @@ -106,7 +204,7 @@ in }; }; }; - default = { }; + default = {}; example = lib.literalExpression '' { gaps = { @@ -140,16 +238,16 @@ in message = "AeroSpace started at login is managed by home-manager and launchd instead of itself via this option."; } { - assertion = cfg.settings.after-login-command == [ ]; + assertion = cfg.settings.after-login-command == []; message = "AeroSpace will not run these commands as it does not start itself."; } ]; - environment.systemPackages = [ cfg.package ]; + environment.systemPackages = [cfg.package]; launchd.user.agents.aerospace = { command = "${cfg.package}/Applications/AeroSpace.app/Contents/MacOS/AeroSpace" - + (lib.optionalString (cfg.settings != { }) " --config-path ${configFile}"); + + (lib.optionalString (cfg.settings != {}) " --config-path ${configFile}"); serviceConfig = { KeepAlive = true; RunAtLoad = true; diff --git a/tests/services-aerospace.nix b/tests/services-aerospace.nix index 28688c461..088c92d95 100644 --- a/tests/services-aerospace.nix +++ b/tests/services-aerospace.nix @@ -21,6 +21,32 @@ in alt-k = "focus up"; alt-l = "focus right"; }; + on-window-detected = [ + { + "if" = { + app-id = "Another.Cool.App"; + during-aerospace-startup = false; + }; + check-further-callbacks = false; + run = "move-node-to-workspace m"; + } + { + "if".app-name-regex-substring = "finder|calendar"; + run = "layout floating"; + } + { + "if".workspace = "1"; + run = "layout h_accordion"; + } + ]; + workspace-to-monitor-force-assignment = { + "1" = 1; + "2" = "main"; + "3" = "secondary"; + "4" = "built-in"; + "5" = "^built-in retina display$"; + "6" = [ "secondary" "dell" ]; + }; }; test = '' @@ -43,5 +69,24 @@ in grep 'alt-j = "focus down"' $conf grep 'alt-k = "focus up"' $conf grep 'alt-l = "focus right"' $conf + + grep 'check-further-callbacks = false' $conf + grep 'run = "move-node-to-workspace m"' $conf + grep 'app-id = "Another.Cool.App"' $conf + grep 'during-aerospace-startup = false' $conf + + grep 'run = "layout floating"' $conf + grep 'app-name-regex-substring = "finder|calendar"' $conf + (! grep 'window-title-regex-substring' $conf) + + grep 'workspace = "1"' $conf + grep 'run = "layout h_accordion"' $conf + + grep '1 = 1' $conf + grep '2 = "main"' $conf + grep '3 = "secondary"' $conf + grep '4 = "built-in"' $conf + grep '5 = "^built-in retina display$"' $conf + grep '6 = \["secondary", "dell"\]' $conf ''; }