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

31 KiB
Raw Permalink Blame History

Architecture

Lime is arranged so that task/workflow semantics stay independent of X11, and so that X11 protocol work can grow without becoming one giant event handler.

key / menu / IPC / pointer                 MapRequest / DestroyNotify
          |                                              |
          v                                              v
   x11/input, x11/ui                                   x11/lifecycle
          |                                              |
          +--------------> x11/commands <---------------+
                                  |
                                  v
                             lime.core
                                  |
                                  v
                     x11/render: plan + CLX executor

Core model

src/model.lisp, src/layout.lisp, src/operations.lisp, and src/topology.lisp make no X11 calls. They own Activities, Views, Heads, Presentations, Leases, layout resolution, collision planning, topology-plan reconciliation, and model focus state. Every core transition must preserve the one-lease invariant and be testable without an X server.

src/membership.lisp owns explicit home-Activity moves, shared eligibility, and guarded Activity deletion (empty and unmounted only, with reference cleanup). It prepares affected View/draft copies before mutation, then commits ownership, layout/focus cleanup and lease release together. The adapter chooses destination draft and layout policy; neither menus nor X events implement membership rules. make-client-private-in-presentation atomically adopts the current Activity as home and revokes sharing. It prunes other Activities' saved/live references without replacing the current lease or changing placement, focus, or dirty state. The lower-level, context-free set-client-shared retains its existing-home revocation semantics for callers deliberately operating without a live desk.

src/topology.lisp compares a desired list of logical Head ids with the current model and builds an explicit move/suspend plan. It knows nothing about physical outputs: a removed live Presentation either moves whole to a caller's chosen destination or becomes an unbound, lease-free cached draft. This keeps hotplug routing reusable from an X11 menu, IPC, or test fixture.

src/layout-operations.lisp is the focused, Head-local layout command layer: it selects manual/master-stack/monocle policy, promotes a master, and edits a focused split or visible tiled slots. It changes only a live Presentation; View persistence remains an explicit operation in operations.lisp.

src/floating-operations.lisp is the equally small Head-local floating layer: it toggles an existing placement between tile and float, validates normalized rectangles, and keeps the tiling tree consistent. It neither knows pointer events nor grants leases, so the same commands work from a menu, IPC, or REPL.

The core should receive an intention such as “activate this View partially” or “focus this visible leased client,” never an X event or a pixel rectangle.

X11 adapter

src/x11/backend.lisp owns the CLX connection, root selection, configured Head rectangles, backend-owned window lookup tables, and the event loop. It does not decide layouts or issue rendering effects directly.

src/x11/resilience.lisp owns the narrow X error policy, bounded diagnostic reports, independent input-grab cleanup, and explicit recovery reconciliation. Only request-specific vanished external-client/unviewable-focus races are tolerated; BadAccess and Lime-owned resource failures remain errors. Failed synchronous client queries unwind to an event/adoption boundary rather than waiting for a reply that cannot arrive. Dispatch consumes each current event before mutation (CLX otherwise retains it on an unwind), and returns after one event so a busy client cannot starve developer control requests.

The separate lime/dev runtime catches unexpected dispatch errors with a pre-unwind backtrace, pauses without replacing the world, releases active grabs, and continues servicing the owner-thread mailbox and Slynk. Explicit resume validates model/adapter consistency and reconciles live X windows before render. It is not a rollback, automatic retry, or persistence system. Explicit backend restart keeps the supervisor and REPL alive through success or failure, and constructs a replacement world from session storage when enabled. A separate lifecycle lock serializes REPL and supervisor stop/restart operations without blocking mailbox bookkeeping. Shutdown attempts each cleanup independently and always attempts to close CLX. See recovery behavior.

lime/persistence is separate from X11. src/persistence/model.lisp encodes a versioned plain-data schema and reconstructs a fresh validated world, including cached drafts; it derives leases from active selections/unavailable slots. src/persistence/store.lisp owns bounded non-evaluating reads, a lifetime writer lock, private atomic replacements, and file/directory synchronization. src/x11/persistence.lisp supplies verified server/window tokens, physical-Head reconciliation, restored-client adoption, debounced owner-thread snapshots, and one off-thread disk writer. The worker never sees the live world or CLX. A restore error blocks autosave rather than replacing a rejected file with a blank desk. Configuration remains policy, not generated task state. See persistence boundaries.

src/x11/randr.lisp discovers active physical outputs at startup and maps one active CRTC to one logical Head; cloned outputs share a CRTC and therefore a Head. RandR events update only an adapter-owned topology snapshot. A refresh may safely replace rectangles when the same Head ids still exist. For added or removed Heads, the adapter exposes a report -> build plan -> apply plan command sequence; it rereads RandR immediately before apply and rejects a plan if the physical Head-id set changed while a frontend was presenting choices.

src/x11/lifecycle.lisp turns narrow client lifecycle events into core commands: adopt a normal new client into the selected live Presentation, retire a destroyed or application-withdrawn client, or ask the renderer to restore a tiled geometry. Its pending-unmap token boundary distinguishes a renderer-driven View hide from an application's real ICCCM withdrawal. src/x11/icccm.lisp supplies the protocol details lifecycle and rendering need: cached title/class/capability facts, WM_STATE normal/iconic updates, property refreshes, graceful close requests, protocol-aware focus, and synthetic ConfigureNotify replies. Lime removes WM_STATE on a real client withdrawal. It does not change Activities or lease planning.

src/x11/placement-policy.lisp consumes a managed client's transient-parent fact only at adoption: a WM_TRANSIENT_FOR child follows its parent’s active Presentation and Head, starts as a centered float, and receives focus. Dialog-like EWMH clients float on the selected Head. Ordinary clients whose WM_NORMAL_HINTS declare equal positive minimum/maximum sizes on both axes default to centered floats too, unless a matching configuration rule requests tiling. This is initial-placement policy only: later hint changes do not undo manual floating/tiling choices or saved arrangements. Resizable main windows remain tiled even when their fixed-size startup windows float. The adjacent src/x11/geometry.lisp adapts resolved pixel rectangles to cached WM_NORMAL_HINTS min/max/base/increment constraints inside the target Head. Both modules are adapter policy; they never write View or Lease state.

src/x11/appearance.lisp owns configurable client borders and inner/outer tile gaps. It insets resolved slots without editing the layout tree, contains the complete bordered frame, and applies normal hints within each tile's remaining space. Floats bypass gaps but retain borders. The render planner counts leased tiles with resolved geometry separately on each Head; a sole visible tile bypasses gaps and borders in every layout. This also covers monocle without a layout-name special case. Normal hints and the bar's work area still apply. Fullscreen bypasses all decoration, normal hints, and the bar reservation. The adapter caches allocated border colors and original application border widths, restoring widths on withdrawal or shutdown. Configuration reloads repaint the same draft rather than turning theme choices into saved View data.

src/x11/render.lisp has two separate jobs:

  1. build-render-plan derives inspectable x11-effect data from active Presentations—currently configure, map, unmap, float raise, and one input-focus effect. Configure effects include content geometry, border width, and the color chosen for global client focus.
  2. execute-render-plan performs that plan through CLX and flushes it as one batch. It rejects a plan if the core world revision changed after planning.

The temporary apply-active-presentation name remains as a small convenience wrapper around those two operations. It then redraws the adapter-owned Head bars; that UI state is deliberately outside the model render plan. Normal layouts and floats resolve against x11-head-work-area (below a configured top-bar reservation), while EWMH fullscreen still resolves against the whole physical Head. New policy-aware callers can inspect a plan before executing it.

src/x11/commands.lisp is the frontend-neutral imperative API. A keybinding, menu, REPL, or future IPC service calls it to choose an already-tested core transition and request one reconciliation. It also owns Head-relative selection and focused-client transfer: the target draft retains any existing placement, otherwise the source tile/float placement is copied, and the core performs the exclusive lease move. Private clients require the same Activity; shared clients may transfer across Activities without changing their home. The interactive backend-send-focused-client-* entry points live in ui.lisp: eligible transfers run immediately, while a private cross-Activity send offers a named confirmation with Cancel selected. It captures the window, both live arrangements, focused Head, and world revision, rejecting stale choices. Confirmation calls the separate backend-rehome-client-to-head command and core move-client-to-presentation transaction: membership, old-checkpoint pruning, new placement, and the single lease commit together after validation. Neither monitor switches Activities. After a successful send, keyboard focus and command/new-window routing follow the moved window to the destination Head; the source remembers a surviving client for when the user returns. Cancelled or rejected sends do not change focus. backend-move-client-to-activity explicitly rehomes a visible client and queues it in the destination Activity's draft for the same Head without switching. backend-set-client-shared enables sharing without moving the window; NIL calls backend-make-client-private, which keeps it private to the current Activity. That explicit command also accepts a Head and expected revision. The menu names the Activity, defaults to Cancel, and rejects changed focus/desk/revision before calling it. The hidden-window picker includes shared guests and asks before transferring an existing lease. src/x11/input.lisp only turns a passive X key grab into such a command; it contains no View names or lease rules. Passive grabs synchronously queue subsequent keyboard input until the handler either opens an asynchronous palette grab or finishes; an unwind-protect always thaws input, including failed or missing handlers. This closes the gap where releasing the shortcut before a slow menu opened sent typed letters to the previously focused application. The pointer remains asynchronous. src/x11/default-bindings.lisp supplies the default, replaceable dwm-shaped starter map plus named Activity/View/Head configuration helpers; it owns neither X events nor model policy. src/x11/pointer.lisp does the equivalent minimal routing for unmodified Button1 plus Super+Button1/2/3 float move/toggle/resize gestures. It uses passive grabs so a click in an application's child window reaches the managed client boundary; an ordinary managed-client click resolves to its leased Head, focuses that client, then replays the click to the application. A Super gesture focuses a floating client and turns its root-pixel motion into a normalized geometry command. Consecutive queued motions from the same gesture are coalesced in bounded batches; key/button/lifecycle events are never skipped. Release applies its final coordinates, and a gesture is cancelled if its original Presentation or floating lease is no longer current. Geometry-only commands configure just that client through the usual layout/border/normal-hint calculations, without reapplying focus, stacking, EWMH inventories, or other clients' rectangles. Only the clean-to-dirty transition redraws bars. Blank root space selects that Head and deliberately focuses root. The adapter owns reusable left_ptr and hand2 cursors. Bars and dropdowns inherit the root arrow except over their measured clickable regions, where only that UI window selects the hand. Application-specific cursors are untouched. Enter/motion/leave events update the cursor; redraws also recheck the pointer so changed widgets, filtered lists, and resized menus cannot leave a stale hand or highlight under a stationary mouse. This is event-driven, not a pointer polling timer. Root selection is confirmed before installing the arrow, and shutdown releases both cursor resources.

src/x11/cursor.lisp owns cursor configuration, assets, and resource lifetime; pointer.lisp only routes gestures and selects arrow/hand roles. Optional libXcursor loads theme images into memory, including inherited/default themes. CLX uploads their ARGB pixels through X Render on the existing WM connection, independent of root visual depth. There is no additional Xlib connection or GTK dependency. Temporary images, pixmaps, GCs, pictures, and animation frames are released after upload; the installed cursor set lives until replacement or shutdown. Theme/size reload is transactional with the rest of configuration: old cursors remain live until commit, and rollback rebinds the original XIDs, including on hovered UI. Unsupported/missing theme facilities use core cursors with diagnostics available through backend-cursor-report.

src/x11/session-settings.lisp is the application-facing settings boundary, separate from cursor rendering. Explicit cursor config publishes only Gtk/CursorThemeName / Gtk/CursorThemeSize using the XSETTINGS protocol. It claims _XSETTINGS_Sn only when vacant, announces a complete initial property with a real server timestamp, preserves unrelated queued events, and destroys its owner window on SelectionClear without stealing ownership back. Existing managers remain authoritative for their toolkit clients.

The same layer owns reversible cursor-only RESOURCE_MANAGER edits and a child launch-environment overlay. It preserves unrelated resource records and later external edits; resolving NIL defaults excludes Lime's own publication. No desktop config files, process-global environment, or activation environment are mutated. Configuration failure restores prior publication; shutdown releases it before closing X. Runtime ownership lives in a backend-keyed table, not the pure model or session persistence. Clients may cache settings; protocol publication cannot force an application to replace its own cursor resources.

src/x11/config.lisp is a user-facing adapter-policy layer, exposed as lime.config. Its DEFCONFIGURATION macro produces ordinary data: first-class commands, bindings to those commands, ordered initial window rules, window appearance, optional bar settings, and startup hooks. The configuration is held by the backend, not the core world. A named command appears in direct palette search and, when it has a key letter, under the palette’s User branch as well as being eligible for a physical binding. Reload validates all data before it touches X11 and rolls back binding/bar/appearance settings after an installation error; it never recreates Activities, Views, Presentations, leases, or Viewsets. Current rules style only ordinary new clients (float/center) and intentionally cannot make a class match mount a View or move a client between task contexts.

src/x11/navigation.lisp owns contiguous Activity positions, stable Activity-local View slots, and last-used Presentation ids per (Head, Activity). A separately owned, backend-keyed runtime avoids changing core identities or the live backend's structure; shutdown releases it (SBCL also uses weak keys). Rendering records active arrangements once per model revision. Slot reassignment changes only navigation metadata, whose own generation participates in session autosave.

src/x11/activity-navigation.lisp owns persistent portable/extended navigation intent and the portable arrangement id. New named Activities are portable; the bootstrap Scratch desk starts with its existing per-Head blank portions. src/presentation-navigation.lisp supplies the pure atomic bring/swap operation: it preserves active lease identity, transposes displaced cached portions, and preflights inactive resumes without replaying missing checkpoint slots. Portable checkpoint recall previews the compound move/recall on a detached world before any live mutation, so a lease conflict or cancellation never swaps monitors as a side effect. Explicit multi-Head Viewsets and direct multi-portion model edits remain capable of extending an Activity. The footprint sidecar has its own autosave generation; it is not a slot added to live backend or model structures. activity-navigation-ui.lisp exposes extension and return-to-portable commands through the normal dropdown, with cancel-first choices and stale-context guards. Activity deletion closes gaps while preserving surviving bar order; new Activities append. Persisted sparse Activity slots are validated before being compacted. Explicit Activity slot assignment swaps within the existing count, whereas View assignments retain their stable, gap-filling policy. The versioned X11 session envelope stores these indexes separately from the pure world. The core's unique (Activity, Head) arrangement is authoritative; last-used ids remain migration metadata, not a separate navigation target. Activity navigation resumes current locations without a collision menu. Explicit checkpoint recall still uses lease preflight/choice commands. :navigation-keys expands to ordinary configuration commands/bindings rather than hardcoded key policy.

src/viewset.lisp adds the corresponding grouped core planner for named whole-desk shortcuts. It records only Head -> View entries, preflights every target Head as one outgoing group, and commits one atomic multi-Head lease transition. src/x11/commands.lisp exposes save/preflight/strict/partial/take commands; no X11 frontend is allowed to recreate that grouped collision logic.

UI boundary

src/ui/session.lisp is a renderer-neutral direct-search, one-string-prompt, and key-tree session. It owns a candidate list, filtering, selection, minibuffer text, a key-path stack/page, and declarative results (:close, :keep-open, :replace, or :prompt). Candidate actions and key nodes are supplied by a frontend, which makes a collision prompt an ordinary replacement candidate set, a name prompt an ordinary session transition, and a which-key menu a data tree rather than special modes embedded in a View-switch command. MAKE-UI-SESSION explicitly selects :search (the default for standalone pickers) or :key-path (the Super-y menu). A separate root candidate catalog survives scoped/collision pickers: menu → search always restores the complete catalog rather than accidentally searching the last replacement list.

src/x11/bar.lisp supplies the first reusable persistent UI surface: one override-redirect bar per Head. It renders Activity names (not checkpoint or dirty state), the Head-local layout token, and a focused-client title section whose colors identify Head focus. Its optional right-side providers return HEAD-BAR-SEGMENT values and may give each segment a click action; music, volume, and clock integration therefore remain configuration rather than model state. The Lime segment opens the palette and the layout segment cycles the local layout. The Activity strip is slot-ordered on every Head; that Head's active entry uses the accent foreground. Saved Views live in the recall/save/update menu; updates choose a named target and confirm replacement rather than infer it from the last recall. Each independently shaped run owns its click region, reusing ordinary Activity resume and lease prompts. Budgeted overflow retains the active entry and nearby slots when possible, with a dedicated … target opening an Activity-only picker. Neither clipping nor clicking invents new layouts, changes membership, or takes leases silently.

src/x11/status.lisp owns opt-in managed provider lifecycle and cached data, separate from bar.lisp painting and status-desktop.lisp source protocols. Each managed provider has one cancellable worker per backend, not per Head. Workers publish changed data snapshots under a lock and send one coalesced _LIME_STATUS_CHANGED event through a separate, serialized CLX notification connection to an unmapped adapter-owned window. They never use the WM's X connection or access model state. The ordinary X event loop consumes the wake and redraws bars on their owning thread—even when there has been no keyboard, mouse, or client event. No status refresh timer is attached to bar rendering.

Music publishes playback/title/source facts, volume publishes percentage/mute/ sink facts, and the clock publishes local date fields. A configuration-supplied :formatter consumes a defensive copy on the X thread, outside worker locks, and returns text, NIL, or a HEAD-BAR-SEGMENT with optional foreground, width, and click overrides. status-format.lisp contains optional, parameterized presentation helpers, not subscription logic. Config therefore chooses icons, spacing, date style, visibility, and colors without editing protocol code. Legacy generic providers may still publish strings through an identity formatter. Formatting failures hide one widget without stopping its source or other widgets. The bar's overall left/title/right geometry remains renderer policy; this seam does not claim an arbitrary bar-layout language.

MPD snapshots and idle use the same native connection, retaining events between query and subscription. MPRIS uses the documented playerctl follow interface; audio uses a persistent pactl subscribe process and bounded one-shot reads. The clock alone schedules a wall-time deadline, recomputed for each minute. See the MPD idle protocol for the accumulated-event guarantee. Reconnection timers use 1–30 second backoff only after service failure; they are not a healthy-state polling loop.

Configuration validation accepts managed sources alongside legacy pure provider functions. Reconciliation keeps identical provider objects, cancels removed workers, and starts replacements; rollback reinstalls the previous set. Shutdown wakes sleeping workers, shuts down owned MPD sockets, and kills and reaps only owned child helpers. Provider failures are retained in backend-status-provider-report, not allowed to kill the X loop or display stale cached data. Public worker functions must cooperate with cancellation.

Bar text is shaped and measured by the shared Pango/Cairo font layer, with a bounded per-font metrics cache. A server-free text-layout helper computes pixel-budgeted truncation and positions, shared by drawing and click regions. Identity and layout fields use measured | separators; the title is left-aligned inside an expanding section with two muted one-pixel edges, and status text is right-aligned. Validated :padding-x controls matching outer text margins and title insets; the separator's own space supplies the gap after Lime, without duplicate logo padding. The same layout data positions paint and click targets. A font-derived optical baseline centers a stable letter-body reference instead of unused line leading. Every bar field uses that baseline, independent of its current text or fallback font's ascent, avoiding title-content-dependent vertical jumps. HEAD-BAR-STYLE is validated configuration, separate from window appearance and layout. Each bar caches its own colormap allocations and releases unused colors on redraw/configuration rollback and shutdown. Oversized status text is truncated while preserving room for the layout control and a small Activity navigation area. Ordinary separators and status truncation dots do not acquire status click actions. Each provider is shaped independently so bidirectional text cannot reorder adjacent widgets; click regions use the shaped source text's visual ranges, not an assumed character advance.

src/x11/attention.lisp owns a backend-only, two-source request cache for ICCCM urgency and EWMH demands-attention. Property/client-message events update it without touching the world revision. The renderer's global input target and normal FocusIn events acknowledge only a selected visible client with verified X focus; another Head's local focus, root focus, or a palette grab is not enough. The cache is not serialized: adoption rereads surviving clients' protocol state. The bar renders a configurable global count using its existing status-segment and click machinery; the picker consumes cached data and calls backend-attend-client. Visiting an Activity resumes its current arrangement without restoring stale claims for unrelated windows. See attention for the protocol and interaction boundaries.

The cached client title prefers EWMH _NET_WM_NAME decoded with Babel over ICCCM WM_NAME. Title/class property events repaint bars without touching geometry, leases, or draft dirty state. src/x11/text-native.lisp contains the narrow CFFI boundary to Pango, Cairo, and GLib. src/x11/text.lisp owns font descriptions, shaping/measurement, grapheme-safe fitting, clipped painting, and pixel upload for both bars and the palette. Pango handles Fontconfig fallback and shaping, including color emoji with installed fonts. Cairo paints a complete opaque RGB frame in memory; CLX uploads it on the existing WM connection. The font renderer needs no extra Xlib connection or toolkit event loop (the optional status notifier described above is separate). The adapter requires a TrueColor root visual and converts RGB pixels to its actual masks (including 16-bit displays). The pure model and lime/ui remain independent of these native libraries.

Each UI surface owns a font/layout and a resize-reused canvas, operated only by the backend thread. Font replacement allocates before freeing the old resource; close, Head removal, resize, rollback, and shutdown release native resources explicitly. UI-FONT-STYLE is validated configuration, shared by both surfaces. A font reload repaints/resizes an open palette without replacing its search session. This font-rendering boundary does not add an IME.

src/x11/ui.lisp builds the command catalog/tree and owns keyboard-grab/input routing. src/x11/ui-render.lisp owns the temporary X11 surface, fonts, canvases, measured geometry, and painting. Its server-free grid projection chooses up to four columns, fits labels in pixel budgets, and pages large trees within the Head-local dropdown. The renderer clamps the session's requested page when font/Head dimensions change. Height follows the current menu or picker instead of reserving a fixed ten-row block. Keys, submenu labels, breadcrumb, and footer have distinct visual roles; detailed candidate descriptions appear only for the selected search result. The dropdown reuses the configured bar colors through the adapter's style seam, rather than hard-coding black/white. It draws directly below the persistent bar. Its Activity and View choices call the command layer. A collision first performs the normal read-only preflight, then offers the already-public strict, partial, and take commands explicitly. The overlay is frontend state only: no X11 window, keyboard grab, search query, or visual styling leaks into lime.core.

The same replacement-candidate mechanism drives RandR reconciliation. A pending added/removed-Head snapshot contributes one direct-search candidate and a Head -> topology key-path entry. The frontend collects one move or suspend choice for every removed live Presentation, shows a final Apply choice, then calls the separate RandR build/apply commands. It never reacts directly to an output event or invents a second modal UI protocol.

Super-y and the bar logo open the key root; Super-p remains the separate fuzzy program picker. Executable discovery is cached in the launcher adapter, keyed by PATH, current directory, and PATH-directory modification dates. A five-minute maximum cache age catches permission-only/coarse-timestamp changes on the next open; no timer polls while idle. Name deduplication uses a hash table rather than quadratic list comparisons. Each palette gets fresh candidates/actions; the cache holds only executable names, not a stale backend or UI session. Space (unless explicitly bound in a custom tree) or Tab opens global search; Tab returns to the root map. Prompts retain literal spaces and cannot switch modes with Tab. Backspace goes up a prefix and Escape dismisses the entire overlay without dispatching another command. Key cells and search results are also clickable, with hover feedback and small Back/Search/Menu/ Confirm/Close controls as appropriate. Wheel input navigates menu pages or search selection; paged grids also expose previous/next buttons. Geometry is shared by painting and clipped hit regions, rebuilt each frame. Mouse events on the overlay are consumed before client focus/drag routing, including clicks in blank space. No global pointer grab is needed. Both input paths share the same session transitions and error boundary; candidate clicks carry the actual candidate object and reject stale choices instead of reusing a list index.

Richer text input, dynamic workflow-specific trees, themes, and decorations can reuse the same session protocol or add another renderer without changing Activity/View/Lease rules.

Direction of dependencies

Dependencies should point inward toward the core, never the reverse:

input / pointer / menu / IPC -> commands -> lime.core
user configuration           -> commands + input + bar + lifecycle policy
ui session                   -> commands -> lime.core
x11 bar / UI renderer        -> ui session + CLX + adapter snapshots
lifecycle                    -> lime.core
icccm                        -> CLX
ewmh                         -> commands + CLX + active Lease queries
randr                        -> CLX + backend Head rectangles + core topology plans
render                       -> lime.core + ICCCM/EWMH + CLX
backend                      -> CLX + module interfaces

That leaves room for an IPC frontend, richer Viewset editing, an interactive RandR topology picker, remaining ICCCM/EWMH work, richer pointer policy, and alternate layout engines without forcing the Activity/View/Lease model to know about physical output APIs.