Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

modules/darwin/yabai/default.nix

3c1f45ed7ea27458db27a2af96ef6ec1392dbc15 · 9.3 KB · 296 lines raw

1 # modules/darwin/yabai/default.nix
2 { config, lib, pkgs, ... }:
3
4 let
5 cfg = config.local.yabai;
6
7 spaceFocusBinds = lib.concatStringsSep "\n"
8 (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}")
9 (lib.range 1 cfg.spaceHotkeys.count));
10
11 # symbolic hotkey IDs 118..126 == "Switch to Desktop 1".."Switch to Desktop 9"
12 spaceKeycodes = [ 18 19 20 21 22 23 25 26 28 ];
13
14 mkSpaceHotkey = i: {
15 name = toString (117 + i);
16 value = {
17 enabled = true;
18 value = {
19 type = "standard";
20 parameters = [
21 (48 + i) # ASCII '1'..'9'
22 (builtins.elemAt spaceKeycodes (i - 1)) # virtual keycode
23 cfg.spaceHotkeys.modifierMask
24 ];
25 };
26 };
27 };
28
29 spaceHotkeys = builtins.listToAttrs
30 (map mkSpaceHotkey (lib.range 1 cfg.spaceHotkeys.count));
31
32 padArms = lib.concatStringsSep "\n" (lib.mapAttrsToList
33 (uuid: pad: " ${uuid}) pad=${toString pad} ;;")
34 cfg.displayTopPadding);
35
36 paddingSignals = ''
37 yabai -m signal --add event=display_added action="${displayPaddingScript}" label="per-display padding (added)"
38 yabai -m signal --add event=display_removed action="${displayPaddingScript}" label="per-display padding (removed)"
39 yabai -m signal --add event=space_created action="${displayPaddingScript}" label="per-display padding (space)"
40 ${displayPaddingScript}
41 '';
42
43
44 displayPaddingScript = pkgs.writeShellScript "yabai-display-padding" ''
45 set -eu
46 yabai="${cfg.package}/bin/yabai"
47
48 "$yabai" -m query --displays \
49 | ${lib.getExe pkgs.jq} -r '.[] | "\(.uuid) \(.spaces | map(tostring) | join(" "))"' \
50 | while read -r uuid spaces; do
51 case "$uuid" in
52 ${padArms}
53 *) pad=${toString cfg.defaultTopPadding} ;;
54 esac
55 for s in $spaces; do
56 for k in top_padding; do
57 "$yabai" -m config --space "$s" "$k" "$pad" || true
58 done
59 done
60 done
61 '';
62
63 yabaiBin = "${cfg.package}/bin/yabai";
64
65 sudoersFile = pkgs.runCommand "sudoers-yabai" { } ''
66 YABAI_BIN="${yabaiBin}"
67 SHASUM=$(sha256sum "$YABAI_BIN" | cut -d' ' -f1)
68 cat > "$out" <<EOF
69 ${cfg.user} ALL=(root) NOPASSWD: sha256:$SHASUM $YABAI_BIN --load-sa
70 EOF
71 '';
72
73 dockRestartSignal = ''
74 yabai -m signal --add event=dock_did_restart action="/usr/bin/sudo ${yabaiBin} --load-sa" label="reload scripting addition"
75 '';
76
77 directionBinds = lib.concatStringsSep "\n" (map (d: ''
78 alt - ${d.key} : ${yabaiBin} -m window --focus ${d.dir} || ${yabaiBin} -m display --focus ${d.dir}
79 shift + alt - ${d.key} : ${yabaiBin} -m window --swap ${d.dir} || ${yabaiBin} -m window --display ${d.dir}
80 '') [
81 { key = "up"; dir = "north"; }
82 { key = "down"; dir = "south"; }
83 { key = "right"; dir = "east"; }
84 { key = "left"; dir = "west"; }
85 ]);
86
87 spaceMoveBinds = lib.concatStringsSep "\n"
88 (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}")
89 (lib.range 1 cfg.spaceHotkeys.count));
90
91 in {
92 options.local.yabai = {
93 enable = lib.mkEnableOption "yabai tiling window manager setup";
94
95 package = lib.mkOption {
96 type = lib.types.package;
97 default = pkgs.yabai;
98 description = "yabai package. Referenced by keybindings and the padding script.";
99 };
100
101 user = lib.mkOption {
102 type = lib.types.str;
103 default = config.system.primaryUser;
104 defaultText = lib.literalExpression "config.system.primaryUser";
105 description = ''
106 User that is granted permission to run --load-sa
107 '';
108 };
109
110 enableScriptingAddition = lib.mkOption {
111 type = lib.types.bool;
112 default = false;
113 description = ''
114 Only gates space creation/destruction and moving windows between
115 spaces. Window geometry and padding work without it.
116 '';
117 };
118
119 useYabaiSpaceFocus = lib.mkOption {
120 type = lib.types.bool;
121 default = false;
122 description = ''
123 Bind alt+n to yabai native control rather than skhd. Requires scripting
124 additions.
125 '';
126 };
127
128 defaultPadding = lib.mkOption {
129 type = lib.types.int;
130 default = 10;
131 description = ''
132 Padding and window_gap for any display not listed in displayTopPadding.
133 Sized for Retina (2x) panels.
134 '';
135 };
136
137 defaultTopPadding = lib.mkOption {
138 type = lib.types.int;
139 default = 40;
140 description = ''
141 Padding for top edge of screen
142 '';
143 };
144
145 displayTopPadding = lib.mkOption {
146 type = lib.types.attrsOf lib.types.int;
147 default = { };
148 example = lib.literalExpression ''
149 { "37D8832A-2D66-02CA-B9F7-8F30A301B230" = 40; }
150 '';
151 description = ''
152 Display UUID -> padding value. The value is applied to all four
153 paddings and to window_gap for every space on that display.
154
155 Get UUIDs with:
156 yabai -m query --displays | jq -r '.[] | "\(.uuid) \(.frame)"'
157
158 Do not key off frame dimensions: yabai reports points, not pixels,
159 so a scaled 1080p panel will not report 1920x1080.
160 '';
161 };
162
163 manageKeybindings = lib.mkOption {
164 type = lib.types.bool;
165 default = true;
166 description = "Emit yabai skhd bindings into services.skhd.skhdConfig.";
167 };
168
169 spaceHotkeys = {
170 enable = lib.mkEnableOption "opt+<n> macOS space-switching hotkeys" // {
171 default = true;
172 };
173
174 count = lib.mkOption {
175 type = lib.types.ints.between 1 9;
176 default = 9;
177 description = "Number of spaces to bind. macOS exposes 9 symbolic hotkeys.";
178 };
179
180 modifierMask = lib.mkOption {
181 type = lib.types.int;
182 default = 524288; # option
183 description = "Carbon modifier mask. 524288 = option, 1048576 = command.";
184 };
185 };
186
187 extraSymbolicHotkeys = lib.mkOption {
188 type = lib.types.attrs;
189 default = { };
190 description = ''
191 Merged into com.apple.symbolichotkeys alongside the space hotkeys.
192 Put non-yabai symbolic hotkeys here rather than defining
193 CustomUserPreferences."com.apple.symbolichotkeys" in the host config --
194 CustomUserPreferences is types.attrs and merges shallowly, so a second
195 definition of that domain silently clobbers this one.
196 '';
197 };
198
199 extraSettings = lib.mkOption {
200 type = lib.types.attrs;
201 default = { };
202 description = "Extra keys merged into services.yabai.config.";
203 };
204
205 extraConfig = lib.mkOption {
206 type = lib.types.lines;
207 default = "";
208 description = "Extra shell appended to yabairc (rules, signals).";
209 };
210 };
211
212 config = lib.mkIf cfg.enable {
213 assertions = [
214 {
215 assertion = cfg.user != null && cfg.user != "";
216 message = ''
217 local.yabai.user is unset. Set it or system.primaryUser
218 '';
219 }
220 {
221 assertion = cfg.useYabaiSpaceFocus -> cfg.enableScriptingAssition;
222 message = ''
223 local.yabai.useYabaiSpaceFocus requires enableScriptingAddition.
224 '';
225 }
226 ];
227
228 services.yabai = {
229 enable = true;
230 inherit (cfg) package enableScriptingAddition;
231
232 config = {
233 mouse_follows_focus = "on";
234 layout = "bsp";
235 window_placement = "second_child";
236 top_padding = toString cfg.defaultTopPadding;
237 bottom_padding = toString cfg.defaultPadding;
238 left_padding = toString cfg.defaultPadding;
239 right_padding = toString cfg.defaultPadding;
240 window_gap = toString cfg.defaultPadding;
241 mouse_modifier = "alt";
242 mouse_drop_action = "swap";
243 mouse_action1 = "move";
244 mouse_action2 = "resize";
245 focus_follows_mouse = "autofocus";
246 } // cfg.extraSettings;
247
248 extraConfig = lib.concatStringsSep "\n" (
249 [
250 ''
251 yabai -m rule --add app='System Settings' manage=off
252 yabai -m rule --add app="^Zen$" title="^Picture-in-Picture$" manage=off
253 ''
254 ]
255 ++ lib.optional cfg.enableScriptingAddition dockRestartSignal
256 ++ lib.optional (cfg.displayTopPadding != { }) paddingSignals
257 ++ lib.optional (cfg.extraConfig != "") cfg.extraConfig
258 );
259 };
260
261 environment.etc."sudoers.d/yabai".source =
262 lib.mkIf cfg.enableScriptingAddition (lib.mkForce sudoersFile);
263
264 services.skhd = lib.mkIf cfg.manageKeybindings {
265 enable = true;
266 skhdConfig = lib.concatStringsSep "\n" (
267 [
268 ''
269 shift + alt - q : ${yabaiBin} -m window --close
270
271 ${directionBinds}
272
273 shift + alt - space : ${yabaiBin} -m window --toggle float
274 shift + alt - return : ${yabaiBin} -m window --toggle sticky
275 alt - f : ${yabaiBin} -m window --toggle zoom-fullscreen
276 shift + alt - f : ${yabaiBin} -m window --toggle native-fullscreen
277
278 ${spaceMoveBinds}
279 ''
280 ]
281 ++ lib.optional cfg.useYabaiSpaceFocus spaceFocusBinds
282 );
283 };
284
285 # Prerequisites, not preferences: yabai requires per-display spaces, and
286 # macOS's own tiling fights it for window placement.
287 system.defaults = {
288 spaces."spans-displays" = false;
289 WindowManager.EnableTilingOptionAccelerator = false;
290
291 CustomUserPreferences."com.apple.symbolichotkeys".AppleSymbolicHotKeys =
292 cfg.extraSymbolicHotkeys
293 // (lib.optionalAttrs cfg.spaceHotkeys.enable spaceHotkeys);
294 };
295 };
296 }