diff options
Diffstat (limited to 'modules/darwin/yabai/default.nix')
| -rw-r--r-- | modules/darwin/yabai/default.nix | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/modules/darwin/yabai/default.nix b/modules/darwin/yabai/default.nix new file mode 100644 index 0000000..8e64dd1 --- /dev/null +++ b/modules/darwin/yabai/default.nix | |||
| @@ -0,0 +1,237 @@ | |||
| 1 | # modules/darwin/yabai/default.nix | ||
| 2 | { config, lib, pkgs, ... }: | ||
| 3 | |||
| 4 | let | ||
| 5 | cfg = config.local.yabai; | ||
| 6 | |||
| 7 | # symbolic hotkey IDs 118..126 == "Switch to Desktop 1".."Switch to Desktop 9" | ||
| 8 | spaceKeycodes = [ 18 19 20 21 22 23 25 26 28 ]; | ||
| 9 | |||
| 10 | mkSpaceHotkey = i: { | ||
| 11 | name = toString (117 + i); | ||
| 12 | value = { | ||
| 13 | enabled = true; | ||
| 14 | value = { | ||
| 15 | type = "standard"; | ||
| 16 | parameters = [ | ||
| 17 | (48 + i) # ASCII '1'..'9' | ||
| 18 | (builtins.elemAt spaceKeycodes (i - 1)) # virtual keycode | ||
| 19 | cfg.spaceHotkeys.modifierMask | ||
| 20 | ]; | ||
| 21 | }; | ||
| 22 | }; | ||
| 23 | }; | ||
| 24 | |||
| 25 | spaceHotkeys = builtins.listToAttrs | ||
| 26 | (map mkSpaceHotkey (lib.range 1 cfg.spaceHotkeys.count)); | ||
| 27 | |||
| 28 | padArms = lib.concatStringsSep "\n" (lib.mapAttrsToList | ||
| 29 | (uuid: pad: " ${uuid}) pad=${toString pad} ;;") | ||
| 30 | cfg.displayTopPadding); | ||
| 31 | |||
| 32 | displayPaddingScript = pkgs.writeShellScript "yabai-display-padding" '' | ||
| 33 | set -eu | ||
| 34 | yabai="${cfg.package}/bin/yabai" | ||
| 35 | |||
| 36 | "$yabai" -m query --displays \ | ||
| 37 | | ${lib.getExe pkgs.jq} -r '.[] | "\(.uuid) \(.spaces | map(tostring) | join(" "))"' \ | ||
| 38 | | while read -r uuid spaces; do | ||
| 39 | case "$uuid" in | ||
| 40 | ${padArms} | ||
| 41 | *) pad=${toString cfg.defaultPadding} ;; | ||
| 42 | esac | ||
| 43 | for s in $spaces; do | ||
| 44 | for k in top_padding; do | ||
| 45 | "$yabai" -m config --space "$s" "$k" "$pad" || true | ||
| 46 | done | ||
| 47 | done | ||
| 48 | done | ||
| 49 | ''; | ||
| 50 | |||
| 51 | yabaiBin = "${cfg.package}/bin/yabai"; | ||
| 52 | |||
| 53 | directionBinds = lib.concatStringsSep "\n" (map (d: '' | ||
| 54 | alt - ${d.key} : ${yabaiBin} -m window --focus ${d.dir} || ${yabaiBin} -m display --focus ${d.dir} | ||
| 55 | shift + alt - ${d.key} : ${yabaiBin} -m window --swap ${d.dir} || ${yabaiBin} -m window --display ${d.dir} | ||
| 56 | '') [ | ||
| 57 | { key = "up"; dir = "north"; } | ||
| 58 | { key = "down"; dir = "south"; } | ||
| 59 | { key = "right"; dir = "east"; } | ||
| 60 | { key = "left"; dir = "west"; } | ||
| 61 | ]); | ||
| 62 | |||
| 63 | spaceMoveBinds = lib.concatStringsSep "\n" | ||
| 64 | (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}") | ||
| 65 | (lib.range 1 cfg.spaceHotkeys.count)); | ||
| 66 | |||
| 67 | in { | ||
| 68 | options.local.yabai = { | ||
| 69 | enable = lib.mkEnableOption "yabai tiling window manager setup"; | ||
| 70 | |||
| 71 | package = lib.mkOption { | ||
| 72 | type = lib.types.package; | ||
| 73 | default = pkgs.yabai; | ||
| 74 | description = "yabai package. Referenced by keybindings and the padding script."; | ||
| 75 | }; | ||
| 76 | |||
| 77 | enableScriptingAddition = lib.mkOption { | ||
| 78 | type = lib.types.bool; | ||
| 79 | default = false; | ||
| 80 | description = '' | ||
| 81 | Only gates space creation/destruction and moving windows between | ||
| 82 | spaces. Window geometry and padding work without it. | ||
| 83 | ''; | ||
| 84 | }; | ||
| 85 | |||
| 86 | defaultPadding = lib.mkOption { | ||
| 87 | type = lib.types.int; | ||
| 88 | default = 10; | ||
| 89 | description = '' | ||
| 90 | Padding and window_gap for any display not listed in displayTopPadding. | ||
| 91 | Sized for Retina (2x) panels. | ||
| 92 | ''; | ||
| 93 | }; | ||
| 94 | |||
| 95 | defaultTopPadding = lib.mkOption { | ||
| 96 | type = lib.types.int; | ||
| 97 | default = 10; | ||
| 98 | description = '' | ||
| 99 | Padding for top edge of screen | ||
| 100 | ''; | ||
| 101 | }; | ||
| 102 | |||
| 103 | displayTopPadding = lib.mkOption { | ||
| 104 | type = lib.types.attrsOf lib.types.int; | ||
| 105 | default = { }; | ||
| 106 | example = lib.literalExpression '' | ||
| 107 | { "37D8832A-2D66-02CA-B9F7-8F30A301B230" = 40; } | ||
| 108 | ''; | ||
| 109 | description = '' | ||
| 110 | Display UUID -> padding value. The value is applied to all four | ||
| 111 | paddings and to window_gap for every space on that display. | ||
| 112 | |||
| 113 | Get UUIDs with: | ||
| 114 | yabai -m query --displays | jq -r '.[] | "\(.uuid) \(.frame)"' | ||
| 115 | |||
| 116 | Do not key off frame dimensions: yabai reports points, not pixels, | ||
| 117 | so a scaled 1080p panel will not report 1920x1080. | ||
| 118 | ''; | ||
| 119 | }; | ||
| 120 | |||
| 121 | manageKeybindings = lib.mkOption { | ||
| 122 | type = lib.types.bool; | ||
| 123 | default = true; | ||
| 124 | description = "Emit yabai skhd bindings into services.skhd.skhdConfig."; | ||
| 125 | }; | ||
| 126 | |||
| 127 | spaceHotkeys = { | ||
| 128 | enable = lib.mkEnableOption "opt+<n> macOS space-switching hotkeys" // { | ||
| 129 | default = true; | ||
| 130 | }; | ||
| 131 | |||
| 132 | count = lib.mkOption { | ||
| 133 | type = lib.types.ints.between 1 9; | ||
| 134 | default = 9; | ||
| 135 | description = "Number of spaces to bind. macOS exposes 9 symbolic hotkeys."; | ||
| 136 | }; | ||
| 137 | |||
| 138 | modifierMask = lib.mkOption { | ||
| 139 | type = lib.types.int; | ||
| 140 | default = 524288; # option | ||
| 141 | description = "Carbon modifier mask. 524288 = option, 1048576 = command."; | ||
| 142 | }; | ||
| 143 | }; | ||
| 144 | |||
| 145 | extraSymbolicHotkeys = lib.mkOption { | ||
| 146 | type = lib.types.attrs; | ||
| 147 | default = { }; | ||
| 148 | description = '' | ||
| 149 | Merged into com.apple.symbolichotkeys alongside the space hotkeys. | ||
| 150 | Put non-yabai symbolic hotkeys here rather than defining | ||
| 151 | CustomUserPreferences."com.apple.symbolichotkeys" in the host config -- | ||
| 152 | CustomUserPreferences is types.attrs and merges shallowly, so a second | ||
| 153 | definition of that domain silently clobbers this one. | ||
| 154 | ''; | ||
| 155 | }; | ||
| 156 | |||
| 157 | extraSettings = lib.mkOption { | ||
| 158 | type = lib.types.attrs; | ||
| 159 | default = { }; | ||
| 160 | description = "Extra keys merged into services.yabai.config."; | ||
| 161 | }; | ||
| 162 | |||
| 163 | extraConfig = lib.mkOption { | ||
| 164 | type = lib.types.lines; | ||
| 165 | default = ""; | ||
| 166 | description = "Extra shell appended to yabairc (rules, signals)."; | ||
| 167 | }; | ||
| 168 | }; | ||
| 169 | |||
| 170 | config = lib.mkIf cfg.enable { | ||
| 171 | services.yabai = { | ||
| 172 | enable = true; | ||
| 173 | inherit (cfg) package enableScriptingAddition; | ||
| 174 | |||
| 175 | config = { | ||
| 176 | mouse_follows_focus = "on"; | ||
| 177 | layout = "bsp"; | ||
| 178 | window_placement = "second_child"; | ||
| 179 | top_padding = toString cfg.defaultTopPadding; | ||
| 180 | bottom_padding = toString cfg.defaultPadding; | ||
| 181 | left_padding = toString cfg.defaultPadding; | ||
| 182 | right_padding = toString cfg.defaultPadding; | ||
| 183 | window_gap = toString cfg.defaultPadding; | ||
| 184 | mouse_modifier = "alt"; | ||
| 185 | mouse_drop_action = "swap"; | ||
| 186 | mouse_action1 = "move"; | ||
| 187 | mouse_action2 = "resize"; | ||
| 188 | focus_follows_mouse = "autofocus"; | ||
| 189 | } // cfg.extraSettings; | ||
| 190 | |||
| 191 | extraConfig = '' | ||
| 192 | yabai -m rule --add app='System Settings' manage=off | ||
| 193 | yabai -m rule --add app="^Zen$" title="^Picture-in-Picture$" manage=off | ||
| 194 | '' | ||
| 195 | + lib.optionalString (cfg.displayTopPadding != { }) '' | ||
| 196 | |||
| 197 | # Per-display padding. space_created is required, not redundant: | ||
| 198 | # display_added fires when yabai sees the display, which can precede | ||
| 199 | # macOS finishing space creation on it, so the first pass may find | ||
| 200 | # zero or one space. display_removed is required because yabai | ||
| 201 | # reindexes spaces on disconnect and the overrides are keyed by index. | ||
| 202 | yabai -m signal --add event=display_added action="${displayPaddingScript}" label="per-display padding (added)" | ||
| 203 | yabai -m signal --add event=display_removed action="${displayPaddingScript}" label="per-display padding (removed)" | ||
| 204 | yabai -m signal --add event=space_created action="${displayPaddingScript}" label="per-display padding (space)" | ||
| 205 | ${displayPaddingScript} | ||
| 206 | '' | ||
| 207 | + cfg.extraConfig; | ||
| 208 | }; | ||
| 209 | |||
| 210 | services.skhd = lib.mkIf cfg.manageKeybindings { | ||
| 211 | enable = true; | ||
| 212 | skhdConfig = '' | ||
| 213 | shift + alt - q : ${yabaiBin} -m window --close | ||
| 214 | |||
| 215 | ${directionBinds} | ||
| 216 | |||
| 217 | shift + alt - space : ${yabaiBin} -m window --toggle float | ||
| 218 | shift + alt - return : ${yabaiBin} -m window --toggle sticky | ||
| 219 | alt - f : ${yabaiBin} -m window --toggle zoom-fullscreen | ||
| 220 | shift + alt - f : ${yabaiBin} -m window --toggle native-fullscreen | ||
| 221 | |||
| 222 | ${spaceMoveBinds} | ||
| 223 | ''; | ||
| 224 | }; | ||
| 225 | |||
| 226 | # Prerequisites, not preferences: yabai requires per-display spaces, and | ||
| 227 | # macOS's own tiling fights it for window placement. | ||
| 228 | system.defaults = { | ||
| 229 | spaces."spans-displays" = false; | ||
| 230 | WindowManager.EnableTilingOptionAccelerator = false; | ||
| 231 | |||
| 232 | CustomUserPreferences."com.apple.symbolichotkeys".AppleSymbolicHotKeys = | ||
| 233 | cfg.extraSymbolicHotkeys | ||
| 234 | // (lib.optionalAttrs cfg.spaceHotkeys.enable spaceHotkeys); | ||
| 235 | }; | ||
| 236 | }; | ||
| 237 | } | ||
