{ config, lib, pkgs, ... }: let cfg = config.services.aerospace; format = pkgs.formats.toml { }; 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 { options = { services.aerospace = with lib.types; { enable = lib.mkEnableOption "AeroSpace window manager"; package = lib.mkPackageOption pkgs "aerospace" { }; settings = lib.mkOption { type = submodule { freeformType = format.type; options = { start-at-login = lib.mkOption { type = bool; default = false; description = "Do not start AeroSpace at login. (Managed by launchd instead)"; }; after-login-command = lib.mkOption { type = listOf str; default = [ ]; description = "Do not use AeroSpace to run commands after login. (Managed by launchd instead)"; }; after-startup-command = lib.mkOption { type = listOf str; default = [ ]; description = "Add commands that run after AeroSpace startup"; example = [ "layout tiles" ]; }; enable-normalization-flatten-containers = lib.mkOption { type = bool; default = true; description = "Containers that have only one child are \"flattened\"."; }; enable-normalization-opposite-orientation-for-nested-containers = lib.mkOption { type = bool; default = true; description = "Containers that nest into each other must have opposite orientations."; }; accordion-padding = lib.mkOption { type = int; default = 30; description = "Padding between windows in an accordion container."; }; default-root-container-layout = lib.mkOption { type = enum [ "tiles" "accordion" ]; default = "tiles"; description = "Default layout for the root container."; }; default-root-container-orientation = lib.mkOption { type = enum [ "horizontal" "vertical" "auto" ]; default = "auto"; description = "Default orientation for the root container."; }; on-window-detected = lib.mkOption { 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 = [ ]; 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" ]; description = "Commands to run every time focused monitor changes."; }; exec-on-workspace-change = lib.mkOption { type = listOf str; default = [ ]; example = [ "/bin/bash" "-c" "sketchybar --trigger aerospace_workspace_change FOCUSED=$AEROSPACE_FOCUSED_WORKSPACE" ]; description = "Commands to run every time workspace changes."; }; key-mapping.preset = lib.mkOption { type = enum [ "qwerty" "dvorak" ]; default = "qwerty"; description = "Keymapping preset."; }; }; }; default = { }; example = lib.literalExpression '' { gaps = { outer.left = 8; outer.bottom = 8; outer.top = 8; outer.right = 8; }; mode.main.binding = { alt-h = "focus left"; alt-j = "focus down"; alt-k = "focus up"; alt-l = "focus right"; }; } ''; description = '' AeroSpace configuration, see for supported values. ''; }; }; }; config = ( lib.mkIf (cfg.enable) { assertions = [ { assertion = !cfg.settings.start-at-login; message = "AeroSpace started at login is managed by home-manager and launchd instead of itself via this option."; } { assertion = cfg.settings.after-login-command == [ ]; message = "AeroSpace will not run these commands as it does not start itself."; } ]; environment.systemPackages = [ cfg.package ]; launchd.user.agents.aerospace = { command = "${cfg.package}/Applications/AeroSpace.app/Contents/MacOS/AeroSpace" + (lib.optionalString (cfg.settings != { }) " --config-path ${configFile}"); serviceConfig = { KeepAlive = true; RunAtLoad = true; }; }; } ); }