material-shell's spatial desktop, on the Mac. A tiling window manager for macOS where every window has one address, you move by direction, and the shell remembers where you put things.
Get started · Install · Build · Contributing · Docs site · Keybindings · Config · spacialctl
Live capture: Fn+Space through the layouts, Fn+D/Fn+A along the row, Fn+W/Fn+S between rows.
brew install --cask askalice/tools/spacialshellLaunch it, grant Accessibility when asked, and press Fn+S. Hold Fn on its own for a cheat
sheet of every binding. More in Install.
Your brain is already a very good window manager, if you give it a place to work with. Spatial memory is how you know where the mugs are in your own kitchen; mental mapping is how you plan a route through a place you know. A pile of overlapping windows gives neither anything to hold on to. A stable grid gives them everything.
So SpacialShell lays your apps out in two dimensions — every workspace is a row, every window a
cell — grouped by use case: browsers on one row, editor and terminal on the next, media below. It
remembers that arrangement as you use it, with nothing to configure. You move by direction,
like in a game world: Fn+W/Fn+S between rows, Fn+A/Fn+D along one. And the rail and tab
bar show the whole map at a glance, so the mouse works as well as the keyboard. Finding a window
stops being a search and becomes wayfinding.
This is the paradigm of GNOME's material-shell and its successor Veshell — a "not-desktop" you inhabit rather than tidy. SpacialShell is that lineage rebuilt for macOS, not Linux: not a port and no shared code, under the same GPL-3.0.
Both of those projects own the compositor. On a Mac nobody does, which shapes everything here:
- The public Accessibility API instead of a compositor — the same interface screen readers use.
- Screenshot-proxy animations — switches slide pictures of the windows, then move the real ones once, because macOS has no hooks for animating another app's window.
- Beside Spaces, fullscreen and the menu bar, not replacing them; an app can't drag you out of a fullscreen Space by stealing focus.
- No private APIs, SIP stays on — nothing to reinstall after a macOS update.
- Notarised, outside the App Store — the App Store sandbox forbids the control a tiler needs.
- Native look — vibrancy materials, your accent colour, light/dark from System Settings.
Recorded in the test VM (one 1024×768 display) while a scenario drives the real shell through
spacialctl and synthetic input: Scripts/e2e/e2e.sh --vm --suite media, then
Scripts/e2e/media.sh. Multi-display features aren't in these; they need a real multi-monitor Mac.
The rail with real apps, each in the row its category sends it to; hover cards with live previews.
A tab dragged along the bar, then onto a workspace in the rail, which moves its window there.
Settings, opened with Fn+,: General, Appearance, Layout, Workspaces, Keybindings.
Live capture: one row per workspace (web, terminal, notes, two TextEdit windows); Fn+S / Fn+W between rows, Fn+D / Fn+A along a row.
A workspace is a row; an application window is a cell. New windows append to the current
row, new workspaces append underneath. Up/down changes workspace, left/right changes
window. The screen is a viewport over a larger, always-sorted grid, and the spatial view shows
that grid: Fn+Z, or holding Fn+W / Fn+S, zooms out to every workspace as a mini-desktop,
drawn from the model, with the camera sliding between rows as you move.
- Single address. Every managed window lives in exactly one workspace of exactly one display.
- There's always a way down. Each display's stack ends with one empty workspace; empty rows in the middle disappear on their own.
- Per-display stacks. Every display has its own rail; a window (
Fn+⇧+arrows) or a whole workspace (Fn+⌥⇧+arrows) can move to the display that way. - Categories are identity. The rail shows what each row is — web, terminal, coding, media —
and
category-ordergives each category its own row, so a new browser window lands with the other browsers. - Placement memory. Windows go back to their workspace and display across restarts (displays matched by UUID) — into their own tab slot: a window whose app has not reopened it waits as a placeholder tab that opens the app when clicked — and nothing is ever left invisible: every window the shell lists is one click away, and parked windows are recovered after a crash.
Live capture: Fn+Space through maximize, split, column, half and grid, four windows in one row.
- The rail (left): one row per workspace with its apps and category, hover previews, a tray for hidden and minimized windows, right-click menus (quit an app, set a row's category), scroll to switch, and Dock-style auto-hide.
- The tab bar (top): a tab per window, drag to reorder or onto a rail row, right-click for Close / Float / Move to workspace, middle-click to close; plus the layout switcher.
- Layouts:
Fn+Spacecycles maximize · split (an N-column sliding view) · column · half · grid. Draw your own in the layout editor or as[[layout]]blocks. - Resize: drag a border, or
Fn+⌃A/D/W/Sin 5% steps that stop on 25/50/75%;Fn+⌃=balances. Sizes are kept per workspace and per layout. - Hold
Fnfor the cheat sheet,Fn+Tabfor the overview,Fn+Escfor Zen mode.
Fn/Globe is the default modifier; ⌃⌥ is the preset for keyboards without a Globe key
(keybinding-preset = "ctrl-alt"). The grammar: Fn navigates, +⇧ moves the window, +⌃
resizes, +⌥ reaches displays or the whole app.
| Command | fn preset |
ctrl-alt preset |
|---|---|---|
| Focus workspace up / down | Fn+W / Fn+S |
⌃⌥W / ⌃⌥S |
| Focus window left / right | Fn+A / Fn+D |
⌃⌥A / ⌃⌥D |
| Focus workspace 1…10 (again to go back) | Fn+1 … Fn+0 |
⌃⌥1 … ⌃⌥0 |
| Focus tab 1…9 | Fn+⌥1 … Fn+⌥9 |
— |
| Move window left / right | Fn+⇧A / Fn+⇧D |
⌃⌥⇧A / ⌃⌥⇧D |
| Move window to workspace up / down / N | Fn+⇧W / Fn+⇧S / Fn+⇧1…0 |
⌃⌥⇧W / ⌃⌥⇧S / — |
| Move every window of the app up / down | Fn+⌥⇧W / Fn+⌥⇧S |
— |
| Resize narrower / wider / shorter / taller | Fn+⌃A / D / W / S |
⌃⌥⌘A / D / W / S |
| Balance sizes | Fn+⌃= |
⌃⌥⌘= |
| Focus display that way | Fn+⌥W/A/S/D |
— |
| Move window / workspace to display that way | Fn+⇧+arrows / Fn+⌥⇧+arrows |
— |
| Focus / move to display prev, next | Fn+[ Fn+] / Fn+⇧[ Fn+⇧] |
⌃⌥[ ⌃⌥] / ⌃⌥⇧[ ⌃⌥⇧] |
| Cycle layout / backwards | Fn+Space / Fn+⇧Space |
⌃⌥Space / ⌃⌥⇧Space |
| Toggle float | Fn+G |
⌃⌥G |
| Close focused window | Fn+Q |
⌃⌥Q |
| Zen mode / overview / config file | Fn+Esc / Fn+Tab / Fn+, |
⌃⌥Esc / ⌃⌥Tab / ⌃⌥, |
Fn+F is deliberately unbound — Globe+F is Apple's full-screen shortcut. Every chord can be
rebound in config.toml or Settings; see docs/keybindings.md.
brew install --cask askalice/tools/spacialshell, or download the notarised DMG from Releases and moveSpacialShell.appto/Applications. Keep it where you put it: moving it invalidates the Accessibility grant.- Launch it. It asks for Accessibility and opens System Settings → Privacy & Security → Accessibility; tick SpacialShell and it continues on its own.
- Optional: hover previews and sliding switches need Screen Recording; the hover card offers it when it matters. Restart SpacialShell after granting it. Without it, switches are instant and everything else works.
- Quit from the rail's app menu. Quitting restores every managed window to the centre of its screen and saves your layout.
Requires macOS 14+ and a Swift 6 toolchain (Xcode 16 or later).
git clone https://github.com/AskAlice/SpacialShell-MacOS.git && cd SpacialShell-MacOS
swift test # the gate for every change
Scripts/dev.sh # build debug and run in the foreground
Scripts/bundle.sh # build/SpacialShell.app, signed with a stable identityScripts/dev.sh runs the raw binary, whose Accessibility grant is tied to its path; see
Scripts/README for the TCC details and SPACIAL_LOG_KEYS=1 for hotkey
debugging. make hooks installs the pre-commit hook. CONTRIBUTING.md has the
rest.
- One native macOS Space per display. Workspaces are emulated by parking inactive windows, not
by native Spaces, so Mission Control and
⌘Tabcan still see parked windows. - Non-Apple keyboards never send
Fn. Use thectrl-altpreset or Karabiner-Elements. - Some
Fn+letterchords collide with macOS's own shortcuts. The keyboard hook runs at the HID level to win that race; seedocs/keybindings.mdfor known conflicts.
SpacialShell is licensed under the GNU General Public License v3.0 (GPL-3.0) — see LICENSE.
Its Accessibility/platform layer adapts MIT-licensed code from
AeroSpace (Copyright (c) 2023 Nikita Bobko); the licence
ships in legal/third-party/LICENSE-AeroSpace.txt, every adapted file carries an attribution
header, and NOTICE lists them all. The spatial paradigm is design inspiration from
material-shell and
Veshell (both GPL-3, like SpacialShell); no code from
either was used.
The Raycast extension in raycast/ is MIT-licensed, as the Raycast Store requires of every
extension.





