Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
modules/darwin/yabai/default.nix
aeb9107982a669bb3d2f270a6d8e78eb021f6501
· 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 | } |