A story of exploration, optimization, and how awesome open-source tooling can be.
I'm somewhat stuck using a mac because of multiple critical tools that I use on it. I've always been a DIYer with my tools though, which is something FOSS OS's like Linux or *BSD enable much more. But I've found two tools that I enjoy a great deal: Aerospace (a tiling window manager in the vein of i3) and sketchybar (a status bar like i3bar or polybar). Both of these tools offer fantastic configurability and do an admirable job piercing the walled-garden that is the Mac OS.
Because a desktop status bar generally displays the list of workspaces and indicates which one is active, there's a good deal of integration needed between the two. I got started with the suggestion in the Aerospace wiki.
First, the instructions have us configure aerospace to (a) start up sketchybar after it starts up itself, and (b) trigger a custom sketchybar event whenever the active workspace changes.
# ~/.aerospace.toml after-startup-command = ['exec-and-forget sketchybar'] exec-on-workspace-change = ['/bin/bash', '-c', 'sketchybar --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE' ]
Next, in sketchybar's configuration file (which is just a shell script - beautiful) we'll both create the custom event for aerospace to trigger, and loop over aerospace's workspaces to display them in the status bar. These status bar items will also listen for the custom event, and trigger a script in the sketchybar config's plugin directory.
# ~/.config/sketchybar/sketchybarrc sketchybar --add event aerospace_workspace_change for sid in $(aerospace list-workspaces --all); do sketchybar --add item space.$sid left \ --subscribe space.$sid aerospace_workspace_change \ --set space.$sid \ background.color=0x44ffffff \ background.corner_radius=5 \ background.height=20 \ background.drawing=off \ label="$sid" \ click_script="aerospace workspace $sid" \ script="$CONFIG_DIR/plugins/aerospace.sh $sid" done
The "click_script" in there is a nice touch, it's having aerospace change the active workspace when we click one in the status bar.
Finally, we'll create that script in the plugin directory. It's ultimately what's going to be run whenever there's a change to the currently-active workspace.
# ~/.config/sketchybar/plugins/aerospace.sh #!/usr/bin/env bash if [ "$1" = "$FOCUSED_WORKSPACE" ]; then sketchybar --set $NAME background.drawing=on else sketchybar --set $NAME background.drawing=off fi
The condition here ensures that an extra background will be drawn around only the currently active workspace, which is great.
The visual update in the bar felt a little sluggish though.
I realized that I was never aware of where my windows were. I could still always cmd-tab my way around applications in standard mac fashion, but I felt I was under-utilizing my great tiling WM. I decided I wanted a different background to appear around any workspace which has an application window on it.
A simple enough edit to sketchybar's aerospace plugin script.
# ~/.config/sketchybar/plugins/aerospace.sh #!/usr/bin/env bash POPULATED="$(aerospace list-windows --all --format '%{workspace}' | sort | uniq | tr '\n' ' ')" if [ "$1" = "$FOCUSED_WORKSPACE" ]; then sketchybar --set $NAME background.drawing=on background.color=0xaaeab676 else if [[ $POPULATED =~ $1 ]]; then sketchybar --set $NAME background.drawing=on background.color=0x8800ffff else sketchybar --set $NAME background.drawing=off fi fi
I can first shell out to ask aerospace for the workspace that each window is on (and then unique that list), then with an extra conditional on non-active workspaces I can style them differently if they were in the list. Spruce it up with a bit of color and we look to be there!
But the "little sluggish" from before has become horrendous performance. It's a few seconds for this to update now.
I had just added a shell-out to an external program into sketchybar/plugins/aerospace.sh. Looking back at how this script gets executed,
This means that the script runs *separately for each workspace* whenever the active workspace changes. In my case this was 22 times. But that "POPULATED" list would be the same for each - I definitely don't have to collect it 22 times.
So: this will be a sizable refactor, but I instead want to have a plugin script that updates the display of all 22 workspaces in the sketchybar, and then perhaps I can wire it up to only run once for every change of active workspace.
First, let's update the plugin script to reset styles on *all* the workspaces.
# ~/.config/sketchybar/plugins/aerospace.sh #!/usr/bin/env bash POPULATED="$(aerospace list-windows --all --format '%{workspace}' | sort | uniq | tr '\n' ' ')" FOCUSED="$(aerospace list-workspaces --focused | tr '\n' ' ')" for ws in $(aerospace list-workspaces --all); do if [[ $FOCUSED =~ $ws ]]; then sketchybar --set space.$ws background.drawing=on background.color=0xaaeab676 else if [[ $POPULATED =~ $ws ]]; then sketchybar --set space.$ws background.drawing=on background.color=0x8800ffff else sketchybar --set space.$ws background.drawing=off fi fi done
Now let's update the sketchybar startup script as well and get rid of the "--subscribe" and "script=" on each and every workspace displayed.
# ~/.config/sketchybar/sketchybarrc sketchybar --add event aerospace_workspace_change for sid in $(aerospace list-workspaces --all); do sketchybar --add item space.$sid left \ --set space.$sid \ background.color=0x44ffffff \ background.corner_radius=5 \ background.height=20 \ background.drawing=off \ label="$sid" \ click_script="aerospace workspace $sid" done
But, now *nothing* is subscribed to that "aerospace_workspace_change" event. This was perhaps pretty lazy, but since I just need *anything* to bridge the event to the script, and the next thing in the sketchybar is a little arrow icon...
sketchybar --add item chevron left \ --set chevron icon= label.drawing=off \ --subscribe chevron aerospace_workspace_change \ --set chevron script="$CONFIG_DIR/plugins/aerospace.sh" \ ...
Eh, it'll do. We should be ready! And it does, indeed, work. And it is, indeed, faster. Testing the script itself by just running it directly under "time", It's taking around 550ms. Passable for something that's at least only running once per interaction.
I realized, though, that I hadn't gotten rid of the N+1 subprocesses problem. Namely, my script is running "sketchybar" independently for every workspace, even though you can stack up multiple --set parameters in a single call. Let's try that instead, building up the command in the loop and then just running it once.
# ~/.config/sketchybar/plugins/aerospace.sh #!/usr/bin/env bash POPULATED="$(aerospace list-windows --all --format '%{workspace}' | sort | uniq | tr '\n' ' ')" FOCUSED="$(aerospace list-workspaces --focused | tr '\n' ' ')" CMD="sketchybar" for ws in $(aerospace list-workspaces --all); do if [[ $FOCUSED =~ $ws ]]; then CMD+=" --set space.$ws background.drawing=on background.color=0xaaeab676" else if [[ $POPULATED =~ $ws ]]; then CMD+=" --set space.$ws background.drawing=on background.color=0x8800ffff" else CMD+=" --set space.$ws background.drawing=off" fi fi done $CMD
It works, and runs in about 170ms - over 3X faster.
I found that if I kick a window to a different workspace with CMD+Option+Shift+<letter> (my keybinding for this action), of course the display in the status bar is now potentially all wrong.
The nice thing about having a 170ms script that updates everything though, is you can just call it whenever you need to and not worry too much about it.
In my aerospace config file I previously had:
cmd-alt-shift-a = 'move-node-to-workspace A' cmd-alt-shift-b = 'move-node-to-workspace B' cmd-alt-shift-c = 'move-node-to-workspace C' ...
And now those directives are:
cmd-alt-shift-a = ['move-node-to-workspace A', 'exec-and-forget /usr/local/bin/sketchybar --trigger aerospace_workspace_change'] cmd-alt-shift-b = ['move-node-to-workspace B', 'exec-and-forget /usr/local/bin/sketchybar --trigger aerospace_workspace_change'] cmd-alt-shift-c = ['move-node-to-workspace C', 'exec-and-forget /usr/local/bin/sketchybar --trigger aerospace_workspace_change'] ...
---