Files
lime/docs/xephyr-demo.md
2026-10-02 16:35:17 -05:00

33 KiB
Raw Permalink Blame History

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.

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:

(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:

(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.

In a third terminal, create some clients:

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:

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 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:

(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:

(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:

;; 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:

(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:

;; 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:

(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:

(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:

;; 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:

(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:

(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:

(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:

(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:

;; 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:

;; 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:

(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:

;; 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:

;; 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 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.