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 }