modules/darwin/yabai/default.nix
a2c015245921f589bbd7cc787bfd092474a07992
· 7.7 KB · 237 lines
raw
| 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.defaultTopPadding} ;; |
| 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 | } |