From 7ef533e8d1fb6ece563232b326fc5b5d5c2f9257 Mon Sep 17 00:00:00 2001
From: cmacrae <hi@cmacr.ae>
Date: Wed, 6 May 2020 17:21:46 +0100
Subject: [PATCH] module: add yabai service

---
 modules/module-list.nix            |   1 +
 modules/services/yabai/default.nix | 103 +++++++++++++++++++++++++++++
 release.nix                        |   1 +
 tests/services-yabai.nix           |  27 ++++++++
 4 files changed, 132 insertions(+)
 create mode 100644 modules/services/yabai/default.nix
 create mode 100644 tests/services-yabai.nix

diff --git a/modules/module-list.nix b/modules/module-list.nix
index ae5cf699..bbe8e678 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -53,6 +53,7 @@
   ./services/skhd
   ./services/synapse-bt.nix
   ./services/synergy
+  ./services/yabai
   ./programs/bash
   ./programs/fish.nix
   ./programs/gnupg.nix
diff --git a/modules/services/yabai/default.nix b/modules/services/yabai/default.nix
new file mode 100644
index 00000000..9318ff9e
--- /dev/null
+++ b/modules/services/yabai/default.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.yabai;
+
+  toYabaiConfig = opts:
+    concatStringsSep "\n" (mapAttrsToList
+      (p: v: "yabai -m config ${p} ${toString v}") opts);
+
+  configFile = mkIf (cfg.config != {} || cfg.extraConfig != "")
+    "${pkgs.writeScript "yabairc" (
+      (if (cfg.config != {})
+       then "${toYabaiConfig cfg.config}"
+       else "")
+      + optionalString (cfg.extraConfig != "") cfg.extraConfig)}";
+in
+
+{
+  options = with types; {
+    services.yabai.enable = mkOption {
+      type = bool;
+      default = false;
+      description = "Whether to enable the yabai window manager.";
+    };
+
+    services.yabai.package = mkOption {
+      type = path;
+      description = "The yabai package to use.";
+    };
+
+    services.yabai.enableScriptingAddition = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether to enable yabai's scripting-addition.
+        SIP must be disabled for this to work.
+      '';
+    };
+
+    services.yabai.config = mkOption {
+      type = attrs;
+      default = {};
+      example = literalExample ''
+        {
+          focus_follows_mouse = "autoraise";
+          mouse_follows_focus = "off";
+          window_placement    = "second_child";
+          window_opacity      = "off";
+          top_padding         = 36;
+          bottom_padding      = 10;
+          left_padding        = 10;
+          right_padding       = 10;
+          window_gap          = 10;
+        }
+      '';
+      description = ''
+        Key/Value pairs to pass to yabai's 'config' domain, via the configuration file.
+      '';
+    };
+
+    services.yabai.extraConfig = mkOption {
+      type = str;
+      default = "";
+      example = literalExample ''
+        yabai -m rule --add app='System Preferences' manage=off
+      '';
+      description = "Extra arbitrary configuration to append to the configuration file";
+    };
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable) {
+      environment.systemPackages = [ cfg.package ];
+
+      launchd.user.agents.yabai = {
+        serviceConfig.ProgramArguments = [ "${cfg.package}/bin/yabai" ]
+                                         ++ optionals (cfg.config != {} || cfg.extraConfig != "") [ "-c" configFile ];
+
+        serviceConfig.KeepAlive = true;
+        serviceConfig.RunAtLoad = true;
+        serviceConfig.EnvironmentVariables = {
+          PATH = "${cfg.package}/bin:${config.environment.systemPath}";
+        };
+      };
+    })
+
+    # TODO: [@cmacrae] Handle removal of yabai scripting additions
+    (mkIf (cfg.enableScriptingAddition) {
+      launchd.daemons.yabai-sa = {
+        script = ''
+          if [ ! $(${cfg.package}/bin/yabai --check-sa) ]; then
+            ${cfg.package}/bin/yabai --install-sa
+          fi
+        '';
+
+        serviceConfig.RunAtLoad = true;
+        serviceConfig.KeepAlive.SuccessfulExit = false;
+      };
+    })
+  ];
+}
diff --git a/release.nix b/release.nix
index 9ffa6c37..4f006ac8 100644
--- a/release.nix
+++ b/release.nix
@@ -123,6 +123,7 @@ let
     tests.services-skhd = makeTest ./tests/services-skhd.nix;
     tests.services-synapse-bt = makeTest ./tests/services-synapse-bt.nix;
     tests.services-synergy = makeTest ./tests/services-synergy.nix;
+    tests.services-yabai = makeTest ./tests/services-yabai.nix;
     tests.system-defaults-write = makeTest ./tests/system-defaults-write.nix;
     tests.system-environment = makeTest ./tests/system-environment.nix;
     tests.system-keyboard-mapping = makeTest ./tests/system-keyboard-mapping.nix;
diff --git a/tests/services-yabai.nix b/tests/services-yabai.nix
new file mode 100644
index 00000000..876ba387
--- /dev/null
+++ b/tests/services-yabai.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  yabai = pkgs.runCommand "yabai-0.0.0" {} "mkdir $out";
+in
+
+{
+  services.yabai.enable = true;
+  services.yabai.package = yabai;
+  services.yabai.config = { focus_follows_mouse = "autoraise"; };
+  services.yabai.extraConfig = "yabai -m rule --add app='System Preferences' manage=off";
+
+  test = ''
+    echo >&2 "checking yabai service in ~/Library/LaunchAgents"
+    grep "org.nixos.yabai" ${config.out}/user/Library/LaunchAgents/org.nixos.yabai.plist
+    grep "${yabai}/bin/yabai" ${config.out}/user/Library/LaunchAgents/org.nixos.yabai.plist
+
+    conf=`sed -En '/<string>-c<\/string>/{n; s/\s+?<\/?string>//g; p;}' \
+      ${config.out}/user/Library/LaunchAgents/org.nixos.yabai.plist`
+
+    echo >&2 "checking config in $conf"
+    grep "yabai -m config focus_follows_mouse autoraise" $conf
+    grep "yabai -m rule --add app='System Preferences' manage=off" $conf
+  '';
+}