Files
lime/docs/xephyr-demo.md
2026-10-02 14:14:47 -05:00

687 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Xephyr demo
`lime/x11` is Lime's first deliberately small adapter from the pure
Activity/View/Presentation/Lease model to real X11 operations. It exists to
make the model visible and testable early. It is not a complete X11 window
manager yet.
## Run it safely
Use a clean nested X server. Do not run the prototype on the same root window
as an existing window manager: X11 allows only one client to select
`SubstructureRedirect` on a root window.
```sh
Xephyr :99 -ac -screen 1000x700 -br -nolisten tcp
```
In a second terminal, in the repository root, start Lime in a Lisp image with
Quicklisp, CLX, Babel, and CFFI available. Install the native Pango/Cairo
libraries (including PangoCairo) and your desired fonts first:
```lisp
(ql:quickload '(:clx :babel :cffi))
(asdf:load-system "lime/x11")
(lime.x11:run-demo :display-name ":99")
```
For a user-owned configuration file instead of the starter map, load the same
system and start through `lime.config:start-lime`:
```lisp
(lime.config:start-lime
:display-name ":99"
:config-file #P"/home/adan/.config/lime/config.lisp")
```
The configuration file interface, command registry, reload behavior, and
initial floating rules are documented in [Configuration](configuration.md).
In a third terminal, create some clients:
```sh
DISPLAY=:99 xterm
DISPLAY=:99 xterm
```
For two clients the fallback layout divides the 1000px-wide server into two
500px columns below Lime's default 22px Head bar. When either client exits,
the other immediately occupies the full usable width. This was exercised
against Xephyr during implementation.
## Read the first Head bar
Every Head owns a small persistent top bar. Its initial left-to-right shape is
roughly:
```text
Lime | chill work code | []= focused window title music | volume | clock
```
The local Activity is highlighted. Saved Views are checkpoints recalled from
the menu, not nested workspaces, so there is no View/Draft/dirty suffix. `[]=` is
master/stack, `|M|` is the retained manual tree, and `[N]` is monocle with `N`
tiled clients in its stack, including the visible one. Hidden Activity clients
and clients leased elsewhere are excluded. Individual floats do
not change that token; Lime has no fake all-floating layout. The expanding,
left-aligned title section has quiet one-pixel edges on both sides. Its
background and text colors identify the focused Head, without a center
underline or a redundant "left Head" or "right Head" label.
The fields use actual font measurements rather than fixed-width columns.
The left fields use measured, contrasting ` | ` separators; long names,
titles, and status text are shortened to fit their pixel budgets. Click
targets follow the displayed text, excluding status separators and ellipses.
The right-hand music/volume/clock above are illustrative providers, not built-in
integrations. Bar colors, including optional title edges, live in `(:bar ...)`.
Titles prefer UTF-8 `_NET_WM_NAME`, then Latin-1 `WM_NAME`, then the client
class/name or "Untitled window". Property notifications refresh the bar
immediately. The shared Pango/Cairo renderer preserves Unicode, shapes text,
and uses system font fallback, including color emoji when installed. Configure
both the bar and dropdown with `(:font :family "monospace" :size 14 :weight
:normal)`; size is in pixels. Long text is shortened at grapheme boundaries,
not in the middle of an accented character or joined emoji. See
[font configuration](configuration.md#fonts) for family lists and live reload.
Normal tiles and floats use the client work area below the bar. A real EWMH
fullscreen client takes the whole physical Head and hides its bar. Click
`Lime` to open the same Activity/View palette as `Super-y`; click the layout token to cycle
master/stack and monocle. Manual remains available through Lisp and saved Views,
but is hidden from the everyday cycle and layout menu. The palette is a dropdown below the bar, so
the current Activity stays visible while searching.
The right side has no built-in shell commands. Configure optional status
providers in Lisp instead:
```lisp
(lime.x11:backend-set-head-bar-status-providers
*lime*
(list (lambda (backend head-id)
(declare (ignore backend head-id))
(lime.x11:make-head-bar-segment :id :clock :text "12:34"))))
```
Each provider receives `(backend head-id)` and may return one segment or a
list. A segment can also carry a `:click-action` function of those same two
arguments.
## Exercise Activities and Views from the REPL
The first control surface is intentionally Lisp-first. Start the backend in
the REPL and give its event loop a background SBCL thread:
```lisp
(defparameter *lime* (lime.x11:start-backend :display-name ":99"))
(defparameter *lime-event-thread*
(sb-thread:make-thread (lambda () (lime.x11:run-event-loop *lime*))))
```
Now the initial task workflow is directly usable:
```lisp
;; 1. A fresh named task with a blank unsaved live draft.
(lime.x11:backend-start-activity *lime* "wm" "WM project")
;; 2. Launch clients with DISPLAY=:99; they enter the WM project draft.
;; 3. Capture the current arrangement as a checkpoint.
(lime.x11:backend-save-view *lime* "wm-code" "Code")
;; 4. Arrange through the model, then capture a second checkpoint.
(lime.x11:backend-set-layout-root *lime* my-debug-layout)
(lime.x11:backend-save-view *lime* "wm-debug" "Debug")
;; 5. Recall either View. The selected policy is explicit.
(lime.x11:backend-activate-view *lime* "wm-code" :policy :strict)
(lime.x11:backend-activate-view *lime* "wm-debug" :policy :partial)
;; 6. Inspect the active Head without digging through structures.
(lime.x11:backend-status *lime*)
```
`backend-update-view` deliberately overwrites an existing checkpoint from the
live draft. `backend-resume-presentation` restores a cached draft by id; both
accept the same `:strict`, `:partial`, and `:take` collision policies where
they apply. Every transition applies the resulting Presentation to X11 before
returning.
If a REPL or future IPC client only has a human-facing name, these helpers make
the same palette workflow available without hand-writing an id:
```lisp
(lime.x11:backend-start-named-activity *lime* "WM project")
(lime.x11:backend-save-named-view *lime* "Code")
```
They derive readable unique ids and make the saved View current, so the normal
`backend-update-current-view` command follows the new checkpoint.
## Capture and recall a whole-desk Viewset
A Viewset is the explicit multi-Head companion to a single-Head View. It saves
the current `Head -> saved View` mapping, then later recalls every named target
as one atomic transition:
```lisp
;; Every selected Head must currently be mounted from a saved View. By default
;; this captures all configured Heads in display order.
(lime.x11:backend-save-named-viewset *lime* "Work desk")
;; Later, recall every mapped Head together.
(lime.x11:backend-activate-viewset *lime* "work-desk" :policy :strict)
```
`backend-save-viewset` accepts an explicit id/name and optional `:head-ids`
when you deliberately want a subset. It never serializes the live draft; save
the relevant View first if a Head has unsaved changes. Viewset recall uses the
same `:strict`, `:partial`, and `:take` vocabulary as View recall, but checks
the selected Heads as one outgoing group. That is what makes a saved left/right
swap work without briefly duplicating a View or lease. A client held by an
unselected Head can be left there (`:partial`) or deliberately taken (`:take`).
Two target Views that request the same client are rejected as an ambiguous
Viewset—Lime will not pick one based on ordering.
`bind-viewset-key` is the corresponding opt-in Lisp configuration helper:
```lisp
(lime.x11:bind-viewset-key *lime* #\w "work-desk")
```
The focused client is also saved on the live Presentation. Focus a particular
visible client or cycle its leased clients without exposing raw CLX calls:
```lisp
(lime.x11:backend-focus-client *lime* "x11-...")
(lime.x11:backend-focus-next-client *lime*)
(lime.x11:backend-focus-previous-client *lime*)
```
`backend-status` includes `:focus-client-id`. The renderer sends X input focus
to that registered visible client after it maps all windows; a blank selected
Head focuses the root window instead.
## Use built-in layouts and manual tree edits
Layout policy belongs to the active Head-local Presentation, so it is an
unsaved draft change until `backend-save-view` or `backend-update-view` is
called. The first built-ins are master/stack and monocle:
```lisp
;; First tiled client is the 60% left-hand master; the rest share the stack.
(lime.x11:backend-use-master-stack-layout
*lime* :master-count 1 :master-ratio 3/5 :orientation :horizontal)
;; Promote the selected Head's focused tiled client to the master slot.
(lime.x11:backend-promote-focused-client *lime*)
;; Full-Head focus mode. Other tiled clients remain leased but are unmapped.
(lime.x11:backend-use-monocle-layout *lime*)
(lime.x11:backend-focus-next-client *lime*) ; shows the next client in monocle
;; Return to the retained binary tree, then edit the focused client's nearest
;; split. These actions deliberately make the draft :MANUAL.
(lime.x11:backend-use-manual-layout *lime*)
(lime.x11:backend-orient-focused-split *lime* :vertical)
(lime.x11:backend-resize-focused-split *lime* 1/20)
(lime.x11:backend-swap-focused-client *lime* :direction :next)
```
`backend-status` exposes `:layout-mode` and `:layout-options` for UI/IPC
frontends. The optional starter map below covers everyday commands; the named
helpers add task and physical-output shortcuts without hard-coding them.
## Float a client and use Super-drag
Floating is placement state, not a second kind of client or lease. Toggle the
focused client, then optionally set an exact Head-relative rectangle:
```lisp
(lime.x11:backend-toggle-focused-client-floating *lime*)
;; All values are normalized fractions of the selected Head.
(lime.x11:backend-set-client-floating-geometry
*lime* "x11-..." :x 1/5 :y 1/6 :width 3/5 :height 2/3)
;; The same generic operation is useful to a future menu or IPC client.
(lime.x11:backend-toggle-client-floating *lime* "x11-...")
```
The renderer places visible floats above tiled content. `backend-status`
includes `:floating-client-ids`; save or update the active View to make a
float checkpoint durable.
On a floating client, hold Super (`Mod4`) and use Button1 to move or Button3
to resize. Super+Button2 toggles the clicked client's floating placement, the
same way as Super+Space. The adapter converts each root-pixel drag motion into
the same normalized geometry command above. Super-drag only claims floating
clients; on a tiled client it simply focuses the window and changes no geometry.
## Click to focus
An unmodified Button1 anywhere in a managed client's window tree selects the Head leasing it
and focuses that client, then the press is replayed to the application. Button1 on a blank part of the root selects the Head under the pointer
and focuses root, while retaining that Presentation's saved client-focus target
for later keyboard cycling or another client click. A background click is thus
not a View edit.
The initial pointer policy reserves Super+Button1/2/3 for float move/toggle/
resize. Menus, focus-follows-pointer, button rebinding, and robust Caps/Num-
Lock modifier normalization remain separate future policies.
## Inspect ICCCM client facts or close a client
`x11/icccm.lisp` tracks the conventional title, resource name/class,
`WM_PROTOCOLS`, InputHint, transient parent, and normal size hints for every
managed client. This data stays out of the Activity/View model and is available
to a future UI:
```lisp
(let ((client-id (first (getf (lime.x11:backend-status *lime*) :client-ids))))
(lime.x11:backend-client-icccm-info *lime* client-id))
;; Sends WM_DELETE_WINDOW when supported; only legacy clients use XKillClient.
(lime.x11:backend-close-focused-client *lime*)
```
Focus now honors ICCCM's InputHint and sends `WM_TAKE_FOCUS` when a client
advertises it. When Lime denies a tiled client's ConfigureRequest, it restores
the model geometry, adjusted to its cached normal hints, and sends that client
a synthetic ConfigureNotify response.
When a lease makes a client visible Lime publishes `WM_STATE` `NormalState`;
when a View hides it, Lime publishes `IconicState` while retaining the client
in its Activity. Those renderer-originated unmaps are tracked explicitly.
If the application later unmaps itself, Lime recognizes the distinct ICCCM
withdrawal, removes `WM_STATE`, and retires that client from its Views.
## Fullscreen and small EWMH compatibility
`Super-f` follows your dwm binding and toggles true fullscreen for the focused
client. It also responds to a client's `_NET_WM_STATE_FULLSCREEN` request,
including the initial state a game publishes before mapping. The fullscreen
window receives the entire rectangle of the Head holding its exclusive Lease,
is raised above the other windows on that Head, and becomes the input focus.
Normal size hints intentionally do not constrain that fullscreen rectangle.
Fullscreen is temporary EWMH adapter state, not a saved View edit. Switching
to a different View hides the game through its normal Lease; returning to the
View restores fullscreen on whichever Head legally holds that Lease. A second
fullscreen request on the same visible Head replaces the first. Lime also
advertises and handles `_NET_ACTIVE_WINDOW` for already visible clients and
`_NET_CLOSE_WINDOW`; it publishes `_NET_ACTIVE_WINDOW` while it controls
focus. Lime also creates the required `_NET_SUPPORTING_WM_CHECK` window and
keeps `_NET_CLIENT_LIST` plus `_NET_CLIENT_LIST_STACKING` current, so ordinary
desktop tools can identify the WM and enumerate its clients. It deliberately
does **not** implement fake numbered EWMH desktops.
`_NET_WM_WINDOW_TYPE_DIALOG`, `UTILITY`, and `SPLASH` receive the same
centered floating/focus behavior as a transient dialog when no
`WM_TRANSIENT_FOR` parent exists. `DOCK` and `DESKTOP` windows are mapped at
their application-supplied geometry but never become Activity/View task
clients. External dock struts are intentionally a later work-area pass.
## Dialogs and normal size hints
When a newly managed window declares `WM_TRANSIENT_FOR` for a client Lime is
already managing, it follows that parent’s active Presentation and Head rather
than the most recently selected Head. Lime adds it as a float, centers it over
the parent, and focuses it. The dialog is still a normal placement and lease,
so it disappears/restores with the same View rules as every other client.
Before every X11 configure, Lime applies the client’s `WM_NORMAL_HINTS`
minimum/maximum size, base-size, and resize increments. This includes tiling,
direct float geometry commands, and every Super-resize motion. The requested
Lime rectangle remains model state; the final client-compatible pixel rectangle
is adapter-local, so client constraints never mutate a saved View. If a client
demands more than the whole Head, Head containment wins.
## Exercise two independent Heads
On a normal X11 session, `start-backend` uses RandR automatically. Every active
CRTC becomes a Lime Head named after its representative output (for example,
`DP-1` and `DP-3`). Mirrored outputs sharing one CRTC deliberately become one
Head, so Lime never renders the same task rectangle twice. Inspect the live
hardware snapshot with:
```lisp
(lime.x11:backend-randr-status *lime*)
```
Xephyr normally exposes one virtual output, so explicit rectangles remain a
useful way to exercise two independent Heads in the nested demo. This
1000×700 server becomes a left and right 500×700 Head:
```lisp
(defparameter *lime*
(lime.x11:start-backend
:display-name ":99"
:heads
(list
(lime.x11:make-x11-head :head-id "left"
:x 0 :y 0 :width 500 :height 700)
(lime.x11:make-x11-head :head-id "right"
:x 500 :y 0 :width 500 :height 700))))
```
Each configured rectangle receives its own active Presentation: initially an
unsaved, blank `Scratch` draft rather than a fabricated empty View. A saved
View is never mounted on two Heads, while two Head-local drafts may show the
same Activity when their exclusive window leases do not collide. Set the
temporary routing target before opening a window for a different Head:
```lisp
;; Keep the existing Scratch task on the left.
(lime.x11:backend-start-activity *lime* "game" "Game" :head-id "right")
(lime.x11:backend-select-head *lime* "right")
;; A subsequent DISPLAY=:99 client joins Game on the right.
(lime.x11:backend-status *lime* :head-id "left")
(lime.x11:backend-status *lime* :head-id "right")
```
`backend-select-head` selects the routing target and applies that Head's
saved focus target as real X input focus. Head rectangles must be positive,
inside the X root, and non-overlapping. Explicit `:heads` always takes
precedence over discovery; pass `:head-discovery :none` to force the old
single-root fallback instead.
Lime subscribes to RandR screen/CRTC/output notifications for a discovered
startup. An event only refreshes the observed topology and marks it pending:
it will never silently decide where an Activity, View, or Lease should go when
a monitor is unplugged. `backend-refresh-randr-heads` safely applies a
resolution/position change when the same Head ids remain; an added or removed
Head returns `:reason :topology-changed`. For that case, keep observation,
choice, and mutation separate:
```lisp
;; Read-only data for a future palette, IPC client, or REPL inspection.
(defparameter *randr-change*
(lime.x11:backend-randr-topology-report *lime*))
(getf *randr-change* :removed-active-sources)
;; Example after DP-3 disappears: keep its exact live draft on DP-1.
;; Omit a removed source from :MOVES to suspend its draft and release leases.
(defparameter *randr-plan*
(lime.x11:backend-build-randr-topology-plan
*lime*
:moves
(list (lime.core:make-head-topology-move
:from-head-id "DP-3" :to-head-id "DP-1"))))
;; Rereads RandR, rejects a physically stale plan, then renders the result.
(lime.x11:backend-apply-randr-topology-plan *lime* *randr-plan*)
```
An added Head without an incoming move is blank. Select it, then use the
ordinary palette or `backend-start-named-activity` / `backend-activate-view`
commands to put the task you actually want there.
## Watch a cross-Head lease collision resolve
With two clients on the left Head, save two overlapping checkpoints from that
Head and ask to mount one on the right:
```lisp
(lime.x11:backend-save-view *lime* "code" "Code" :head-id "left")
(lime.x11:backend-save-view *lime* "docs" "Docs" :head-id "left")
;; Read-only: returns an ACTIVATION-REPORT with :CLIENT-LEASED conflicts.
(lime.x11:backend-preflight-view-activation *lime* "docs" :head-id "right")
;; "Open what is available": left stays visible; right records holes.
(lime.x11:backend-activate-view-partially *lime* "docs" :head-id "right")
;; "Move windows here": explicit transfer; the old left draft becomes partial.
(lime.x11:backend-activate-view-taking *lime* "docs" :head-id "right")
```
`backend-activate-view-strictly` is the default “open only if complete”
command: it signals `activation-blocked` rather than changing either Head if
any lease conflicts. The preflight function does no mutation; a future menu
can inspect its report, offer the complete, partial, and move-here commands,
or cancel. The adapter applies all configured Heads after a transition, so
an explicit take unmaps/reconfigures the source and target in one visual
update.
## Install the starter keymap and name your own task shortcuts
`start-backend` (and therefore `run-demo`) installs a Super (`Mod4`) keymap
for ordinary window operations. It has no Activity, View, or physical Head
names, so it does not guess at your workflow. Reinstall it after clearing or
changing bindings, or opt out at startup for a fully custom map:
```lisp
;; Reinstall with a different primary modifier.
(lime.x11:install-default-keybindings *lime* :modifiers '(:mod-1))
;; Or start with no defaults and bind everything yourself.
(lime.x11:start-backend :display-name ":99" :install-default-keybindings-p nil)
```
| Binding | Command |
| --- | --- |
| Super-t / Super-h | Focus next / previous visible client |
| Super-Shift-t / Super-Shift-h | Move focused tiled client forward / backward |
| Super-Return / Super-z | Promote focused client / focus primary tiled client |
| Super-Tab / Super-Shift-Tab | Cycle master-stack ↔ monocle forward / backward |
| Super-d / Super-n | Shrink / grow master area (master-stack only) |
| Super-Up / Super-Down | Add / remove a master client (master-stack only; minimum one) |
| Super-Space | Toggle focused client floating |
| Super-f | Toggle focused client fullscreen |
| Super-i / Super-Shift-i | Show a hidden/shared window / hide the focused window |
| Super-Shift-c | Close focused client |
| Super-Shift-q | Quit Lime cleanly |
| Super-Shift-s | Update the mounted saved View from its live draft |
| Super-p | Open fuzzy PATH-program picker |
| Super-y | Open the Activity/View command palette |
| Super-e / Super-Shift-e | Select next Head / send focused client to next Head |
`Super-Shift-i` parks the focused window only from the current Head-local
draft: it does not close, minimize, or remove the client from its Activity.
`Super-i` opens a fuzzy list of parked Activity windows plus shared windows and reveals the
chosen one in the current draft. If the window is presently visible on a
different Head, Lime asks for an explicit **Move here** confirmation rather
than showing one X11 window twice. Saving or updating a View records its
current shown/parked set, so this is the everyday way to keep a focused
monocle or tiled context uncluttered without inventing another workspace.
Under `Super-y → w` (Windows), `m` moves the focused window to an existing
Activity. Create one first through `a → n` if needed. The move keeps you here
and queues the window in the destination's working draft for this Head; opening
that Activity shows it. A private window is removed from its old checkpoints
and cached drafts, so recalling them cannot pull it back across Activities.
Another Head already displaying the destination Activity is not rearranged.
`Super-y → w → s` makes the focused window shared. It keeps one home Activity,
but becomes available through `Super-i` in every Activity. It does not appear
everywhere automatically. If another Head leases it, the picker offers an
explicit **Move here**; shared slots taken away can be summoned back using
the same picker. Views may save shared placements, subject to normal lease
conflicts on recall. On a shared window, `w → s` instead offers **Make private to
Activity**: the Activity where you're using it becomes its home. The window stays
on this Head, with the same position and focus. Saved views in this Activity keep
it; references in every other Activity (including its old home) are removed.
The confirmation names the Activity and defaults to Cancel; choose the named
action with arrows and Enter, or click it. Changing the desk while the prompt is
open invalidates the choice rather than acting on a different window or Activity.
`Super-Shift-e` (or `Super-y → h → s`) sends the focused window to the next
Head. Same-Activity and shared-window transfers happen immediately, without
changing home membership. For a private window crossing Activities, the
dropdown names both Activities and offers **Cancel** (default) or **Move to
destination**. Choose with arrows and Enter, or click; Escape cancels. The
warning explains that moving removes the private window from its old saved
views. Changing the desk while this confirmation is open invalidates it.
Neither Head switches Activities; keyboard focus follows the window automatically,
so you don't need a second `Super-e`. `Super-e` still switches monitors without
moving a window. The destination selects the arriving
window in its arrangement, so it is visible even in monocle. A destination View or
draft that already contains the client controls the placement there; otherwise
Lime creates an unsaved destination placement matching the source tile/float
kind. The old Head loses the client and focuses a surviving window. The low-level
`backend-transfer-focused-client-*` functions remain strict, non-prompting lease
transfers; `backend-rehome-client-to-head` is the separate explicit rehome API. Lime
does not yet have dwm's all-floating layout; floating remains per-client.
The retained manual tree is accessible through Lisp and existing Views,
not through the everyday layout cycle.
For names that only you can choose, use the configuration helpers. The strict
View policy below is safe by default; a future picker can use the same
preflight result to offer partial or take alternatives:
```lisp
;; Super is normally Mod4 on X11.
(lime.x11:bind-head-key *lime* #\1 "left")
(lime.x11:bind-head-key *lime* #\2 "right")
;; A View binding chooses an explicit collision policy.
(lime.x11:bind-view-key *lime* #\d "docs" :policy :strict)
;; A Viewset binding atomically applies its saved Head -> View mapping.
(lime.x11:bind-viewset-key *lime* #\w "work-desk" :policy :strict)
;; Without :VIEW-ID this resumes the Activity's unsaved Head-local draft,
;; creating a blank draft on that Head when needed.
(lime.x11:bind-activity-key *lime* #\g "game" :view-id "play")
(lime.x11:list-keybindings *lime*)
(lime.x11:clear-keybindings *lime*)
```
Handlers that signal an error—such as a strict activation blocked by a
lease—do not kill the event loop. Inspect `backend-last-command-error` from
the REPL, and use `clear-keybindings` during reconfiguration. `bind-key`
remains available for arbitrary commands. The first pass matches modifier
state exactly, so Caps Lock/Num Lock tolerance and an actual focus policy
remain later input-layer work.
## Open the first command palette
Press Super-y to open a which-key-style command map below the bar. It shows
only the current prefix's keys in a compact grid; `+` labels are submenus.
Press a shown key to enter a group or run a command. Space or Tab opens direct
search; type to filter, use Up/Down to select, and Enter to run. Tab in search
returns to the root map. Escape closes in every mode. Super-p remains the
separate fuzzy program launcher. This is a real X11 overlay with a temporary keyboard grab,
not a managed client, so it is local to the currently focused Head and does
not affect client leases by itself.
The launcher reuses its executable catalog while PATH directories are unchanged;
rapid typing immediately after the shortcut is queued for the picker, not sent
to the previously focused application. The isolated `test/x11-launcher-smoke.lisp`
checks complete shortcut-plus-query-plus-Enter bursts and keyboard error cleanup.
The direct-search catalog includes `New named Activity...`, `Save current
arrangement as View...`, and—when every configured Head has a saved source
View—`Save current Heads as Viewset...`. Press Enter on either to enter a one-line name prompt;
Enter confirms it and Esc cancels it. A named Activity opens a blank unsaved
live draft; it owns no permanent View until you save a meaningful checkpoint.
Saving a View checkpoints the live draft and mounts that new checkpoint, so
Super-Shift-s subsequently updates the View you just named.
Lime derives readable durable ids (`WM Project` becomes `wm-project`; `Code`
within it becomes `wm-project/code`) and adds a numeric suffix if needed, while
keeping your original title as the display name.
If RandR reports an added or removed Head, the searchable catalog also
contains `Reconcile changed displays...`. Selecting it asks once for every
live Presentation on a removed Head: move it to one of the discovered Heads,
or `Suspend safely`. The last screen summarizes every move and suspension;
`Apply display plan` is the only action that calls the topology command. Thus
an unplug event never grabs attention or moves a task until you deliberately
open the palette and confirm the plan.
The map shows a breadcrumb instead of a search input. Backspace moves up one
level; PageUp/PageDown reveal additional keys if a large custom tree or narrow
Head requires pages. The panel sizes itself to the current entries and follows
the configured font and bar colors. The first level is deliberately small:
| Key | Menu |
| --- | --- |
| a | Activity (`a n` creates a named Activity; `a o` searches) |
| v | View (`v s` saves; `v u` updates; `v o` searches) |
| x | Viewset (`x s` saves current Heads; `x o` searches) |
| l | Layout (master/stack, monocle, cycle) |
| w | Window (focus, float, close) |
| h | Head (select, send a client, or `h t` reconcile changed displays) |
| r | Launch (terminal, browser, explicit shell command) |
| q | System (`q q` requests a graceful quit) |
| u | User (when the config supplies commands with `:key`) |
| Space | Search all commands |
For example, `Super-y`, `v`, `s` reaches the same new-View name prompt
without typing a search query. The key tree only calls the existing command
layer, so it carries no independent Activity, layout, or lease policy.
When a display change is pending, `Super-y`, `h`, `t` reaches exactly
the same reconciliation flow as the direct-search candidate.
`Super-y → a → d` opens **Delete activity...** (also available in direct search).
First move or close every window owned by the Activity, including hidden/shared
ones, and switch all Heads away from it. The picker explains either blocker.
Choose the Activity, then explicitly select **Delete Activity: name**; the
confirmation defaults to Cancel. This removes its saved Views and cached drafts,
prunes their Viewset entries, and deletes any now-empty Viewsets. No app closes
and no Head switches. There is no undo; a changed desk invalidates the confirmation
and requires reviewing it again. Deletion is saved by ordinary session persistence.
Selecting a ready View uses the normal strict activation command. Selecting a
View whose windows are leased on another Head replaces the result list with
three explicit options:
| Choice | Command it runs |
| --- | --- |
| Recall without busy windows | `backend-activate-view-partially` |
| Move windows here | `backend-activate-view-taking` |
| Cancel | No model transition |
The palette includes `Update saved view...` when the current Activity has saved
checkpoints, even if the live arrangement has no source View. Choose a target
and confirm its name before replacing it; Cancel is the default choice.
`Recall view...` explicitly restores a checkpoint, including the last one used.
Named-entity pickers, lease-conflict choices, and name
prompts remain separate transitions inside the same overlay; the command map
does not force those choices into a giant tree. Returning to the map and
searching again restores the full catalog, not the last scoped picker.
## What the adapter does
`start-backend` creates one Head for each configured rectangle and an
initial `inbox` Activity/View for each, claims the root window, and adopts
already-viewable normal clients into the selected Head. Each normal
`MapRequest` then:
1. creates a Lime client in the active Presentation's Activity if necessary;
2. adds it only to the active Presentation's live draft;
3. gives it a lease when free; and
4. resolves the Head-local layout tree into X11 work-area rectangles before mapping
the leased clients; and
5. focuses the selected Head's preferred visible client (or root if blank).
`apply-active-presentation` is the narrow convenience boundary around a
declarative render plan and its X11 executor. `build-render-plan` produces
inspectable configure/map/unmap/raise/focus effects for every configured Head;
`execute-render-plan` flushes them as one CLX batch and refuses a stale world
revision. `DestroyNotify` lifecycle handling retires a permanently gone client
from the model before the remaining geometry is reapplied. See
[the architecture document](architecture.md) for the module contracts.
## Intentional omissions
This initial bridge is purposefully not a complete ICCCM/EWMH-compliant
daily-driver.
It currently has no per-client decorations, theme system, custom/dynamic
key-path palettes, focus-follows-pointer policy, aspect-ratio hints, general
floating rules or stacking controls, session persistence, or complete
View/Viewset editing UI. It implements a small ICCCM slice: basic client facts,
`WM_DELETE_WINDOW`, InputHint/`WM_TAKE_FOCUS`, transient-dialog placement,
min/max/base/increment normal hints, and synthetic ConfigureNotify for managed
ConfigureRequests. Its small EWMH slice advertises `_NET_SUPPORTED`, handles
fullscreen/activate/close requests, identifies itself, publishes client and
stacking lists, and maintains `_NET_ACTIVE_WINDOW`; it does not provide fake
`_NET_WM_DESKTOP` state, external dock work areas, or the many other EWMH
states expected of a mature desktop WM.
Those are separate layers. The value of this adapter is that the core model
can now be observed controlling real windows while those layers are added
without smuggling policy into lease resolution.