nikitabobko / AeroSpace

AeroSpace is an i3-like tiling window manager for macOS
https://nikitabobko.github.io/AeroSpace/guide
MIT License
5.32k stars 87 forks source link

Provide a CLI flag to specify the target config file #320

Closed remi-gelinas closed 1 month ago

remi-gelinas commented 2 months ago

I'd love to see a --config or -c flag for the CLI that would allow one to specify the target config file. This would not only help in settings where you're testing different configs (directly running the AeroSpace binary in the app bundle to test configs for example, though I realize the intended way to do this is probably to hot reload the config as you make changes).

In my very narrow use case, this would also allow system management tools that manage available packages and launchd services to keep the AeroSpace launchd agent in sync with the available app bundle. I use Nix and nix-darwin to manage my hosts, and yabai provides the -c flag to specify the target config - this allows me to keep the launchd service in sync with the provided binary and config.

For example, one could specify a config file outside of the two allowed config locations AeroSpace currently provides:

AeroSpace.app/Content/MacOS/AeroSpace --running-at-start -c /etc/aerospace.toml

nikitabobko commented 1 month ago

I had an idea to provide flag for custom config file location, when I was implementing --config-path option in config command. But I stumbled upon the following design problem:

aerospace reload-config --path /custom/path/to/aerospace.toml

# Shall the second invocation of reload-config read the config from default location
# or keep reading it from the custom location?
aerospace reload-config

Surely, two flags can be introduced: --persistent-config-path and --one-time-config-path, but I'm afraid of unnecessary complications, and would love to get on only with a single flag if possible. What would be your preferred option and why?


I also noticed that you interact with AeroSpace.app server directly. I've not considered the server CLI as an API entry point. --started-at-login is an internal implementation detail. At least for now. (I assume that you meant --started-at-login instead of --running-at-start. --running-at-start is not an existing flag)

I'm curious to learn more about your use case. I don't know a lot about nix and nix-darwin. All I know is how nix stores the packages to achieve better dependency control. Is AeroSpace installed via Nix, so that's why you have to interact with AeroSpace.app directly?

remi-gelinas commented 1 month ago

As to the design problem, personally I think I'd like to see the server be started with the target config path, and then consider that in all following operations. i.e. if I run it with -c /etc/aerospace.toml, reload-config would reload at that location until the server is restarted with a different config location.

As for why, purely because I think it would follow most daemon design that start parameters don't change unless the daemon is restarted with new ones, but that's just my view :)

As for the nix use case, it'll maybe take a bit more explanation for why this would solve a problem I have. nix-darwin provides modules for configuring a MacOS system (users, system defaults, available packages in the environment, etc.), but the specifics are largely unimportant. Services bundled into nix-darwin modules that need launchd agents typically are installed along with a deterministic agent config based on the Nix store path of the app bundle or binary, and are pointed to a deterministic config again created in the Nix store.

Without getting into the weeds about Nix syntax and the module system, the intent is for a particular revision of a system config to create the same launch agents that reference the same target binary all the time - and nix-darwin manages those agents and their creation and lifecycle.

For a good example, take the nix-darwin module for yabai, here. This definition results in a config file being created in the Nix store, the yabai binary being available in the system PATH, and a launchd agent being created that calls the exact version of the target yabai binary in the store with the config generated in the store being passed in.

If you were to update the yabai package to a new version, for example, nix-darwin will ensure that the resultant launchd agent is unloaded, amended to point to the new correct binary, and then reloaded. Where this breaks when using AeroSpace is because it manages its own agent, if I update the app bundle available in my environment, as it stands I have to stop the AeroSpace service, delete the launch agent, and restart with the new app bundle to have the new agent created and pointing to the correct version of the bundle.

I recognize that this is an oddly narrow use case, and probably not one you intended to support when designing AeroSpace in the first place, so I'll absolutely accept telling me to go kick rocks if it's an out of scope change :) That said, adjusting for out-of-band agent management would also support other configuration management tools that manage agent lifecycle that aren't necessarily Nix - and personally, I could absolutely see a use case for being able to point the binary to a config in order to validate it (though that would likely require the config validation that's done in reload-config to be split out into a separate command, for example I could see a lot of use of aerospace validate-config -c <config_file>).

EDIT: And yes, I definitely did mean --started-at-login, sorry about that!

nikitabobko commented 1 month ago

Implemented in 0.13.0-Beta:

USAGE: ./.debug/AeroSpace-Debug.app/Contents/MacOS/AeroSpace-Debug [<options>]

OPTIONS:
  -h, --help              Print help
  -v, --version           Print AeroSpace.app version
  --started-at-login      Make AeroSpace.app think that it is started at login
  --config-path <path>    Config path. If the file exists it will takes priority
                          over ~/.aerospace.toml and ${XDG_CONFIG_HOME}/aerospace/aerospace.toml
remi-gelinas commented 1 month ago

Awesome, thank you for the flag! I'll get to testing this today. Does this change agent behaviour at all? i.e Can I manually create a launch agent with the config file flag, or will that still misbehave?

nikitabobko commented 1 month ago

or will that still misbehave?

What do you mean?

remi-gelinas commented 1 month ago

or will that still misbehave?

What do you mean?

Actually, I had assumed that because it was an internal implementation detail as you mentioned earlier that it wasn't really meant for running directly. However, I wrote my nix-darwin module to create a launch agent running the server without the --started-at-login flag and pointing the --config-path at the generated config in the Nix store, and it works like a charm!

DavSanchez commented 2 weeks ago

Hey @remi-gelinas, are you planning to add your module to nix-darwin? I had thought on writing a module myself seeing it was not available there, but checking here I saw you already have one. Would be great to see it!

remi-gelinas commented 2 weeks ago

Hey @remi-gelinas, are you planning to add your module to nix-darwin? I had thought on writing a module myself seeing it was not available there, but checking here I saw you already have one. Would be great to see it!

I was planning on it at some point, but I'm not sure if Aerospace itself is present in nixpkgs. I have a derivation written to pull the built release I could add to nixpkgs.

Sure, I can get the package itself and the module upstreamed

nephalemsec commented 1 week ago

Derivation

{ stdenv, fetchzip, installShellFiles }:

stdenv.mkDerivation rec {
  pname = "aerospace";
  version = "0.14.2-Beta";
  nativeBuildInputs = [ installShellFiles ];
  buildPhase = "";
  installPhase = ''
    mkdir -p $out/bin
    cp bin/aerospace $out/bin

    mkdir -p $out/Applications
    cp -r AeroSpace.app $out/Applications/AeroSpace.app

    installManPage manpage/*
  '';

  src = fetchzip {
    url =
      "https://github.com/nikitabobko/AeroSpace/releases/download/v${version}/AeroSpace-v${version}.zip";
    hash = "sha256-v2D/IV9Va0zbGHEwSGt6jvDqQYqha290Lm6u+nZTS3A=";
  };
}

Options:

{ config, lib, pkgs, osConfig, ... }:

with lib;
let
  inherit (pkgs) system;
  inherit (osConfig.flake.packages.${system}) aerospace;

  cfg = config.programs.aerospace;
  tomlFormat = pkgs.formats.toml { };
in {
  options = {
    programs.aerospace = {
      enable = mkEnableOption "an i3-like window manager for macOS";

      package = mkOption {
        type = types.package;
        default = aerospace;
        description = "The aerospace package to install.";
      };

      settings = mkOption {
        inherit (tomlFormat) type;
        default = { };
        description = "Configuration for aerospace.";
      };

      enableJankyBorders = mkOption {
        type = types.bool;
        default = true;
        description = "Enable the JankyBorders package alongside aerospace.";
      };

      jankybordersPackage = mkOption {
        type = types.package;
        default = pkgs.jankyborders;
        description = "The JankyBorders package to install.";
      };

      aerospaceAppPath = mkOption {
        type = types.str;
        default = "${cfg.package}/Applications/AeroSpace.app";
        description = "Path to the Aerospace.app bundle.";
      };

      runOnStartup = mkOption {
        type = types.bool;
        default = true;
        description = "Run AeroSpace.app on startup.";
      };
    };
  };

  config = mkIf cfg.enable {
    home.packages = [ cfg.package ]
      ++ lib.optional cfg.enableJankyBorders cfg.jankybordersPackage;

    xdg.configFile = {
      "aerospace/aerospace.toml" = mkIf (cfg.settings != { }) {
        source = tomlFormat.generate "config" cfg.settings;
      };
    };
  };
}

Example Config

{ pkgs, ... }: {
  imports = [ ./aerospace.nix ];
  programs.aerospace = {
    enable = true;

    settings = {
      # Normalizations. See: https://nikitabobko.github.io/AeroSpace/guide#normalization
      enable-normalization-flatten-containers = false;
      enable-normalization-opposite-orientation-for-nested-containers = false;
      after-startup-command = [
        "exec-and-forget ${pkgs.jankyborders}/bin/borders style=round width=10 active_color=0xffeba0ac"
      ];

      # Window detection. See: https://nikitabobko.github.io/AeroSpace/guide.html#on-window-detected-callback
      # Common app ids: https://nikitabobko.github.io/AeroSpace/goodness#popular-apps-ids
      # This is a workaround for apps that have no window decorations not being detected.
      on-window-detected = [
        {
          ${"if"}.app-id = "net.kovidgoyal.kitty";
          run = "layout tiling";
        }
        {
          ${"if"}.app-id = "com.brave.Browser";
          run = "layout tiling";
        }
      ];

      workspace-to-monitor-force-assignment = {
        "1" = [ "C34H89x" 2 3 ];
        "2" = [ "C34H89x" 2 3 ];
        "3" = [ "C34H89x" 2 3 ];
        "4" = [ "^built-in retina display$" ];
        "5" = [ "^built-in retina display$" ];
      };

      gaps = {
        inner = {
          horizontal = 10;
          vertical = 10;
        };
        outer = {
          left = 10;
          bottom = 10;
          top = 10;
          right = 10;
        };
      };

      # All possible keys:
      # - Letters.        a, b, c, ..., z
      # - Numbers.        0, 1, 2, ..., 9
      # - Keypad numbers. keypad0, keypad1, keypad2, ..., keypad9
      # - F-keys.         f1, f2, ..., f20
      # - Special keys.   minus, equal, period, comma, slash, backslash, quote, semicolon, backtick,
      #                   leftSquareBracket, rightSquareBracket, space, enter, esc, backspace, tab
      # - Keypad special. keypadClear, keypadDecimalMark, keypadDivide, keypadEnter, keypadEqual,
      #                   keypadMinus, keypadMultiply, keypadPlus
      # - Arrows.         left, down, up, right

      mode = {
        main.binding = {
          "alt-h" = [ ]; # Disable alt-h hiding windows
          "alt-f" =
            "flatten-workspace-tree"; # The oh-shit I fucked up, hit reset button
          "alt-q" = "close";
          "alt-slash" = "layout tiles horizontal vertical";

          # Focus active window in direction
          "alt-left" =
            "focus left --boundaries all-monitors-outer-frame --boundaries-action wrap-around-all-monitors";
          "alt-down" = "focus down";
          "alt-up" = "focus up";
          "alt-right" =
            "focus right --boundaries all-monitors-outer-frame --boundaries-action wrap-around-all-monitors";

          # move a window in direction
          "alt-shift-left" = "move left";
          "alt-shift-down" = "move down";
          "alt-shift-up" = "move up";
          "alt-shift-right" = "move right";

          # Move window to a monitor in general direction
          "alt-shift-ctrl-left" = "move-node-to-monitor left";
          "alt-shift-ctrl-down" = "move-node-to-monitor down";
          "alt-shift-ctrl-up" = "move-node-to-monitor up";
          "alt-shift-ctrl-right" = "move-node-to-monitor right";

          # Cycle through workspaces
          "alt-tab" = "workspace-back-and-forth";

          # Float a window
          "alt-shift-f" = "layout floating tiling"; # "floating toggle" in i3

          # Workspaces replace MacOS native Virtual Desktops
          "alt-1" = "workspace 1";
          "alt-2" = "workspace 2";
          "alt-3" = "workspace 3";
          "alt-4" = "workspace 4";
          "alt-5" = "workspace 5";
          "alt-6" = "workspace 6";
          "alt-7" = "workspace 7";
          "alt-8" = "workspace 8";
          "alt-9" = "workspace 9";
          "alt-0" = "workspace 10";

          "alt-shift-1" = "move-node-to-workspace 1";
          "alt-shift-2" = "move-node-to-workspace 2";
          "alt-shift-3" = "move-node-to-workspace 3";
          "alt-shift-4" = "move-node-to-workspace 4";
          "alt-shift-5" = "move-node-to-workspace 5";
          "alt-shift-6" = "move-node-to-workspace 6";
          "alt-shift-7" = "move-node-to-workspace 7";
          "alt-shift-8" = "move-node-to-workspace 8";
          "alt-shift-9" = "move-node-to-workspace 9";
          "alt-shift-0" = "move-node-to-workspace 10";

          "alt-r" = "mode resize";
        };

        resize.binding = {
          "left" = "resize width -50";
          "up" = "resize height +50";
          "down" = "resize height -50";
          "right" = "resize width +50";
          "enter" = "mode main";
          "esc" = "mode main";
        };
      };

    };
  };
}

Pick the meat off the bones all you like 😃

Only part I'm having issues with it getting the on-window-detected TOML conversion perfect.

on-window-detected = [
  {
    "if.app-id" = "net.kovidgoyal.kitty";
    run = "layout tiling";
  }
  {
    "if.app-id" = "com.brave.Browser";
    run = "layout tiling";
  }
];

Above should work in theory, but doesn't seem to like the quotes in generated TOML.

remi-gelinas commented 1 week ago

@nephalemsec "if.app-id" isn't the structure it's expecting - try "if".app-id.

nephalemsec commented 1 week ago

@nephalemsec "if.app-id" isn't the structure it's expecting - try "if".app-id.

Yeah have tried that too. Nothing seems to work to get brave tiling properly for me.

This is the result of using "if".app-id: image