Skip to content

User Guide

The Settings window explains its own rows: a label, often a caption, and on some rows a ? with a longer note. This guide covers what those cannot say — how things interact, where a setting lives, why a move was refused, and the files behind it.

Open Settings from the KiwiDesk menu in the menu bar, or press ⌘, while a KiwiDesk window is key.

Shortcuts & Gestures ▸ Open applications offers a rebindable Open Settings row for a global key, under KiwiDesk.

That row ships on ⌃⌥,, a default shortcut, so Settings opens from anywhere.

The Settings window tiles into your layout like any other window and answers the window shortcuts (float it with toggle_floating, move it between spaces).

Out of the box, macOS lets Tab reach text fields and lists only. Turn on System Settings ▸ Keyboard ▸ Keyboard navigation and Tab reaches every control.

Opening an area from the keyboard puts focus on the page, so the next Tab reaches its first control; opening it with the mouse takes no focus, as anywhere on macOS. Shift-Tab reaches the ← Home chip; ⌘[ or Escape goes back. ⌘K puts the cursor in the search field from anywhere in the window.

With keyboard navigation off, a control that cannot hold focus hands it to the search field instead. VoiceOver is unaffected, and every card and panel title is a heading for its rotor.

  • A slider takes focus and the arrow keys step it, in the readout’s own step.
  • A segmented control is one Tab stop; ← / → move the choice.
  • Where a row has right-click moves (rename, export or delete a palette; reorder spaces), press ⌃. (Control-Period) on the focused row for the same menu.

Open KiwiHub… opens KiwiHub on its view picker (kiwihub.show), the side ⌃⌥L opens, and shows that side’s shortcut beside it. While KiwiDesk is starting, or until you press Start Tiling, the row still opens it, with the view picker’s items greyed and the reason shown.

A layout switched from the view picker, or from a Space’s right-click Layout menu, is session-only: it does not rewrite the profile, and both say “not saved to profile” while it drifts. Keep in Profile “‹name›” (⌘S in the view picker), or Keep Layout in Profile “‹name›” in the Layout menu, writes what is on screen, every screen at once, into the active profile — the layouts, not a keyboard or mouse resize, which is session-only. A Settings Save does not touch a temporary layout, which stays on screen through it; the one exception is a Space whose layout you changed in Settings → Spaces, which wins on Save.

KiwiDesk checks for updates on its own in the background, and a found update shows as a dot on the menu-bar icon and an Update Available… row, never a pop-up.

The quick menu has no check row of its own: ask for a check from the foot of Settings Home, which also says when KiwiDesk last checked.

If you installed with Homebrew, KiwiDesk keeps itself up to date and brew upgrade steps aside; to move an older copy onto a version that can do this, run brew upgrade --cask kiwidesk once.

KiwiDesk keeps your custom Lua in ~/.config/KiwiDesk/init.lua and never edits it. Settings stores its own configuration in ~/.config/KiwiDesk/gui.json (global) and one JSON file per saved profile.

  • Saving in Settings never rewrites init.lua.
  • Custom Lua that is not app rules, float rules, ignore rules, keybindings or profile bindings lives alongside the visual editor; a blue banner reads “Custom Lua detected”.
  • If init.lua declares managed vocabulary (app_rules, float_rules, ignore_rules, KiwiDesk.bind, keybinding definitions), Settings shows a raw Lua editor instead. Adopt into the GUI imports them, comments the migrated settings out as a backup, and keeps your custom Lua (event hooks such as the sketchybar bridge) live.

Once gui.json exists, the visual editor owns tiling, and hand-written set_gap_global calls stop applying on screen changes; persist custom tiling as a profile.

First launch with an existing init.lua: the default gui.json is seeded as long as init.lua declares no managed settings, so a file of event hooks boots GUI-managed. An init.lua that sets tiling itself (KiwiDesk.set_*, rules, or keybindings) stays Lua-owned, and Adopt into the GUI is offered instead.

Start at login (General) reads and writes the real macOS login item. Crash supervision is command-line only: kiwidesk service start installs a helper that relaunches KiwiDesk after a crash, never after a deliberate Quit (CLI reference). While that service runs, Start at login shows as on and stops being editable; kiwidesk service stop gives the checkbox back.

Lua-only (enable_wake_restore, set_wake_restore_delay in the Lua reference): after sleep or screen unlock, KiwiDesk restores the arrangement captured when the Mac went to rest (on by default, after a 1500 ms delay) and puts focus back on the window you were in. A wake restore is skipped when the screen setup changed during sleep; the screen-change profile switch takes over. If a restore leaves things wrong, General ▸ Files & Reset ▸ Saved window arrangement ▸ Discard clears it.

General ▸ Files & Reset ▸ KiwiDesk backup ▸ Export… writes one file with your settings, every profile, your saved palettes and your Desktop bindings; Restore from backup ▸ Restore… puts it back. Not included: the remembered window arrangement. For continuous sync see The gui.json File.

A backup does not carry which Space each Desktop remembers either: that stays on the Mac it was made on (Spaces and Desktops).

Restoring replaces, it does not merge, and what it replaces goes to the Trash. On the way, the remembered window arrangement on that Mac is forgotten, and KiwiDesk picks the profile matching the screens connected here (a Desktop binding still wins). Refused: a file that is not a KiwiDesk backup, one from a newer KiwiDesk, one that would restore nothing, and one carrying settings when this Mac’s settings come from init.lua (profiles and palettes alone restore there). A restore that skips a profile or a palette says so.

init.lua travels only when asked, off by default: tick Include init.lua when you export, and pick Restore with init.lua when you restore, which runs it at once and is the one restore of settings a Mac whose settings come from init.lua accepts. The same backup is one command away with kiwidesk export_setup and kiwidesk restore_setup (CLI).

Saved looks travel in a backup beside the palettes, and restore wherever they do; All settings ▸ Reset… keeps them too. The shared look travels with the settings, and each profile keeps whether it uses it. Restoring a backup written before profiles could share a look gives every profile its own, then the first saved profile loaded lends the shared look to those wearing the same one.

The alert sound and the windows per pile on quit travel in a backup. An older backup keeps them in its profiles instead, and restoring it takes them from the profile that becomes active.

~/.config/KiwiDesk/gui.json holds the global base configuration. On a fresh install it is created at first launch with the default shortcuts; on a hand-written setup, the first time you Save in Settings.

Keeping several Macs in sync. Pick a folder under Settings › General › Sync across Macs: iCloud Drive, or any folder Dropbox, Syncthing, Nextcloud or a similar app keeps in step. With Keep in sync automatically on, KiwiDesk copies gui.json, your profiles, palettes, looks and init.lua into the folder as they change, and applies what another Mac copies there. Your config stays in ~/.config/KiwiDesk; turning the switch off keeps it there and leaves the folder as it is. Grant Accessibility on each Mac; screen layouts, macOS Desktops and Desktop bindings stay with each Mac.

A change from another Mac that lands while Settings holds unsaved edits leaves your edits in place, and Save writes them on top. A setting both Macs changed waits for Save, which lists each one in a sheet where you pick Yours or Theirs; Save stays dimmed until every row is picked, and init.lua edited on both is one row for the whole file. When both Macs changed the setup while the folder could not be reached, nothing is copied until you choose a side with Compare and Copy. KiwiDesk never reads a conflict copy a sync app leaves beside the files (gui 2.json, a “conflicted copy”); the line under the switch names it. Reset All Settings turns the switch off, so the folder keeps the setup the other Macs use; turning it back on, or restoring a backup while it is on, asks which setup to keep with Compare and Copy.

When an update moves gui.json, a profile, your palettes or your looks to a newer file format, KiwiDesk first keeps the file as it was in ~/Library/Application Support/KiwiDesk/migration-backups/, under its own path with .pre-v<format> added; <format> is the file’s own format number, not a KiwiDesk version. One migration copy is kept per file: the file before its latest migration. To go back to an older KiwiDesk, quit it, copy that file over the original and drop the suffix. A file KiwiDesk cannot read at all — a hand edit that broke the JSON, say — is replaced by the next save that writes it, after the broken file is kept in the same folder with .corrupt added, one copy per file. A backup does not carry the folder, and Reset All Settings leaves it.

The socket, the instance lock, the saved window arrangement, machine.json and bindings.json (your Desktop bindings) live in ~/Library/Application Support/KiwiDesk/ itself, so a synced config folder carries neither file; Reset All Settings clears both.

An older KiwiDesk never writes over gui.json, a profile, your palettes or your looks once a newer one has saved them. It lists the file under menu bar › Config Issues…, and a save that would write it is refused until that Mac is updated. Versions before 3.0.0 do not have this check, so update every Mac sharing a synced folder together.

{
"spaces": [ "1", "2", "mail" ],
"app_rules": { "com.spotify.client": "music" },
"float_rules": [ "com.apple.systempreferences" ],
"ignore_rules": [ "eu.exelban.Stats" ],
"profile_bindings": {
"9C1F2A44-3B0E-4E7D-9A21-6D5C8B7E0143": {
"profiles": [ "Developer" ], "desktop": 1
}
},
"layers": [
{ "name": "default", "bindings": [...] }
]
}
  • spaces: space ids (strings), in order.
  • app_rules: app bundle identifier → space id.
  • float_rules: bundle identifiers (optionally bundle-id:title) whose windows never tile.
  • ignore_rules: bundle identifiers never tracked. No Settings control; Settings preserves it on save.
  • profile_bindings: Desktop identifier → what that Desktop selects on your main screen. The key is the identifier KiwiDesk stamps into the Desktop itself, not its Mission Control number; the number sits in desktop as a label. On a Mac where the stamp cannot be written, the key is the number. A file in the older { "1": "Developer" } shape is rewritten once on load. See A binding follows its Desktop, not its number.
  • layers: keybinding layers, only one active at a time. Each has a name (“default” for the main set), an optional icon (SF Symbol name or emoji), and bindings: rows of combo (e.g. "cmd+alt+left"), lua (the body inside function() ... end), kind (“navigation”, “application”, or “custom”) and label.

A row may also carry desc, group and symbol, written only when set (Describing a Shortcut).

refusal holds sound, the alert sound when an action is blocked, and quit holds grid_target_depth, the windows per pile on quit, beside layout, how windows are spread on quit. The first two are set on General; layout is set from Lua (quit.set_layout).

look holds the shared look: every color, the bars’ styling, Liquid Glass, the focus border’s shape and the global gaps, worn by every profile without its own. Settings writes it; choose which profiles use it in Looks & Animations ▸ Shared look.

A profile_bindings entry names its profiles as a list, profiles, one per screen count, each for all screen setups — a file with the older single profile is rewritten once on load — and may also carry screen, the name of the screen its Desktop was last seen on.

An item of profiles may instead be an object that binds its profile for one screen setup, naming every screen by its fingerprint as list_monitors prints it: { "profile": "Studio", "setup": [ "Built-in Retina Display:1512x982", "LG UltraFine:2560x1440" ] }. A bare name binds for all screen setups.

A hand-edited layers list is normalized on load: empty names are dropped, a duplicated name keeps its first entry, default always exists and sits first, and an icon on default is removed.

This key was called "modes" in earlier pre-release builds. A file still using the old name loads as no shortcuts at all and the next Save writes that emptiness back, so run this before opening Settings:

Terminal window
sed -i '' 's/"modes"/"layers"/' ~/.config/KiwiDesk/gui.json

Repeat it for any file in ~/.config/KiwiDesk/profiles/ with a keybinding override, and rename KiwiDesk.define_mode and switch_mode in a hand-written init.lua. Or Reset all settings… trashes gui.json and reseeds the defaults.

To reset the app to what init.lua declares, delete gui.json. Do not import one from an untrusted source: custom Lua in keybindings runs on every reload.

  • gui.json (global), shared by every profile: the base keyboard shortcuts, app rules, float rules, ignore rules, and Desktop → profile bindings.
  • Each profile’s JSON, applied only while that profile is active: which spaces exist and their order, layout mode, gaps and per-layout / per-space tuning, space-to-screen pins, the Main role and the fallback space.

The alert sound when an action is blocked and the windows per pile on quit are global too: they sit in gui.json, and loading a profile never changes them.

The look is shared unless a profile keeps its own. The shared look sits in gui.json; a profile with its own carries "look": "own" and keeps it in its JSON. Looks & Animations ▸ Shared look ▸ Applies to ticks the profiles using the shared look. The profile you are editing cannot change its own box, so to give a profile its own look or share one, open a profile that uses the shared look and untick or tick it there; on a profile with its own look the list is greyed. A new profile shares the look if the profile it was saved from does. With a Lua-owned init.lua there is no shared look.

The animations are part of the look too: a profile using the shared look takes its animations, and a look you save keeps them. Applying a bundled look, or picking one in the tour, leaves your animations as they are. When you first run this version, a profile whose animations differ from the shared look’s starts keeping its own look, so nothing changes on screen.

The General section leaves the grid while you edit a stored profile without switching to it.

Two hybrids. Shortcuts and app rules are global, but a profile can carry a sparse override of its own: shortcuts can add or change specific bindings (Shortcut Layers), and app rules can pin an app to a different space or un-pin it (Per-Profile Space Assignments). When profiles share a space name, one base rule is enough; when their space sets are disjoint (Work {1, 2, 3} vs Home {media, games}), give each profile its own override.

What the fields’ own notes do not say:

  • Stack — the resize shortcuts are focus-aware: the split axis grows whichever zone holds the focus, the zone’s own axis grows the focused window’s share, and that share is session-only. A master zone lined up along the split axis has no reachable shares, which is the out-of-the-box case once the master count exceeds one; switch the orientation to vertical for individually resizable masters (Accepted limitations).
  • Scrolling — its focus animation and duration live here, beside the layout rather than with the other animations.
  • Track — the track shortcuts sit in Shortcuts & Gestures ▸ Move windows. Previous is the column to the left (or the row above), next the column to the right (or the row below), whichever way the axis runs. Track sizes and in-track shares are session-only.

The track shortcuts sit behind “Move windows in the track layout”, open from the start once you have a Track Space or a track shortcut bound.

  • Floating — switching a space to Floating with any window partly or fully off the screen (a scrolled-out column, a parked Monocle window) or piled behind another (a Monocle stack) arranges the space’s windows in the grid KiwiDesk uses on quit; with everything already reachable, nothing moves.

That grid’s windows per pile is set at General ▸ Windows per pile on quit.

A few resize behaviors are accepted limitations, not bugs — the inner window of a nested BSP pair not growing, or the shares of masters lined up along the split not moving. See Accepted limitations.

A space’s override editor (from its row in Spaces) edits the layout the space currently uses. Each row’s Override checkbox, unchecked, inherits the Layout Defaults value; checked, the control appears seeded with the current value. A field greyed by a switch on another page (Grid’s auto-size, Track’s auto limit) says so and links Layout Defaults.

Every space row carries a Customize button; its badge counts that space’s overrides, dimmed while a Floating space keeps them for when it tiles again. Customize… is also on the row’s right-click menu.

Changing a space’s layout never deletes overrides; each layout keeps its own, and a Saved for N other layouts card at the foot lists them. Reset All Layout Overrides there clears every layout’s, after a confirmation.

Drag a space chip onto a screen to pin it there; drag it onto the dashed Follows main screen tray to give it the Main role, which moves with whichever screen is main when you dock and undock. Outlined chips are placed automatically; a filled chip is yours. Every chip is also a menu, which is the keyboard route (Using Settings from the Keyboard).

KiwiDesk recognises a screen by name and resolution, so two of the same model at the same resolution are one identity and a space pinned to one may open on either. A space pinned to an absent screen gets its own card below the picture with Back to automatic placement.

With more than one screen, keep clear the bottom corner KiwiDesk parks each one’s hidden windows in, or they show on its neighbour (With more than one screen).

The focused screen is the one you last clicked — a window or the bare wallpaper. A new window, and a global sticky window, appear on that screen’s space. Clicking the menu bar or the Dock does not move focus.

Opening an app whose app rule names a space on another screen moves the focus there with its window.

A new window of a running app follows its app rule the same way.

Width and Corners at the top set the focus ring, the drag ghost and the drop zone together. Keep gaps at least twice the width so two neighbouring rings do not touch. Each overlay’s alignment is Lua-only; design decisions has why the GUI offers no switch, the Lua reference the verbs.

The ring’s colours are in Looks & Animations ▸ Colors & glass: the focused one under Accent, the unfocused one under Window marks, each dimmed while the matching switch here is off.

What gets no border. Launcher and panel overlays (Spotlight, Raycast, Alfred): never managed, never in a bar. Windows in native (green-button) fullscreen: macOS moves such a window off the Desktop, so the bars hide there, no layout pass or focus raise targets it, and its space tiles as if it were away; it keeps its slot and tiles back when it leaves fullscreen.

The pills. Focusing or swapping toward an edge with no window there bounces the ring; the window never moves. A refused resize bounces and shows a frosted pill:

  • Minimum window size reached — the window can shrink no further than the configured minimum.

  • Neighboring window at its minimum size — growing stopped where a neighbour would drop below its minimum; the neighbour marks itself.

    It also shows once, without a press, when a window arriving in a split cannot fit beside a neighbour already at its minimum.

  • On a scrolling space, growing stops at a maximum the app enforces (System Settings will not grow past its own width).

Where the limit is the app’s own — a minimum or maximum the app enforces, which KiwiDesk learns — the pill says so instead: This app won’t go smaller, Neighboring app won’t go smaller, This app won’t go bigger. Lowering the configured minimum does not move those.

Running out of screen is a silent stop. Under a held resize shortcut, a refusal shows its pill once and ends the glide.

Floating windows join directional focus as a second tier: when no tiled window lies in the pressed direction, focus reaches a floating window that way.

Releasing a dragged window over another’s slot on the same screen swaps the two. Dragging onto another screen moves it there, live: once the cursor settles on the other screen, its windows slide apart to open a slot, and pulling the cursor back moves the window home. Releasing outside every slot on your own screen snaps the window back. Floating windows show no overlay and cannot be dropped onto a tiled slot; use make tiled first (Accepted limitations).

A floating window dropped on another screen moves into the Space that screen shows, and stays where you let go of it, clear of the bars. A sticky window keeps its own Space.

A sticky window stays visible on every space. Two scopes (stickiness is per window; there is no app-rule list):

  • Toggle sticky everywhere — every space of every screen (∞ mark).
  • Toggle sticky on this screen — every space of the one screen it lives on (📌 mark). Moving it to a space on another screen re-homes it there.

Toggle sticky everywhere is a shortcut under Size & float.

Toggle sticky on this screen has no default key and no row there; bind toggle_display_sticky in the Lua bindings drawer.

Moving a ∞ window anywhere, or a 📌 window to another space on the same screen, is refused with a brief pill. On a single screen the two are identical. The flag survives closing and reopening the window and is independent of floating: a floating sticky window keeps its frame everywhere; a tiled one tiles into every space’s layout near where it sits on its home space, keeps a fully visible slot on a crowded space, and can be reordered or mouse-resized only on its home space — elsewhere the gesture snaps back and the mark expands into a pill naming the home space. The same pill appears when you drag another window onto its slot.

Where macOS supports it, sticky reaches across macOS Desktops: switch Desktops and sticky windows come along with the screen they are on. Keep sticky windows visible across Desktops (beside the mark toggle) switches it and appears only on a macOS that can drive Desktops; override_sticky_reach in Lua pins a single window the other way. Mission Control shows a sticky window on one Desktop at a time.

A ∞ window follows the space you focus onto its screen; when that space is a floating-mode space on another screen, KiwiDesk moves the window there at its last size, scaled to the screen.

The refusal pills ride the sticky mark: with Show mark on sticky windows off, a refused move fails silently. The Space Bar’s badge shows which windows are sticky either way. (Lua: sticky.set_mark, space_bar.set_sticky_badge, sticky.set_color, floating.set_color.)

Thickness runs 20–80 pt on the KiwiShelf card; Lua (kiwishelf.set_thickness) takes any value from 20 up.

The App Bar renders only in Monocle and Scrolling, and its card has no on/off row.

The KiwiShelf card’s App Bar in Monocle and App Bar in Scrolling switches are its visibility.

Drag an item to reorder the windows.

Click a grouped item to list its windows; a row picks one.

Styling it differently per layout is Lua-only: an app_bar.* field has a monocle.set_app_bar_* / scroll.set_app_bar_* twin (Per-layout App Bar overrides), except the App Bar’s edge, which every layout shares.

Whether the App Bar reserves its strip is global too (app_bar.set_reserve).

The starter setup puts the App Bar on the bottom edge and the Space Bar on top; set both to one edge to share one shelf.

Floating windows come last, past a thin line and the floating symbol. They cannot be dragged; Tile Window on the item’s right-click menu tiles one.

Liquid Glass is one switch for both bars, the shortcuts panel, the drag ghost and drop zone, and the sticky mark (its own card in Settings); on macOS before 26 each draws its flat look instead.

It covers the floating mark too.

It covers the plates a Space switch slides too, and the Space, Desktop and layout plates.

On by default, on every surface. While macOS’s Reduce transparency (System Settings ▸ Accessibility ▸ Display) is on, each surface draws its look without glass — the bars their Boxed or Plain shape with the Fill fully opaque, and no plate for a fully transparent Fill — and the switch stays as you set it.

One bar per screen, listing that screen’s Spaces in profile order. Click a Space to switch to it.

Click an app glyph to switch to its Space and focus that window.

Resting the pointer on a one-window glyph, or on an App Bar item that cuts its title, shows that window’s name beside the bar.

A glyph with a count badge, a +n badge and an App Bar group of two or more windows are lists: click one to open its list beside the bar, then click a row to switch to that window. The click on the item switches nothing. A check marks the window that has the focus.

An open list stays until you pick a row, click its item again, press Escape or click elsewhere. The arrow keys walk its rows and Return picks one.

A Space shown as its window count lists its windows when you rest the pointer on it, since clicking it switches to that Space.

A list too long for the screen ends in More windows, which opens a menu of every window. With VoiceOver, pressing the glyph opens that menu.

A Space holding more apps than fit keeps its focused app in the middle, with a +n on each side for the rest — at either end of the row one more glyph fits instead.

A +n always stands for two glyphs or more: where only one would be hidden, that glyph shows in its place.

While your pointer rests on a Space it holds still, so a second click lands where the first did; it re-centres once the pointer leaves. With Windows in other Spaces set to Window count, a Space not on screen draws no glyphs, so a click anywhere on it switches.

While a shortcut layer other than default is active, its icon — or two letters of its name when it has none — leads the bar, ahead of the Spaces (Shortcut Layers).

Wherever a screen shows no Space Bar at rest, the menu bar icon takes its place: the Space each screen is showing — its icon, or its name where it has none — with the active layer’s icon ahead of them, and the KiwiDesk logo, or the layer’s icon, back wherever the bar shows at rest.

The bar shows the Desktop you are looking at: a window on a macOS Desktop you are not looking at is not listed, and Hide empty Spaces hides a Space holding only those. Open or Focus still reaches such a window and switches Desktops to it.

A sticky window’s glyph is listed under one Space only, the one it renders on: a ∞ window under the Space you are focused on, a 📌 window under the current Space of its own screen.

MarkWhere it sitsMeans
∞ markOn the window, top-right cornerGlobal sticky — every Space of every screen
📌 markOn the window, top-right cornerScreen sticky — every Space of the one screen it lives on
Floating markOn the window, top-right corner — left of a sticky markA floating window
Badge, glyph’s top-leftSpace BarThat window (or one in the group) is sticky
Badge, glyph’s bottom-leftSpace BarThat window is floating
+n / count badge, glyph’s top-rightSpace BarHow many windows a grouped glyph holds

A window that floats only because its Space is in Floating layout wears no floating mark. Glyphs wearing the floating badge come last in their Space’s row. The badges have no Settings toggle; Lua hides them with space_bar.set_sticky_badge(false).

A window out of the pocket wears the pocket mark half over its top-right corner, in place of its sticky and floating marks, and its glyph wears the pocket badge on the bottom-left, in place of the floating badge; that glyph comes last in its row too.

Drag a window onto a Space to move it there:

  • Flick and drop — release before the ring fills; the window jumps there and you stay.
  • Hold to place — pause for the Spring delay; the view springs to that Space with the window in its live layout, so you can drop it exactly where you want. Move the cursor off the item before the ring completes to cancel. Under Reduce Motion the ring does not sweep: it stays away for the first half-second, then appears whole for the rest of the hold.

Dropping onto the Space a window is already on does nothing.

While dragging, hold over a bar’s faded end to autoscroll a bar that overflows.

The card at the top of the Profiles page answers for your machine now — “Right now: 2 screens → Desk (it holds this screen setup)” — naming which rung resolved it; its ? lists the rungs.

The rungs, in order:

  1. A Desktop binding on the Desktop your main screen is on (macOS Desktops), when a bound profile is saved for this many screens. With none the binding stands aside and the rungs below decide.
  2. An exact screen match — these exact screens. It stops matching the moment you swap one out, unless you saved a set for the new hardware too.
  3. The default for this screen count, whatever screens are plugged in. The first profile saved for a count becomes its default; make default moves it.
  4. A built-in Standard for that screen count, or a line saying nothing matches.

A new hardware combination therefore uses the Standard and marks the profile dirty until you Save on it; a profile saved for two different screens sorts after your exact matches but before one-screen profiles.

The dropdown at the top of any section picks what your edits target. Each profile is listed once: on top what is on screen — the loaded profile, or the Standard or temporary layout in its place — then every other profile, under a For N screens heading per screen count when there is more than one.

  • The top row: saving adopts your changes into the loaded profile.
  • Another profile, without switching: Home becomes profile-scoped, the General card leaves the grid, and Save writes to that profile’s JSON. Each App Rule and shortcut says which profiles it applies to.

Saving a profile that isn’t loaded leaves the running layout alone until it next loads, except for an App Rule or shortcut it shares with the loaded profile (Per-Profile Space Assignments). Save a copy… while editing a stored profile duplicates it with your pending edits, without touching the running layout.

The shared look is the other exception: saving a profile that uses it changes it at once wherever it is worn, the running layout included. A copy of such a profile whose look you changed keeps that look as its own.

Save writes to the current target and adds or refreshes the connected screen setup; it is greyed when the connected screen count differs from the profile’s. A screen setup the profile has no set for is itself an unsaved change, listed as a Screens row. On a temporary layout or a built-in Standard the slot reads Save as New Profile….

Loading a profile saved for this many screens moves the connected screen setup to it from any other profile, so the next time these screens connect, it loads. Saving adds the connected screen setup only when no other profile has it.

While management is paused (Accessibility off), KiwiDesk detects no screens, so any save that captures the live screen setup is unavailable. Shortcuts, app rules, float and ignore rules, the space list and Desktop bindings carry no screen setup, so Save still writes gui.json for them and keeps counting the layout edits until you grant access.

The same holds before you press Start Tiling for the first time: until then KiwiDesk detects no screens either.

Neither live save carries a keybinding override: to give a profile its own shortcuts, pick it in the banner while it isn’t loaded and edit its Shortcuts & Gestures page.

One workflow layout per screen count is the Standard that resolves silently when no saved profile matches; the starter setup is derived from the screens you have (Your first run) and is offered for that count alone. Where a preset does not name a layout for a space, the space takes the layout its screen suits. Applying a preset saves it as a real, editable profile; presets cannot be deleted.

The title match is case-sensitive, and “Info” also catches “Information”; the live window list under the chips shows what the rule as written would float. Dialogs, sheets and picture-in-picture windows float whatever a rule says, as do windows of apps that remain accessory processes. For an app you have not installed, app_rules in Lua takes bundle identifiers (Finding a bundle identifier).

Apps with macOS native tabs (Finder, Terminal, Ghostty) are one tile per window that follows the active tab. Tabs cannot be split into separate tiles.

An ignore rule makes KiwiDesk treat every window of an app as nonexistent — no space assignment, no window events — for HUDs, menu-bar utilities, and apps that misbehave when AX-tracked. There is no Settings control: add bundle ids to ignore_rules = { ... } in init.lua, or to the root ignore_rules array in gui.json (ignore_rules). An ignore rule also opts a misbehaving tabbed app out.

An App Rules row’s Applies to menu picks the profiles its rule reaches: All profiles keeps the rule in gui.json, and otherwise a ticked profile whose value differs from the shared one stores it in its own JSON. A new value reaches only the ticked profiles; the others keep theirs, and the trash removes a rule. Removing a shared rule from one profile stores a null there.

A profile stores a sparse diff: app_rules maps apps to spaces, while float_rules and ignore_rules are objects whose true entries add rules and null entries remove inherited ones:

{
"app_rules": { "com.apple.mail": null },
"float_rules": {
"com.apple.systempreferences": null,
"com.apple.finder:Get Info": true
},
"ignore_rules": {
"eu.exelban.Stats": true,
"com.example.inherited": null
}
}

An absent entry inherits. Effective Ignore applies last as the hard gate. Ignore has no GUI control; Settings preserves the hidden object across saves, copies and renames.

Every shortcut lives in a layer; only the active layer’s bindings fire. (“Layer”, not “mode”: mode names a space’s layout.)

Upgrading and every shortcut is gone? See The gui.json File for the "modes" → "layers" fix, before opening Settings.

A fresh install seeds a setup chosen for the screens you have. Every screen opens in scrolling, except the smallest, which opens in monocle.

The slot is 85% of the screen; on an ultrawide or super ultrawide it is 30%.

Your screenGets, best first
Laptop (under 1900 pt wide)scrolling · monocle
2K / 4K desktop (1900–3000 pt)grid · stack · bsp · scrolling
Ultrawide (3000 pt +, or 2.1:1 up to 3:1)stack · track · grid
Super ultrawide (3:1 and wider, e.g. 32:9)stack · track · grid
Portrait (taller than wide)stack · grid · monocle

Screens are measured in points, so a 5K 27” and a 1440p 27” get the same answer. Every setup gets exactly one Floating space, on the largest screen. The total is 3 spaces for one screen, 5 for two, 7 for three, then 8, 9 and one more per screen up to ten, each screen’s share proportional to its width. “Smallest” is read from width alone, so a 27” beside an ultrawide opens in monocle.

Each layout is tuned for the screen it sits on, scrolling for the widest screen that opens in it, and the minimum window size follows the main screen — larger on an ultrawide or super ultrawide main. A stack gets two mains side by side on an ultrawide and three on a super ultrawide; on a portrait screen its stack runs along the bottom. Scrolling on an ultrawide or super ultrawide centres the focused window and keeps a lone window at its slot size, and each scrolling space scrolls the way its own screen faces — vertically on a portrait screen.

With an ultrawide beside other screens, scrolling lives on the ultrawide alone: every other screen opens in monocle, and each screen gets three spaces ending in its own floating one — a widescreen monocle · bsp · floating, a portrait screen monocle · grid (one column of three) · floating, a laptop monocle · grid (two by two) · floating.

While you are still on the starter setup, connecting or removing a screen re-derives it and the ⌃⌥N space shortcuts extend to new spaces (up to ten).

It is saved as an ordinary profile named after your main screen — Ultrawide, say, or Widescreen + 1 across two screens — and the same setup is always available as a preset of that name.

ActionShortcut
Focus window left / down / up / right⌃⌥← ⌃⌥↓ ⌃⌥↑ ⌃⌥→
Go to space⌃⌥1 … ⌃⌥9, then ⌃⌥0 for the tenth
Go to previous / next space⌃⌥⌘← / ⌃⌥⌘→
Go back / forward in space history⌥⌘Tab / ⌥⌘⇧Tab
Move to space⌃⌥⇧1 … ⌃⌥⇧9, ⌃⌥⇧0
Swap with window left / down / up / right⌃⌥⇧← ⌃⌥⇧↓ ⌃⌥⇧↑ ⌃⌥⇧→
Move to space and follow⌃⌥⌘1 … ⌃⌥⌘9, ⌃⌥⌘0
Grow / Shrink width⌥⌘2 / ⌥⌘1
Grow / Shrink height⌥⌘5 / ⌥⌘4
Toggle floating⌃⌥F
Toggle sticky everywhere⌃⌥S
Toggle KiwiVision⌃⌥V
Next / previous layout⌃⌥C / ⌃⌥⇧C
Reset the space’s sizes⌃⌥R
Maximize the window⌃⌥M
Pocket this window⌃⌥P
Show / hide Space Bar, App Bar⌃⌥G, ⌃⌥H
Window switcher / view picker⌃⌥J / ⌃⌥L
Show shortcuts panel⌃⌥K

Open Settings ships on ⌃⌥, as well.

⌃⌥ moves your focus, ⇧ acts on the window — swaps it or sends it to a space — and ⌘ steps through the spaces or sends the window and follows. Resizing has its own layer, ⌥⌘, which also carries the space history on Tab, ⇧ stepping it the other way as it does everywhere on macOS. A space past the tenth, or one with a name, has no digit: the space steps and the history reach it, and Shortcuts & Gestures can give it a key of its own.

⌃⌥C moves the focused space to its next layout (cycle_mode) and ⌃⌥⇧C to the previous one, ⇧ stepping back as it does on Tab. ⌃⌥J, ⌃⌥K and ⌃⌥L open KiwiHub (kiwihub.show), one panel with three sides, listed together under Shortcuts & Gestures ▸ KiwiHub: ⌃⌥J the window switcher, ⌃⌥K the shortcuts panel, where typing searches your shortcuts, and ⌃⌥L the view picker, which switches the layout of the space under the pointer, the profile and KiwiVision. Tab and ⇧Tab step through the three sides.

⌃⌥R returns the focused space’s sizes to what your profile set (reset_layout_sizing), listed under Shortcuts & Gestures ▸ Size & float.

⌃⌥G shows or hides the Space Bar and ⌃⌥H the App Bar (space_bar.toggle_reveal, app_bar.toggle_reveal), listed under Shortcuts & Gestures ▸ Show & hide ▸ KiwiShelf.

⌃⌥M makes the focused window fill its screen and hides the bars there, until you press it again or move focus to another window (toggle_maximized), listed under Shortcuts & Gestures ▸ Size & float.

⌃⌥⇧P is Toggle Pocket, and ⌃⌥P is Pocket or take out, under Shortcuts & Gestures ▸ Pocket (toggle_pocket_window, pocket). A pocketed window leaves the screen and every bar, and comes out on the screen you are on, floating on every Space. ⌘-Tab to its app takes it out too, and KiwiHub’s window switcher lists it under Pocket.

⌃⌥⇧⌘1 … ⌃⌥⇧⌘9, ⌃⌥⇧⌘0 go to that macOS Desktop. Each Desktop gets its key the first time KiwiDesk sees it, so a key you delete stays deleted.

With Shortcuts use set to ✦, these defaults sit on Hyper: ✦ takes the place of ⌃⌥, and Go to Desktop takes ✦⌥ + digit. New spaces, new Desktops and later defaults land there too.

Each space digit is bound by name and follows a rename; spaces past the tenth ship without a digit. The set is seeded only while no shortcut is bound anywhere. An install that already has shortcuts gets a later default once, at launch, where its action has no shortcut and its key is free; Restore Defaults… puts back the whole set, keeping the shortcuts you made yourself except one sitting on a key a default needs; the confirmation counts those.

Three things can claim a chord, and they do not lose it the same way:

  • macOS wins outright: when a combo is one of its switched-on shortcuts, the row never fires.
  • KiwiDesk wins against an app’s menu shortcut; the app loses the accelerator but keeps the menu item. A shortcut with no menu behind it (⌥⌘ arrows are next/previous tab in most browsers and terminals) costs the capability outright.

⌃⌥ is the quiet corner macOS and most apps leave alone. ⌥ alone types characters that vary by layout (⌥L is ¬ on a US keyboard, @ on a German one); adding ⌃ or ⌘ stops that. Arrows and digits keep their place on every layout; letters do not.

The ⚠️ on a row means it duplicates another row in the same layer or collides with a macOS shortcut. Those two are the whole of it: KiwiDesk cannot see what other apps have bound, so the safest test of a chord is to bind it and open the app you would miss it in. A collision with a switched-off macOS shortcut (Zoom, Invert Colors) keeps the ⚠️ but does not count toward the banner. A collision with a shortcut every app carries (⌘W, ⌘Q, ⌘H, ⌘M) means your row works and every app loses that item while it is bound.

The recorder suspends your KiwiDesk shortcuts while it is open, so a combo already bound to a window action is recorded instead of firing.

A recording, a clear or a deleted row takes effect when you Save, like every other setting; Revert discards it.

The spelling a stored combo uses (gui.json, Lua):

Modifiers: cmd/command, alt/opt/option, ctrl/control, shift, and hyper (✦, live once a Hyper key is set; see set_hyper_key).

Keys: letters (a–z), digits (0–9), arrows, home, end, pageup, pagedown, space, return, tab, escape, f1–f12, and punctuation by symbol or name: ;/semicolon, ,/comma, ./period, //slash, \/backslash, -/minus, =/equal, [/leftbracket, ]/rightbracket, `/grave/backtick, '/quote/apostrophe.

The numeric keypad’s digits are the same keys as the number row. Its other keys are separate: keypadplus, keypadminus, keypadmultiply, keypaddivide, keypaddecimal, keypadequals, keypadenter, keypadclear. A third-party PC keypad sends digits only while Num Lock is on.

A combo is one set of modifiers plus exactly one key; multi-key chords like cmd+j+k are not supported — use layers. KiwiDesk binds the physical key, so switching input sources changes what the caps print, not which key fires.

  • macOS Desktop rows sit in their own macOS Desktops section, one per Desktop you have, on every screen: Go to Desktop ships bound, and the moves, under Move windows to a macOS Desktop, ship unbound. They appear only where macOS exposes the window-management bridge (macOS Desktops). A bound row whose Desktop leaves with its screen stays in place, dimmed, and works again when the screen is back; a Desktop shortcut for an absent Desktop does nothing, while a Space shortcut for an absent Space still works: it recreates the Space and switches to it. Each row sends the window to the Desktop and nothing more; naming the Space it lands in is a Lua/CLI argument, move_to_desktop(3, "mail") (Lua reference).
  • Resize — a window outside tiling (floated by you, or in a Floating space) resizes its own frame in any layout. Held, a resize shortcut glides: the first press is one step; after your Mac’s key-repeat delay the window resizes smoothly, faster over the next couple of seconds, scaled to your resize step. Only resize glides. A floating window grows around its centre; an edge against the screen edge or a bar stays put and the whole step goes to the other side. Floats are held clear of a bar and the screen edge by the focus ring’s width.
  • Applications — Open or Focus focuses a running app’s window or launches the app, and pressing again cycles its windows; which window each press reaches is in focus_or_spawn. Add the same app twice to bind one shortcut per behaviour.

The verb behind Open or Focus is now focus_or_spawn, formerly pull_or_spawn; shortcuts made here are updated for you.

A window that fills its whole screen — a slide show, a borderless-fullscreen game or player — is not held clear of the bars: it stays where its app put it, and both bars hide while it is in front (limitations).

If init.lua holds custom keybindings, Import from init.lua… (in the Shortcuts & Gestures header) reads them for review before you Save; each binding must be an inline function() … end on one line. Adopt into the GUI imports the whole file’s managed settings and keeps your custom Lua live.

Import and Adopt bring in one key per action: a key is left out when its action already has one, on the Shortcuts page or earlier in init.lua — a Space keeps its own digit where it has one — and the Shortcuts header names each key left out.

The + beside the layer chips defines a layer: a name, an optional icon, and bindings that shadow the base shortcuts while it is active. Every layer gets its own ⌃⌥K row. Switch layers with KiwiDesk.switch_layer (Lua reference).

Each binding’s Applies to says which profiles use it: All profiles, or the profiles ticked. Remove from one profile turns a shared shortcut off there and keeps it everywhere else, and a profile can put a shared shortcut on another key of its own. Ticking a profile that uses the key for something else gives the key to this shortcut there.

A new layer also carries the ⌃⌥, Open Settings row.

Profiles ▸ Profiles per macOS Desktop lists the Desktops on your main screen by their Mission Control number, counted across every screen, so they need not start at 1. A Desktop you bound that is not on your main screen carries a not on main screen badge and cannot fire until a screen change makes it the main screen’s; a Desktop that is not there at all carries not present (Accepted limitations). A binding stays on the Desktop you gave it when Mission Control renumbers — A binding follows its Desktop, not its number.

Under a row, the screen its Desktop is on — for a not present row, the screen it was last seen on.

With profiles saved for more than one screen count, the card groups its rows by count, and a pick in a group binds a profile for that count alone (a binding fires only for its profile’s screen count). A bound name no saved profile carries sits in a last Couldn’t load group; None clears it.

A binding fires when its Desktop becomes current on your main screen (the one with the menu bar) and it holds a profile saved for your screen count (Which Profile Loads). With “Displays have separate Spaces” on, macOS’s default, each screen switches on its own: a swipe on the main screen switches profiles, a swipe on a secondary never does.

A Save while editing a stored profile keeps a binding change too.

flowchart TD
    S["Your main screen switches<br/>to a macOS Desktop"] --> B{"A profile bound<br/>to this Desktop<br/>for these screens?"}
    B -->|"Yes"| A["That profile activates —<br/>its layout, gaps, and rules"]
    B -->|"No binding, or<br/>not for these screens"| K["The current profile<br/>stays active"]
    P["You pick a profile<br/>by shortcut or menu"] --> A

A hand-written config declares bindings in init.lua with bind_profile_to_desktop. Each Desktop remembers which space it was on, on every screen, and your main screen’s Desktops remember which window you had focused. A window that takes more than a few seconds to come back keeps whatever macOS focused (Accepted limitations).

Where macOS exposes its window-management bridge (present on macOS 26.6.1, checked 2026-08-18), KiwiDesk can also drive Desktops: focus_desktop(n), move_to_desktop(n) and move_to_desktop_and_follow(n) (Lua reference). Where it is absent the commands do nothing and log why; KiwiDesk never asks you to turn SIP off.

For Lua configuration, see lua-reference.md; for integration recipes (sketchybar, external commands, …), recipes; for the CLI, cli.md.

To check your current state in raw form:

kiwidesk get_state
kiwidesk get_profile_status

To reload your config after editing init.lua by hand:

kiwidesk reload_config

Accessibility permission missing?
Window management pauses: the menu bar icon shows a warning triangle and the quick menu’s Window Management Paused… row reopens the permission tour. Add KiwiDesk under System Settings › Privacy & Security › Accessibility; management resumes on its own.

Permission granted, but nothing is tiled?
Granting Accessibility does not start tiling on its own: the first time, press Start Tiling — in the permission tour, the quick menu or the banner at the top of Settings.

Settings window won’t open, or KiwiDesk seems stuck?
Run kiwidesk service restart in a terminal, or quit and reopen KiwiDesk. Launching KiwiDesk while a copy is already running never starts a second one: the second launch brings the running instance forward and exits with already running.

Windows come back in the wrong spaces after a restart or wake?
General ▸ Files & Reset ▸ Saved window arrangement ▸ Discard clears the remembered arrangement without changing settings. All settings ▸ Reset… below it is the last resort: it deletes every profile, your spaces, layouts and keybindings and reseeds the defaults, keeping init.lua, your palettes, the display language, the login item and onboarding; the old files go to the Trash.

Reporting a bug?
General ▸ Files & Reset ▸ Log: pick a time range and press Export… to save KiwiDesk’s log for it to a file. Make the problem happen again first: the range counts back from now, and “since KiwiDesk started” means this run. The file names the apps and windows KiwiDesk managed, so glance through it before attaching it. The terminal form is in the CLI reference.

Shortcut not working?
Check Shortcuts for a ⚠️ marker (Conflict Detection). If you hand-edited, run kiwidesk reload_config.

Typo in init.lua?
A misspelled function name (scroll.set_width for scroll.set_slot_size) does not abort the config: the call is skipped with a did-you-mean hint and the rest still runs. Every typo is listed under menu bar › Config Issues….

Windows aren’t tiling?
Ensure the space’s layout is not Floating and the app is not in float_rules. On a hand-written config, make sure init.lua exists and the app is managing tiling (check the banner).

Profile not loading after a screen change?
See Which Profile Loads: a new hardware combination uses the built-in Standard until you Save on it.

© 2026 KiwiCanopy