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:
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.
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.)
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.
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.
Mark
Where it sits
Means
∞ mark
On the window, top-right corner
Global sticky — every Space of every screen
📌 mark
On the window, top-right corner
Screen sticky — every Space of the one screen it lives on
Floating mark
On the window, top-right corner — left of a sticky mark
A floating window
Badge, glyph’s top-left
Space Bar
That window (or one in the group) is sticky
Badge, glyph’s bottom-left
Space Bar
That window is floating
+n / count badge, glyph’s top-right
Space Bar
How 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:
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.
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.
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.
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.
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 screen
Gets, 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.
⌃⌥ 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.
⌃⌥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.
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.
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.