The Phoenix script that runs my four-slot macOS desktop
A 40-inch ultrawide gave me plenty of pixels and nowhere obvious to put a 1,920 by 1,080 screen-share window.
- Published
- Updated
- Reading time
- 10 min read
- Series
- Dotfiles
My Dell U4021QW solved one problem and created another. Its 40-inch ultrawide panel gave me enough room for several useful windows, but ordinary maximization wasted the width and manual placement made me drag the same windows into the same positions every day.
What I wanted was oddly specific. One 1,920 by 1,080 frame should sit at the top center. This is where I put the window that needs my attention and the one I usually share on calls. A second central area should fill the space below it. Two tall columns should frame the center left and right.
No window manager I tried knew that arrangement. Phoenix did not need to know it. Phoenix is a lightweight macOS application and window manager that exposes windows, screens, Spaces, and keys to JavaScript. Kasper Hirvikoski built the application. I only had to describe my desktop in ~/.phoenix.js.
I wrote the first version in June 2022. I still use the same four-part geometry. Most adjustments since then have taught Phoenix which application windows to leave alone.
Four areas from one rectangle
The Dell panel has a physical resolution of 5120×2160, a format sold as 5K2K. Its Thunderbolt 3 connection carries the display signal and supplies up to 90 watts of power, so one cable connects my computer to the display and the peripherals attached to it.
The script does not hard-code that full resolution. It asks Phoenix for the screen’s flippedVisibleFrame(), which excludes visible space used by the menu bar and Dock and puts the origin at the top left. It derives every area from that rectangle and one constant:
const FULLHD = {
width: 1920,
height: 1080,
};
The resulting layout has four named areas:
| Area | Behavior | Frame |
|---|---|---|
main |
one window | 1,920 by 1,080 at the top center |
secondary |
vertical stack | 1,920 wide, below main |
left |
vertical stack | half the width left over from main, full visible height |
right |
vertical stack | the other half, full visible height |
If Phoenix reports a 5,120-wide visible frame, each side column is 1,600 units wide. The exact available height depends on display scaling, the menu bar, and the Dock. That is why the code uses the visible frame instead of repeating a guessed screen height.
The central frame matters most during screen sharing. I can share one conventionally proportioned application window instead of exposing a 5,120-pixel-wide desktop to someone on a laptop. Video-call software may still scale the stream, but its source starts with the 16:9 shape I intended. I also know exactly which part of my own screen other people can see.
The side columns suit terminals, editors, mail, calendars, and chat. A single terminal in one column gets the full available height. Adding another window divides that same column rather than covering the first window.
Two container behaviors
The script implements the layout with a small container model. A SingleFramedContainer owns the main area. A VerticalContainer owns each of the other areas.
The single container gives its current window one fixed frame. Moving a managed window into main exchanges it with the existing occupant. The previous main window returns to the source area, and that area’s windows reflow. If the incoming window was not managed, the previous occupant leaves management and remains at its last frame.
Vertical containers divide their height by the number of windows they contain:
class VerticalContainer extends FramedContainer {
render() {
const windowHeight = Math.floor(this.frame.height / this.stack.length);
for (const [i, window] of this.stack.entries()) {
window.setFrame({
x: this.frame.x,
y: this.frame.y + i * windowHeight,
width: this.frame.width,
height: windowHeight,
});
}
}
}
That loop is the tiling algorithm. Each window receives the container width and an equal share of its height. A new side-window goes to the top of the array, so existing windows move down and shrink to make room. Removing or minimizing one enlarges the remaining windows.
Phoenix supplies the operations underneath this code. Its Window API can set a frame, inspect minimized state, and focus the closest neighbor in a direction. My configuration decides which frame and which neighbor matter.
Hotkeys follow the layout
I use one chord, Control+Command, for every operation. Arrow keys move the focused window. Vim direction keys move focus.
| Hotkey | Action |
|---|---|
Control+Command+Up |
exchange the focused window with the first window in main |
Control+Command+Down |
exchange it with the first window in secondary |
Control+Command+Left |
insert it at the top of the left stack |
Control+Command+Right |
insert it at the top of the right stack |
Control+Command+H/J/K/L |
focus the closest window west, south, north, or east |
Control+Command+X |
release the focused window from management |
Control+Command+R |
remove newly excluded windows from managed stacks and render again |
Control+Command+D |
log every area and the windows it contains |
The arrow keys mirror the screen. I do not need an application-specific shortcut or a command palette. Moving a window takes one chord and produces no animation.
The release key matters as much as the placement keys. Control+Command+X removes a window from every managed container without closing or moving it. Phoenix stops touching that window until the configuration loads again or I move the window into an area. This gives me an ordinary floating desktop when one application needs it.
Exceptions kept the system useful
The first version assumed that visible application windows were ordinary rectangles. macOS applications soon supplied counterexamples. 1Password opens an autofill panel near the field that requested it. Updaters create short-lived dialogs. Microsoft Teams has created invisible notification windows. Utilities may expose empty or zero-size windows that should never occupy a tile.
My current application exclusions cover 1Password, System Settings, Activity Monitor, Remote Desktop, Loopback, LaunchBar, Temp Monitor, Pixelmator Pro, BetterDisplay, and Ollama. Window checks reject empty titles, zero-size frames, the Teams notification title, and updater titles. Minimized windows live in a separate list until Phoenix reports that they have returned.
These exceptions do less than another set of placement rules would. They say that Phoenix has no responsibility for a particular application or window. The application can float a panel, choose its own position, or disappear without causing four unrelated windows to resize.
The configuration also has affinity rules. Slack, Spark, WhatsApp, and Teams go to the left. Notes, Preview, and Google Meet go to the right. Those affinities apply only to windows Phoenix finds when the configuration loads. The current file has its appDidLaunch and screensDidChange handlers commented out, and it does not register a Space-change handler. New windows do not get pulled into a column merely because an application opened them. I place those with the same directional hotkeys.
That limitation is acceptable in daily use and better than claiming automation the script does not perform. Startup is deterministic. Later choices stay under my control.
Keep geometry fixed and exceptions cheap
Custom automation becomes hard to trust when every unusual application forces a redesign. This configuration separates geometry from compatibility. Geometry decides where managed work appears. Compatibility decides whether Phoenix should manage a window at all.
A popup does not justify a new container. It belongs in an exclusion. A window that needs to float for a while uses Control+Command+X. An actual screen-layout change belongs in the functions that define the container frames. Each kind of friction has one place to go.
That boundary also limits what the script attempts. Phoenix does not infer why a window opened or whether it deserves attention. It places accepted windows into four containers and stays away from rejected ones. I make the decision when I add an affinity, an exclusion, or press a directional key.
Phoenix already moves rectangles with setFrame(). My configuration records the four rectangles that match my monitor, my screen-sharing habits, and the applications I use. It gives one task the central 1,920 by 1,080 frame, keeps supporting windows visible around it, and lets troublesome windows opt out.
That is enough automation for my desktop. The layout has remained useful since 2022 because exceptions can stay exceptions instead of changing the whole system.