So you want i3wm on MacOS?

Recently I was forced to leave my beloved Linux setup because I got a Macbook from my new employer. I’m sure some people are laughing now, but the year 2021 is the first time in my life that I own an Apple product. Although MacOS is known to be very user-friendly and to offer a nice, clean and minimal UI, I actually had a lot of trouble switching my workflow at first.

I think I am not the only one in this situation. I have worked with window managers under Linux for years. Over time I have learned to appreciate i3 very much and nowadays I can’t work without it! Thanks to a great colleague (thanks Marcel aka @snowiow!) who went through the same pain, I got some great tips to get an i3wm feeling on MacOS.

In fact, I’m very happy with the result, because it covers 90% of the features that made me love i3wm for years.

yabai + skhd (i3 replacement)

yabai is a window management utility that is designed to work as an extension to the built-in window manager of macOS. yabai allows you to control your windows, spaces and displays freely using an intuitive command line interface and optionally set user-defined keyboard shortcuts or other third-party software.

I recommend installing yabai via brew. This will allow us to conveniently update yabai via our package manager in the future.

brew install koekeishiya/formulae/yabai

Next we activate the service. yabai will ask you for permissions that you need to set. Next we’ll have to grant permissions under Accessibility for yabai, then start the service:

# start yabai
brew services start yabai

yabai is managed by a configuration file. This configuration file is named .yabairc and lives in your home directory (you can also change the path, but I prefer to keep my doftiles all inside my homedir). Create a file with the following command

touch ~/.yabairc

Here is my .yabairc. I will go into details afterwards.

#!/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.."

The yabai config shown above configures some useful things. I like it when I can move the mouse over windows and they are automatically selected. This is what the following section does:

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

I have also completely removed the distances between the individual windows. After all, we want to use all the space on our workspace. You can also adjust these settings to your liking and, for example, increase the spacing a little, if that should be visually more pleasant.

# 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

Of course, you can also add other programmes here. If you don’t use Goland, for example, you can remove the section below completely.

# 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

Next we need to make our .yabairc executable:

chmod +x ~/.yabairc

Optional: Disable window animations. If the animations of MacOS bother you, you can additionally deactivate them. Unfortunately, there is still a short animation when windows are launched and integrated into the tiling. But at least all other animations can be turned off.

I have disabled the following two things, there are many more.

# 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

Have a look at this issue for further settings.

After that, restart yabai

brew services restart yabai

If you’d like to keybind all your commands, you can pair yabai with skhd. skhd is a simple hotkey daemon for MacOS that focuses on responsiveness and performance. Hotkeys are defined in a text file through a simple DSL. skhd is able to hotload its config file, meaning that hotkeys can be edited and updated live while skhd is running.

We can also conveniently install skhd via brew. Then we also start this service

brew install koekeishiya/formulae/skhd
brew services start skhd

The first time skhd is ran, it will request access to the accessibility API.

Next, we create a configuration file for skhd with the name .skhdrc. Here too, we can either create the file under ~/.skhdrc, or under a different path such as ~/.config/skhd/skhdrc.

touch ~/.skhdrc

In this file we can now create some useful keybindings that are very close to our beloved i3 under Linux.

The first thing I configured is the ability to navigate between windows with the arrow keys. Alternatively, h,j,k,l can be used.

# 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

I would also like to move individual tiles back and forth. Here I have created a binding that allows to move tiles with alt + shift + arrow keys or with alt + shift + <h j k l> in any direction. If a tile borders the screen and is dragged outside the space, then the tile is moved 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)

Sometimes you want to set new windows above or below the one currently in focus. To do this, the key combination alt + ctrl + arrow keys or alt + ctrl + <h j k l> can be used to set an insertion point (displayed graphically).

# 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

I also use a simple shortcut (alt - b) to quickly switch back and forth between windows.

# 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

With the key combination alt + shift + n we can easily move focused tiles to another workspace. n’ is in this case the number (1-9) to which the tile should be moved.

# 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

We can also mirror the tiles in a workspace horizontally or vertically.

# # 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

Or detach individual windows from the tiling so that we can move them freely around without attaching to other ones.

# 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

skhd also supports stack mode alt - s, so new windows will be created on top of each other and we can cycle trough them via shortcuts

# 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 {}

With alt + w we can easily terminate a currently focused tile.

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

With alt + f we can bring a focused tile into fullscreen mode. This is very practical if you want to show your colleagues some code or if you want to use the whole desktop quickly in a presentation.

# 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

We are done with the yabai + skhd part. We can still configure some stuff ourselves via the MacOS system settings to make our whole workflow a little more effective.

MacOS settings

The spotlight search is not so wrong! I know you miss dmenu or rofi as much as I do, but I have made my peace with spotlight.

In the system preferences we can still change the Spotlight shortcut to alt + d, then we always have our search conveniently at our side whenever we need it.

image

What is also very nice is the possibility to jump back and forth between workspaces via shortcut. Since I use a lot of shortcuts with alt as a prefix anyway, I have adapted the binding so that I can easily jump to the corresponding, active workspaces with alt + <1-9>.

image

What we want to do now is hide the menu bar and the status bar. We can also easily configure both in the system settings.

spacebar (i3status replacement)

Spacebar is a cool status bar, similar to i3status. It shows everything you need, such as the time, user, battery status and our active workspaces.

We can also install Spacebar again quite comfortably via brew and then start it.

# install spacebar
brew install cmacrae/formulae/spacebar
# start spacebar
brew services start spacebar

Next we’ll have to grant permissions under Accessibility for spacebar again.

Spacebar uses fontawesome for the display of icons. To do this, we install the font via brew

brew install homebrew/cask-fonts/font-fontawesome

Like yabai and skhd, spacebar uses a configuration file that can be used to turn off or personalise information in the bar. Create a config file at ~/.config/spacebar/spacebarrc

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

Here is the content of my spacebarrc. Of course, everyone can tinker with the settings as they like.

#!/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.."

The last thing we need to do to make our bar look nice is to tell yabai to keep some space above the screen. We add the following to our .yabairc:

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

Now we restart the service once and we have a fancy new menu bar!

brew services restart spacebar

Terminals

Opinions differ on the choice of terminal application. Personally, I have tried the two tools kitty and alacritty with MacOS. I think both have advantages and disadvantages, but in the end I stuck with alacritty.

Kitty

kitty has many foundational features, such as: image support, superlative performance, various enhancements to the terminal protocol, etc. These features allow the development of rich terminal applications, such as Side-by-side diff and Unicode input.

To install kitty via brew run the following command:

brew install --cask kitty

Next we’ll have to grant permissions under Accessibility for kitty.

Furthermore, we want to create a skhd shortcut to open a new terminal window with alt + return. For this we can add the following to our .skhdrc:

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

Alacritty

Alacritty is a modern terminal emulator that comes with sensible defaults, but allows for extensive configuration. By integrating with other applications, rather than reimplementing their functionality, it manages to provide a flexible set of features with high performance.

To install alacritty via brew run the following command:

brew install --cask alacritty

Next we’ll have to grant permissions under Accessibility for alacritty.

Furthermore, we want to create a skhd shortcut to open a new terminal window with alt + return. For this we can add the following to our .skhdrc:

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

Cheatsheet: Everyday commands

Switching between modes:

cycle through windows while you’re in stack mode

Conclusion

Although I still love Linux over MacOS, I have to say that I am getting used to the change with this setup.

yabai and skhd (+spacebar) offer a lot of features that allow me to bring my workflow as close as possible to my old i3 bindings. I do miss some things though, for example the convenient stacking of windows or the lack of deactivating animations when creating a new tile. Nevertheless, the setting works for me, despite some minor drawbacks.

I hope this blogpost helps some people who work with MacOS but don’t want to miss their old i3wm setup.

You can find my .yabairc, skhdrc and spacebarrc files here

#MacOS #Window Manager