So you want i3wm on MacOS?

I recently got a Macbook from my new employer and had to leave my Linux setup behind. 2021 was the first time in my life I owned an Apple product. MacOS is fine, but switching my workflow was painful.

I’d been using i3 under Linux for years and couldn’t imagine working without it. A colleague (thanks Marcel aka @snowiow!) had gone through the same thing and pointed me in the right direction. The result covers about 90% of what I loved about i3.

yabai + skhd (i3 replacement)

yabai is a tiling window manager for macOS. It extends the built-in window manager and lets you control windows, spaces, and displays from the command line or via keyboard shortcuts.

Install it with brew:

brew install koekeishiya/formulae/yabai

Start the service and grant the accessibility permissions it asks for:

brew services start yabai

yabai reads its config from ~/.yabairc:

touch ~/.yabairc

Here’s mine:

#!/usr/bin/env sh

# the scripting-addition must be loaded manually if
# you are running yabai on macOS Big Sur. Uncomment
# the following line to have the injection performed
# when the config is executed during startup.
#
# for this to work you must configure sudo such that
# it will be able to run the command without password
#
# see this wiki page for information:
#  - https://github.com/koekeishiya/yabai/wiki/Installing-yabai-(latest-release)
#
# sudo yabai --load-sa
# yabai -m signal --add event=dock_did_restart action="sudo yabai --load-sa"

#!/usr/bin/env sh

# bar settings
yabai -m config top_padding 0

# global settings
yabai -m config mouse_follows_focus          off
yabai -m config focus_follows_mouse          autofocus

yabai -m config window_placement             second_child
yabai -m config window_topmost               off

yabai -m config window_opacity               off
yabai -m config window_opacity_duration      0.0
yabai -m config window_shadow                on

yabai -m config active_window_opacity        1.0
yabai -m config normal_window_opacity        0.90
yabai -m config split_ratio                  0.50
yabai -m config auto_balance                 off

# Mouse support
yabai -m config mouse_modifier               alt
yabai -m config mouse_action1                move
yabai -m config mouse_action2                resize

# general space settings
yabai -m config layout                       bsp
yabai -m config bottom_padding               0
yabai -m config left_padding                 0
yabai -m config right_padding                0
yabai -m config window_gap                   0

# float system preferences
yabai -m rule --add app='^System Information$' manage=off
yabai -m rule --add app='^System Preferences$' manage=off
yabai -m rule --add title='Preferences$' manage=off

# float settings windows
yabai -m rule --add title='Settings$' manage=off

# Some Goland settings, in case you are using it. float Goland Preference panes
yabai -m rule --add app='Goland IDEA' title='^$' manage=off
yabai -m rule --add app='Goland IDEA' title='Project Structure' manage=off
yabai -m rule --add app='Goland IDEA' title='Preferences' manage=off
yabai -m rule --add app='Goland IDEA' title='Edit configuration' manage=off

echo "yabai configuration loaded.."

A few things worth pointing out:

Focus follows mouse — hover over a window and it gets focus, no clicking:

yabai -m config mouse_follows_focus          off
yabai -m config focus_follows_mouse          autofocus

Zero gaps between windows. I want every pixel. You can bump these up if you prefer some breathing room:

yabai -m config layout                       bsp
yabai -m config bottom_padding               0
yabai -m config left_padding                 0
yabai -m config right_padding                0
yabai -m config window_gap                   0

Some apps don’t play well with tiling, so I float them. Remove the Goland rules if you don’t use it:

yabai -m rule --add app='^System Information$' manage=off
yabai -m rule --add app='^System Preferences$' manage=off
yabai -m rule --add title='Preferences$' manage=off
yabai -m rule --add title='Settings$' manage=off

yabai -m rule --add app='Goland IDEA' title='^$' manage=off
yabai -m rule --add app='Goland IDEA' title='Project Structure' manage=off
yabai -m rule --add app='Goland IDEA' title='Preferences' manage=off
yabai -m rule --add app='Goland IDEA' title='Edit configuration' manage=off

Make the config executable:

chmod +x ~/.yabairc

Optional: disable window animations. There’s still a short animation when new windows appear, but you can kill most of the others:

# Disable animations when opening and closing windows.
defaults write NSGlobalDomain NSAutomaticWindowAnimationsEnabled -bool false

# Accelerated playback when adjusting the window size.
defaults write NSGlobalDomain NSWindowResizeTime -float 0.001

More options in this StackExchange thread.

Restart yabai to pick up the config:

brew services restart yabai

For keybindings, pair yabai with skhd — a hotkey daemon for macOS. It hotloads its config, so you can edit bindings without restarting.

brew install koekeishiya/formulae/skhd
brew services start skhd

Grant accessibility permissions when prompted, then create the config:

touch ~/.skhdrc

Here are the bindings I use. They’re close to my old i3 setup.

Navigate between windows with arrow keys or h,j,k,l:

# change focus
alt - h : yabai -m window --focus west
alt - j : yabai -m window --focus south
alt - k : yabai -m window --focus north
alt - l : yabai -m window --focus east
# (alt) change focus (using arrow keys)
alt - left  : yabai -m window --focus west
alt - down  : yabai -m window --focus south
alt - up    : yabai -m window --focus north
alt - right : yabai -m window --focus east

Move windows around with alt + shift. If a window hits the screen edge, it moves to the next monitor:

# shift window in current workspace
alt + shift - h : yabai -m window --swap west || $(yabai -m window --display west; yabai -m display --focus west)
alt + shift - j : yabai -m window --swap south || $(yabai -m window --display south; yabai -m display --focus south)
alt + shift - k : yabai -m window --swap north || $(yabai -m window --display north; yabai -m display --focus north)
alt + shift - l : yabai -m window --swap east || $(yabai -m window --display east; yabai -m display --focus east)
# alternatively, use the arrow keys
alt + shift - left : yabai -m window --swap west || $(yabai -m window --display west; yabai -m display --focus west)
alt + shift - down : yabai -m window --swap south || $(yabai -m window --display south; yabai -m display --focus south)
alt + shift - up : yabai -m window --swap north || $(yabai -m window --display north; yabai -m display --focus north)
alt + shift - right : yabai -m window --swap east || $(yabai -m window --display east; yabai -m display --focus east)

Set where the next window should appear relative to the focused one with alt + ctrl:

# set insertion point in focused container
alt + ctrl - h : yabai -m window --insert west
alt + ctrl - j : yabai -m window --insert south
alt + ctrl - k : yabai -m window --insert north
alt + ctrl - l : yabai -m window --insert east
# (alt) set insertion point in focused container using arrows
alt + ctrl - left  : yabai -m window --insert west
alt + ctrl - down  : yabai -m window --insert south
alt + ctrl - up    : yabai -m window --insert north
alt + ctrl - right : yabai -m window --insert east

Quick workspace switching with alt + b (like back_and_forth in i3):

# go back to previous workspace (kind of like back_and_forth in i3)
alt - b : yabai -m space --focus recent

# move focused window to previous workspace
alt + shift - b : yabai -m window --space recent; \
                  yabai -m space --focus recent

Move a window to workspace 1-9 with alt + shift + n:

# move focused window to next/prev workspace
alt + shift - 1 : yabai -m window --space 1
alt + shift - 2 : yabai -m window --space 2
alt + shift - 3 : yabai -m window --space 3
alt + shift - 4 : yabai -m window --space 4
alt + shift - 5 : yabai -m window --space 5
alt + shift - 6 : yabai -m window --space 6
alt + shift - 7 : yabai -m window --space 7
alt + shift - 8 : yabai -m window --space 8
alt + shift - 9 : yabai -m window --space 9
#alt + shift - 0 : yabai -m window --space 10

Mirror and rebalance the layout:

# # mirror tree y-axis
alt + shift - y : yabai -m space --mirror y-axis

# # mirror tree x-axis
alt + shift - x : yabai -m space --mirror x-axis

# balance size of windows
alt + shift - 0 : yabai -m space --balance

Switch between tiling modes:

# change layout of desktop
alt - e : yabai -m space --layout bsp
alt - l : yabai -m space --layout float
alt - s : yabai -m space --layout stack

In stack mode, new windows open on top of each other. Cycle through them with alt + p / alt + n:

# cycle through stack windows
# alt - p : yabai -m window --focus stack.next || yabai -m window --focus south
# alt - n : yabai -m window --focus stack.prev || yabai -m window --focus north

# forwards
alt - p : yabai -m query --spaces --space \
            | jq -re ".index" \
            | xargs -I{} yabai -m query --windows --space {} \
            | jq -sre "add | map(select(.minimized != 1)) | sort_by(.display, .frame.y, .frame.x, .id) | reverse | nth(index(map(select(.focused == 1))) - 1).id" \
            | xargs -I{} yabai -m window --focus {}

# backwards
alt - n : yabai -m query --spaces --space \
            | jq -re ".index" \
            | xargs -I{} yabai -m query --windows --space {} \
            | jq -sre "add | map(select(.minimized != 1)) | sort_by(.display, .frame.y, .frame.y, .id) | nth(index(map(select(.focused == 1))) - 1).id" \
            | xargs -I{} yabai -m window --focus {}

Close a window with alt + w:

# close focused window
alt - w : yabai -m window --close

Fullscreen with alt + f. Handy for showing code to someone or during presentations:

# enter fullscreen mode for the focused container
alt - f : yabai -m window --toggle zoom-fullscreen

# toggle window native fullscreen
alt + shift - f : yabai -m window --toggle native-fullscreen

That’s it for yabai + skhd.

macOS settings

I miss dmenu and rofi, but Spotlight is good enough. Rebind it to alt + d in system preferences so it’s always one shortcut away:

image

You can also bind alt + 1-9 to jump directly to specific workspaces, which I’d recommend:

image

Hide the menu bar and dock in system settings too.

spacebar (i3status replacement)

Spacebar is a status bar for macOS, similar to i3status. It shows the time, user, battery, and active workspaces.

brew install cmacrae/formulae/spacebar
brew services start spacebar

Grant accessibility permissions again. Spacebar uses Font Awesome for icons:

brew install homebrew/cask-fonts/font-fontawesome

Create the config:

mkdir -p ~/.config/spacebar
touch ~/.config/spacebar/spacebarrc
chmod +x ~/.config/spacebar/spacebarrc

My spacebarrc:

#!/usr/bin/env sh

# Fixes a bug on the newest MacOS version, Thanks to Kamen Vakavchiev!
# see: https://github.com/cmacrae/spacebar/issues/101#issuecomment-1083252227
spacebar -m config right_shell off

spacebar -m config position                    top
spacebar -m config height                      26
spacebar -m config title                       on
spacebar -m config spaces                      on
spacebar -m config clock                       on
spacebar -m config power                       on
spacebar -m config padding_left                20
spacebar -m config padding_right               20
spacebar -m config spacing_left                25
spacebar -m config spacing_right               15
spacebar -m config text_font                   "Helvetica Neue:Bold:12.0"
spacebar -m config icon_font                   "Font Awesome 5 Free:Solid:12.0"
spacebar -m config background_color            0xff202020
spacebar -m config foreground_color            0xffa8a8a8
spacebar -m config power_icon_color            0xffcd950c
spacebar -m config battery_icon_color          0xffd75f5f
spacebar -m config dnd_icon_color              0xffa8a8a8
spacebar -m config clock_icon_color            0xffa8a8a8
spacebar -m config power_icon_strip             
spacebar -m config space_icon                  •
spacebar -m config space_icon_color            0xffffab91
spacebar -m config space_icon_color_secondary  0xff78c4d4
spacebar -m config space_icon_color_tertiary   0xfffff9b0
spacebar -m config space_icon_strip            1 2 3 4 5 6 7 8 9 10
spacebar -m config clock_icon                  
spacebar -m config dnd_icon                    
spacebar -m config clock_format                "%d/%m/%y %R"
spacebar -m config right_shell                 on
spacebar -m config right_shell_icon            
spacebar -m config right_shell_command         "whoami"

echo "spacebar configuration loaded.."

Tell yabai to leave room for the bar by adding this to .yabairc:

# spacebar padding on top screen
SPACEBAR_HEIGHT=$(spacebar -m config height)
yabai -m config external_bar all:$SPACEBAR_HEIGHT:0

Restart spacebar:

brew services restart spacebar

Terminals

I tried both kitty and alacritty and stuck with alacritty. Both work fine — kitty has more built-in features (image support, side-by-side diff, unicode input), alacritty is more minimal and delegates to other tools.

Kitty

brew install --cask kitty

Grant accessibility permissions, then add to .skhdrc:

# open terminal
alt - return : open -n /Applications/kitty.app/Contents/MacOS/kitty --single-instance -d

Alacritty

brew install --cask alacritty

Grant accessibility permissions, then add to .skhdrc:

# open terminal
alt - return : open -n /Applications/Alacritty.app

Cheatsheet

Modes:

In stack mode:

Wrapping up

I still prefer Linux, but this setup makes macOS workable for me. yabai + skhd cover most of what i3 does. I miss a few things —> proper window stacking, no animation when new tiles appear but it’s close enough.

My .yabairc, skhdrc, and spacebarrc files are here.

#MacOS #Window Manager