Initial Lime window manager
This commit is contained in:
487
docs/architecture.md
Normal file
487
docs/architecture.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# 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.
|
||||
|
||||
```text
|
||||
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](development.md#recovering-from-an-event-loop-error).
|
||||
|
||||
`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](persistence.md).
|
||||
|
||||
`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](https://specifications.freedesktop.org/xsettings/latest/).
|
||||
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](https://github.com/altdesktop/playerctl#following-changes);
|
||||
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](https://mpd.readthedocs.io/en/stable/protocol.html#idle)
|
||||
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](attention.md) 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:
|
||||
|
||||
```text
|
||||
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.
|
||||
78
docs/attention.md
Normal file
78
docs/attention.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Application attention
|
||||
|
||||
Lime watches ICCCM `WM_HINTS` UrgencyHint and EWMH
|
||||
`_NET_WM_STATE_DEMANDS_ATTENTION`. A request adds a quiet, clickable `! N`
|
||||
badge to every visible Head bar. There are no flashes, popups, timers, or
|
||||
automatic Activity switches. A window requesting both hints counts once.
|
||||
|
||||
Click the badge or use **Super-y → w → a** for a searchable list across all
|
||||
Activities. The direct command search also offers “Windows needing attention”.
|
||||
Entries show the Activity and the current application title. The ordinary
|
||||
hidden-window picker marks requesting windows with `!`, and Activity search
|
||||
shows request counts in its details.
|
||||
|
||||
## Visiting a request
|
||||
|
||||
- A leased window is focused on the Head that already owns it. It is not
|
||||
pulled onto the Head where you opened the picker. This also works for a
|
||||
non-visible member of a monocle stack.
|
||||
- A hidden window is shown in its own Activity on the selected Head. If
|
||||
necessary, Lime resumes that Activity's **draft**, not an arbitrary saved View.
|
||||
- Resuming a draft with contested windows still offers explicit partial/take
|
||||
choices. Opening or cancelling that choice does not change either Head.
|
||||
The target and its lease are checked again when you select an option.
|
||||
- Visiting a window behind a different fullscreen window exits that fullscreen
|
||||
state so the requested window can actually be seen. Visiting the fullscreen
|
||||
window itself preserves fullscreen.
|
||||
- Revealing a hidden window edits the live draft normally; it never rewrites
|
||||
a checkpoint. Merely receiving or displaying attention changes no model state.
|
||||
|
||||
Lime acknowledges attention when the selected, visible input target actually
|
||||
has X focus, including focus inside a child widget. Another Head's saved focus,
|
||||
root/background focus, and an open command-palette grab do not count. Clients
|
||||
using `WM_TAKE_FOCUS` keep their request until they actually answer by taking
|
||||
focus. Closing the menu after a visit then acknowledges the chosen application.
|
||||
|
||||
Acknowledgement clears only the urgency flag and the demands-attention atom;
|
||||
other `WM_HINTS` fields, fullscreen, and unrelated EWMH state remain intact.
|
||||
Applications can cancel either source independently. Destruction and genuine
|
||||
withdrawal remove the client from the list; stale picker entries cannot revive it.
|
||||
|
||||
## Boundaries
|
||||
|
||||
This is **application attention**, not a desktop notification daemon or an
|
||||
unread-message counter. Applications must publish the relevant X11 hints.
|
||||
A game's fullscreen Head still hides its bar; another visible Head can show
|
||||
the badge, and the menu remains accessible. Disabling the badge or the entire
|
||||
bar does not disable the picker.
|
||||
|
||||
Attention is adapter-local, never part of session snapshots. Surviving X
|
||||
windows have their current hints reread when adopted after a WM restart.
|
||||
Dead windows and old alerts are not resurrected from disk. Hidden
|
||||
`_NET_ACTIVE_WINDOW` requests which Lime declines become attention requests;
|
||||
the existing activation policy for already leased clients is otherwise unchanged.
|
||||
|
||||
Configuration controls the badge's color, marker, and visibility; see
|
||||
[bar appearance](configuration.md#bar-appearance). No personal configuration
|
||||
changes are required for the default badge/menu. This addition changes backend
|
||||
structure and ASDF components: use a fresh Lime process to load it, rather than
|
||||
a function-only hot reload. Your running session is not modified by installing
|
||||
the code.
|
||||
|
||||
## Implementation and tests
|
||||
|
||||
`src/x11/attention.lisp` owns the live source cache and acknowledgement.
|
||||
ICCCM/EWMH adoption and property/client-message handlers feed it; focus effects
|
||||
and normal FocusIn events acknowledge it. Focus queries run only for pending
|
||||
requests, not as an idle poll. Bar/UI code consumes cached queries; the command
|
||||
layer owns visiting and the ordinary Activity/lease transitions.
|
||||
|
||||
The owned-Xvfb smoke test is `test/x11-attention-smoke.lisp`. It covers real
|
||||
ICCCM/EWMH changes and cancellation, both message atom slots, initial hints,
|
||||
NoInput and asynchronous child focus, hidden and inactive Activities, bar clicks,
|
||||
which-key visits, independent Heads, fullscreen preservation, stale selections,
|
||||
and withdrawal/destruction. Server-free tests also cover partial/take collisions,
|
||||
stale choices, checkpoint isolation, and configurable presentation.
|
||||
|
||||
Protocol references: [ICCCM WM_HINTS](https://www.x.org/releases/current/doc/xorg-docs/icccm/icccm.html)
|
||||
and [EWMH window state](https://specifications.freedesktop.org/wm/1.5/ar01s05.html).
|
||||
674
docs/configuration.md
Normal file
674
docs/configuration.md
Normal file
@@ -0,0 +1,674 @@
|
||||
# Configuration
|
||||
|
||||
Lime’s configuration is a user-owned Common Lisp file—not a source-tree fork
|
||||
and not a collection of patches. Load `lime/x11`, then start through
|
||||
`lime.config:start-lime` with the file path you want to own, for example
|
||||
`~/.config/lime/config.lisp`.
|
||||
|
||||
```lisp
|
||||
;;;; ~/.config/lime/config.lisp
|
||||
(in-package #:cl-user)
|
||||
|
||||
(lime.config:defconfiguration my-lime
|
||||
(:options :default-keybindings t
|
||||
:default-modifiers '(:mod-4)
|
||||
:default-layout :master-stack
|
||||
;; New tiles append at the bottom of master/stack, but receive
|
||||
;; focus; closing one returns focus toward master.
|
||||
:new-client-focus t
|
||||
:new-tiled-client-position :back
|
||||
:close-focus-direction :previous)
|
||||
|
||||
(:command terminal
|
||||
:label "Terminal"
|
||||
:description "launch Alacritty"
|
||||
:category "Launch"
|
||||
:key #\t
|
||||
:action (lambda (backend)
|
||||
(declare (ignore backend))
|
||||
(uiop:launch-program '("alacritty") :output :interactive)))
|
||||
(:command reload-config
|
||||
:label "Reload configuration"
|
||||
:description "reload this file without restarting Lime"
|
||||
:key #\r
|
||||
:action #'lime.config:reload-configuration)
|
||||
(:command palette
|
||||
:label "Command palette"
|
||||
:description "open Lime's command picker"
|
||||
:action #'lime.x11:backend-open-command-palette)
|
||||
|
||||
;; This replaces the starter map's Super-t binding with TERMINAL.
|
||||
(:bind #\t terminal :modifiers '(:mod-4))
|
||||
(:bind #\y palette :modifiers '(:mod-4))
|
||||
(:bind #\r reload-config :modifiers '(:mod-4 :shift))
|
||||
|
||||
;; Rules are ordered. The first match styles an ordinary newly mapped
|
||||
;; client; strings are case-insensitive exact ICCCM WM_CLASS matches.
|
||||
(:rule :class "mpv" :float t :center t)
|
||||
(:rule :class "Pavucontrol" :float t :center t)
|
||||
|
||||
(:font :family "monospace" :size 14 :weight :normal)
|
||||
(:bar :height 22
|
||||
:status-providers (list (lime.x11:make-clock-status-provider)))
|
||||
(:appearance :border-width 3
|
||||
:focused-border "#ffac54" :unfocused-border "#121f25"
|
||||
:inner-gap 5 :outer-gap 5))
|
||||
```
|
||||
|
||||
```lisp
|
||||
(asdf:load-system "lime/x11")
|
||||
(lime.config:start-lime
|
||||
:display-name ":99"
|
||||
:config-file #P"/home/adan/.config/lime/config.lisp")
|
||||
```
|
||||
|
||||
`Super-y`, the clickable `Lime` bar item, and a command’s optional
|
||||
`:key` all meet in the same command registry. `Super-p` is deliberately kept
|
||||
for the immediate dmenu_run-like PATH-program picker. `:key` places a command under the
|
||||
palette’s `User` which-key branch; it does not make an X key grab by itself.
|
||||
Use `:bind` for that. Commands are ordinary functions, so anything beyond this
|
||||
small DSL is just Common Lisp calling Lime's public command functions.
|
||||
|
||||
The program picker caches PATH executable names. It refreshes when PATH or a
|
||||
directory modification date changes, or after five minutes on the next open
|
||||
(covering executable-permission edits). For immediate rediscovery from the REPL,
|
||||
use `(lime.x11::%path-program-names :refresh-p t)`. Opening shortcuts queue rapid
|
||||
follow-up keystrokes safely until the menu takes over, including cold launches.
|
||||
|
||||
## Numbered navigation
|
||||
|
||||
The starter map binds `Super-1` through `Super-9`, then `Super-0`, to Activity
|
||||
positions 1–10 in the Activity bar order. Add Shift to select View slots 1–10
|
||||
**inside the current Activity**.
|
||||
For a complete personal keymap with starter bindings disabled, opt in with:
|
||||
|
||||
```lisp
|
||||
(:navigation-keys)
|
||||
;; Or use another modifier family:
|
||||
;; (:navigation-keys :modifiers '(:mod-1))
|
||||
```
|
||||
|
||||
This clause expands into ordinary named commands and bindings. It inherits
|
||||
`:default-modifiers` unless explicitly overridden. For individual custom keys,
|
||||
commands can call `backend-select-activity-slot` or `backend-select-view-slot`.
|
||||
The clause uses command ids `:navigate-activity-1` … `:navigate-activity-10`
|
||||
and `:navigate-view-1` … `:navigate-view-10`.
|
||||
|
||||
Numbers are durable navigation metadata, not identities or a ten-Activity
|
||||
limit. The bar shows Activity names only, such as `Work`; the picker retains each
|
||||
Activity/View's number for reference. The bar lists Activities in slot order.
|
||||
Saved Views are arrangements to recall, not subworkspaces: no View name,
|
||||
Draft label, or dirty marker appears in the bar. Active text
|
||||
uses the configured `:accent-color` (falling back to `:focused-foreground`);
|
||||
inactive text uses `:foreground`, without changing background colors.
|
||||
Click a name to use the same portable/extended navigation as the number keys.
|
||||
Clicking the already active name restores client
|
||||
focus without resetting its arrangement. Long lists retain the active tab
|
||||
where space permits and use a clickable `…` Activity picker for overflow;
|
||||
title and status areas remain bounded. Custom commands can open the same
|
||||
picker with `backend-open-activity-picker`.
|
||||
|
||||
Activity numbers are contiguous positions: deleting an Activity shifts later
|
||||
ones left, and new Activities append to the end. Existing order is otherwise
|
||||
preserved, including across restarts; older sparse assignments are compacted
|
||||
in their existing bar order. No Activity identity, window, View, or lease changes.
|
||||
On narrow bars, the `…` picker shows the full order, including clipped tabs.
|
||||
|
||||
Drafts have no View number. View slots remain stable and local: new Views take
|
||||
the lowest free slot and deleting one does not renumber the others. Previously
|
||||
unnumbered sessions start with Scratch on 1, then Activities alphabetically;
|
||||
existing Views use library order. Beyond position/slot 10, use name search or
|
||||
reassign a shortcut. Empty shortcuts do nothing. Session persistence saves
|
||||
navigation metadata across restarts; without it, the order lasts for this runtime.
|
||||
|
||||
New Activities are portable: selection brings their current arrangement here,
|
||||
including unsaved edits. If already displayed elsewhere, the two monitors swap
|
||||
arrangements; an inactive Activity changes only the selected monitor. Numbered selection of the current Activity
|
||||
or View is a no-op, not a reset. Selecting another View recalls its checkpoint.
|
||||
The menu's explicit `Recall view...` restores a checkpoint even when it was the
|
||||
last one recalled. For portable Activities, recalling a checkpoint on another
|
||||
monitor first brings that Activity's arrangement there; lease conflicts are
|
||||
checked before either step, so cancelling does not swap monitors. A View still
|
||||
cannot be mounted twice. Extended Activities retain independent Head-local
|
||||
recall and explicit partial/take lease choices.
|
||||
Both `backend-activate-activity` and `backend-switch-to-activity` use the same
|
||||
current working arrangement. Activity navigation never reloads a checkpoint
|
||||
or retries an old window claim. Moving a window updates its previous live
|
||||
location even while that Activity is undisplayed. Hidden windows remain hidden
|
||||
when an arrangement travels; ordinary navigation never gathers the whole pool.
|
||||
|
||||
`Super-y → a → e` chooses a monitor to **extend** the current Activity onto.
|
||||
Its existing portion stays put; a new portion is empty, or an existing portion
|
||||
resumes. That Activity then navigates independently per Head, even when one
|
||||
portion becomes empty. `Super-y → a → p` makes the current portion portable
|
||||
again once other portions are empty and inactive; it preserves saved Views
|
||||
and hidden windows. Both actions are also searchable commands.
|
||||
Noninteractive callers use `backend-extend-activity-to-head` and
|
||||
`backend-make-activity-portable`; `backend-activity-navigation-mode` reports
|
||||
`:portable` or `:extended`. The startup Scratch desk retains its initial blank
|
||||
per-Head portions; newly created named Activities default to portable.
|
||||
|
||||
Sending a window to a portable Activity targets its real arrangement, even on
|
||||
another monitor. It appears there if that arrangement is active, otherwise it
|
||||
is queued there; focus stays with the sending monitor. Extended destinations
|
||||
still use the sending monitor's portion.
|
||||
|
||||
`Super-y → v` contains saved-arrangement actions: `s` saves a new named
|
||||
checkpoint, `o` recalls one, and `u` chooses a saved view in the current Activity
|
||||
to replace. Updating requires a named confirmation (Cancel is selected first)
|
||||
and rejects stale choices if the desk changes. The starter `Super-Shift-s`
|
||||
binding opens this same picker via `backend-open-view-update-picker`. Advanced
|
||||
Lisp callers can still explicitly use `backend-update-view` or
|
||||
`backend-update-current-view`; the UI no longer silently targets the last recall.
|
||||
|
||||
For interactive monitor sends, bind a command calling
|
||||
`(lime.x11:backend-send-focused-client-relatively backend :next)` (the starter
|
||||
`Super-Shift-e` binding). It transfers same-Activity/shared windows immediately
|
||||
and opens a Cancel-first confirmation for a private cross-Activity move.
|
||||
Successful sends follow the window with keyboard focus and route subsequent
|
||||
commands/new windows to its destination Head. Cancelling leaves focus unchanged.
|
||||
The same entry point works inside configured menu commands. Strict non-prompting
|
||||
`backend-transfer-focused-client-*` APIs still reject private cross-Activity
|
||||
transfers; they never silently change home membership.
|
||||
|
||||
Shortcuts can be reassigned through the REPL; occupied assignments swap.
|
||||
Activity positions must be within 1 through the Activity count, so reordering
|
||||
cannot create a gap. View slots may still be any positive integer:
|
||||
|
||||
```lisp
|
||||
(lime.dev:with-development-backend (backend)
|
||||
(lime.x11:backend-set-activity-slot backend "work" 2)
|
||||
(lime.x11:backend-set-view-slot backend "work/docs" 3)
|
||||
(lime.x11:backend-navigation-slots backend))
|
||||
```
|
||||
|
||||
## Browser launch profiles
|
||||
|
||||
The built-in Waterfox command and the PATH picker use the browser's normal
|
||||
profile selection, like running `waterfox` in a terminal. Testing alongside
|
||||
another graphical session can explicitly opt into an isolated profile:
|
||||
|
||||
```lisp
|
||||
(lime.x11:backend-launch-browser backend :test-profile-p t)
|
||||
```
|
||||
|
||||
That test profile lives in Lime's XDG state directory (`lime/waterfox-profile/`)
|
||||
and intentionally has separate tabs, settings, and extensions. It is never
|
||||
selected implicitly for normal desktop use.
|
||||
|
||||
Super-y opens the compact key map first: `w → +Windows` enters the Window
|
||||
group, for example. Space or Tab opens command search, Backspace goes up a
|
||||
menu level, and Escape closes. Tab in search returns to the root map. Large
|
||||
User groups can be paged with PgUp/PgDn. The dropdown uses the bar's configured
|
||||
background, foreground, focus, separator, and edge colors, plus the shared
|
||||
font below; it no longer has a separate hard-coded black/white appearance.
|
||||
|
||||
## Configuration versus session state
|
||||
|
||||
Your configuration defines policy and commands. Activities, saved Views,
|
||||
Viewsets, and drafts are automatically stored separately when using `lime/dev`.
|
||||
Choose `:session-file :default`, a pathname, or `nil` on the session launcher;
|
||||
the lower-level `lime.config:start-lime` defaults to no session persistence.
|
||||
Reloading this configuration does not reload or replace the desk. See the
|
||||
[persistence guide](persistence.md), including the distinction between restarting
|
||||
Lime and relaunching applications after logout.
|
||||
|
||||
## Status widgets
|
||||
|
||||
Opt into managed providers in your own configuration; no music/audio services
|
||||
are started by Lime's starter configuration:
|
||||
|
||||
```lisp
|
||||
(:bar :height 22
|
||||
:status-providers
|
||||
(list (lime.x11:make-music-status-provider
|
||||
:source :mpd :sources '(:mpd "spotify")
|
||||
:mpd-host "127.0.0.1" :mpd-port 6600 :maximum-width 320
|
||||
:formatter (lambda (data)
|
||||
(lime.x11:format-music-status data
|
||||
:playing-icon "" :paused-icon "" :icon-gap " ")))
|
||||
(lime.x11:make-volume-status-provider
|
||||
:foreground nil ; inherit the bar's color, or set "#RRGGBB"
|
||||
:formatter (lambda (data)
|
||||
(lime.x11:format-volume-status data
|
||||
:icon "" :muted-icon "" :icon-gap " ")))
|
||||
(lime.x11:make-clock-status-provider
|
||||
:formatter (lambda (data)
|
||||
(lime.x11:format-clock-status data
|
||||
:icon "" :icon-gap " " :twenty-four-hour-p nil)))))
|
||||
```
|
||||
|
||||
Subscriptions publish **data**, not display strings. The configuration owns
|
||||
the ordered provider list, formatters, icons, gaps, optional per-widget colors,
|
||||
width limits, and click actions. Rendering still owns the overall left/title/
|
||||
right bar geometry; this is not yet an arbitrary bar-layout DSL.
|
||||
|
||||
Each `:formatter` is a fast function of one cached data plist:
|
||||
|
||||
| Provider | Snapshot fields |
|
||||
| --- | --- |
|
||||
| Music | `:state` (`:playing`, `:paused`, `:stopped`, `:unknown`), `:title` (string/NIL), `:source` (`:mpd` or MPRIS player name) |
|
||||
| Volume | `:volume` (first-channel integer percentage), `:muted-p`, `:sink` (output name) |
|
||||
| Clock | `:universal-time`, `:minute`, `:hour` (0–23), `:day`, `:month`, `:year`, `:weekday` (Monday=0), `:daylight-p`, `:timezone` (CL hours west of GMT) |
|
||||
|
||||
The clock publishes at minute resolution, even if a custom formatter decodes
|
||||
seconds from the initial timestamp. NIL source data means unavailable: the
|
||||
widget is hidden and its formatter is not called until data arrives.
|
||||
|
||||
A formatter may return a string, NIL to hide available data, or a
|
||||
`head-bar-segment`. For example, this configuration can color muted volume
|
||||
differently without making the source know anything about themes:
|
||||
|
||||
```lisp
|
||||
:formatter
|
||||
(lambda (data)
|
||||
(lime.x11:make-head-bar-segment
|
||||
:text (lime.x11:format-volume-status data :icon-gap " ")
|
||||
:foreground (if (getf data :muted-p) "#9f9584" "#d8c9ad")))
|
||||
```
|
||||
|
||||
Segment foreground/width/click overrides take precedence over the provider's
|
||||
defaults; NIL fields inherit those defaults. Provider `:foreground nil`
|
||||
inherits the bar foreground. Colors must be `#RRGGBB`. Invalid formatter
|
||||
output or a formatter error hides only that provider and records a diagnostic;
|
||||
other widgets and subscriptions keep running. Formatters run on the X thread,
|
||||
outside worker locks, on a defensive copy. They must not block or do I/O.
|
||||
|
||||
The `format-*-status` functions are optional presentation helpers in
|
||||
`src/x11/status-format.lisp`, not part of the subscription protocols. Supply
|
||||
your own formatter for a different date format, stopped-track display, labels,
|
||||
or icons (NIL/empty icons omit both icon and gap). Helpers default to one space
|
||||
for music and two for volume/date. `:twenty-four-hour-p` now belongs to
|
||||
`format-clock-status`, not `make-clock-status-provider`.
|
||||
|
||||
Pass `:icon-foreground "#76929f"` to any of these helpers for a colored icon
|
||||
with an independently colored label. With this option the helper returns a
|
||||
`head-bar-segment` instead of a string; the label still inherits the provider
|
||||
or bar foreground. Without it, the existing string/NIL behavior is unchanged.
|
||||
For example:
|
||||
|
||||
```lisp
|
||||
:formatter (lambda (data)
|
||||
(lime.x11:format-volume-status data
|
||||
:icon-foreground "#76929f" :icon-gap " "))
|
||||
```
|
||||
|
||||
Custom segments can use `:prefix-length` (Lisp character count, not UTF-8
|
||||
bytes) and `:prefix-foreground` for the same effect. The icon and label remain
|
||||
one shaped item and one click target; fitting never colors truncation dots as
|
||||
part of the icon. Text is still plain text, not markup.
|
||||
|
||||
Music uses a native MPD connection with `idle player playlist`, or a long-lived
|
||||
`playerctl --follow` process for an MPRIS player name such as `"spotify"` or
|
||||
`"brave"`. Default formatting hides stopped players; a custom formatter can
|
||||
show their retained data. Disconnected players disappear. Paused tracks retain
|
||||
their pause indicator. The song's pixel budget truncates at grapheme boundaries,
|
||||
leaving room for the following widgets. Volume uses `pactl subscribe` and
|
||||
queries the default output only initially and on relevant events. It follows
|
||||
mute, volume, output changes, and hotplug; the first channel percentage matches
|
||||
the old dwm script. Queued bursts are coalesced without a debounce delay.
|
||||
The clock shows local date/time and recomputes the next wall-clock minute
|
||||
boundary after every wake, not a repeating sleep measured from startup.
|
||||
It is a date/time widget, not a calendar/appointment application.
|
||||
|
||||
The external event bridges need `pactl` (PulseAudio or PipeWire-Pulse) and
|
||||
`playerctl` (only when using MPRIS); MPD controls use `mpc`. The built-in desktop
|
||||
sources currently target SBCL/Linux. MPD host/port/password are explicit
|
||||
constructor options, not inferred by reading a shell script. MPRIS and audio
|
||||
inherit the session's D-Bus/audio environment; a nested Xephyr session will
|
||||
normally share the host user's player and volume. No controls are invoked
|
||||
merely by displaying the widgets.
|
||||
|
||||
Click music to play/pause; click volume to toggle mute. Each constructor accepts
|
||||
an optional `:click-action` function of `(backend head-id)` to customize this.
|
||||
The clock has no default click action. Bind these commands through your usual
|
||||
configuration command registry:
|
||||
|
||||
```lisp
|
||||
(lime.x11:backend-cycle-music-source backend) ; :mpd -> "spotify" -> :mpd
|
||||
(lime.x11:backend-music-command backend :toggle) ; also :next / :previous
|
||||
```
|
||||
|
||||
The widget and controls share the same selected source. An optional `:id`
|
||||
supports multiple music providers; commands accept the corresponding `:id`.
|
||||
Reloading a newly evaluated music constructor resets its source to the
|
||||
configuration's `:source`. Your dwm scripts remain independent.
|
||||
|
||||
Managed providers share one cached snapshot across Heads. Subscription failures
|
||||
clear stale data and reconnect with bounded 1–30 second backoff. Queries have
|
||||
timeouts, and reload/removal/quit cancel and join old workers, kill/reap their
|
||||
owned helpers, and close sockets. Set `:status-providers nil` explicitly to
|
||||
remove all providers; omitting the option preserves the previous setting.
|
||||
To inspect connection errors through SLY:
|
||||
|
||||
```lisp
|
||||
(lime.dev:with-development-backend (backend)
|
||||
(lime.x11:backend-status-provider-report backend))
|
||||
```
|
||||
|
||||
Fast plain `(backend head-id)` provider functions returning `head-bar-segment`
|
||||
values still work. They run during drawing, so must not perform blocking I/O
|
||||
and do not schedule their own refresh. For a Lisp-only scheduled source,
|
||||
`make-status-provider` accepts `:id`, `:run`, `:click-action`, and optional
|
||||
`:formatter`, `:foreground`, and `:maximum-width`. Its worker function uses
|
||||
`status-publish` (scalar/string/list data, or NIL for unavailable),
|
||||
`status-wait` (interruptible deadline), and `status-cancelled-p`; it must honor
|
||||
cancellation, never mutate the WM/backend, and never draw from its thread.
|
||||
The built-in I/O sources additionally register short resource-cancellation
|
||||
callbacks through the internal runtime boundary.
|
||||
|
||||
Publication and formatting copy data trees, so mutating a producer's input or
|
||||
a formatter's argument cannot change the worker cache. Formatters are never
|
||||
called by the publisher. Generic providers default to the identity formatter,
|
||||
so existing string-publishing providers continue to work. Reports expose both
|
||||
`:value` (raw data) and `:text` (formatted output), plus source/formatter errors.
|
||||
Changing a formatter on an existing provider can redraw the cache without
|
||||
restarting its worker; evaluating new constructor objects during config reload
|
||||
still replaces those providers using the normal lifecycle.
|
||||
|
||||
## Fonts
|
||||
|
||||
The bar and dropdown share one font setting, independent of window layouts:
|
||||
|
||||
```lisp
|
||||
(:font :family "monospace" :size 14 :weight :normal)
|
||||
```
|
||||
|
||||
These are the defaults. `:size` is a pixel size (1–96), not points; `:weight`
|
||||
is `:normal` or `:bold`. Family names are installed Fontconfig font families,
|
||||
not X11 core-font names or Xft patterns such as `Dina:size=11`.
|
||||
Comma-separated families give a preferred fallback order, for example:
|
||||
|
||||
```lisp
|
||||
(:font :family "Dina, Fira Code Nerd Font, Noto Color Emoji"
|
||||
:size 15 :weight :bold)
|
||||
```
|
||||
|
||||
Pango/Cairo supplies antialiasing, Unicode shaping, bidirectional text, and
|
||||
system font fallback. Japanese, emoji (including color emoji), and Nerd Font
|
||||
icons work when suitable fonts are installed. A missing family falls back
|
||||
to a system font; a character missing from every installed font may still
|
||||
show a missing-glyph box. Lime no longer converts Unicode to `?`.
|
||||
|
||||
Font changes reload live, including an already-open dropdown. Its rows resize
|
||||
to the font; the bar keeps its explicitly configured `:height`, so increase
|
||||
that too for larger fonts. Fitting uses measured glyphs and truncates only at
|
||||
grapheme boundaries, preserving combining accents and joined emoji. This is
|
||||
rendering support, not an input-method/IME implementation.
|
||||
|
||||
The first upgrade from the old core-font renderer changes native resources
|
||||
and structure definitions: restart the Lisp/Xephyr test session once. Later
|
||||
font **configuration** changes use the ordinary configuration reload.
|
||||
|
||||
## Cursors
|
||||
|
||||
The desktop arrow, clickable bar/menu hand, and cooperating applications share
|
||||
a configurable Xcursor theme:
|
||||
|
||||
```lisp
|
||||
(:cursor :theme nil :size nil) ; defaults: follow the system
|
||||
;; Or choose an installed theme and nominal pixel size explicitly:
|
||||
(:cursor :theme "Adwaita" :size 24)
|
||||
```
|
||||
|
||||
Use just one `:cursor` clause. `:theme` is a theme directory name, not a font
|
||||
or file path; `:size` is an integer from 8 to 256, or NIL for automatic sizing.
|
||||
Explicit config wins over `XCURSOR_THEME` / `XCURSOR_SIZE`, which win over the
|
||||
current X screen's `Xcursor.theme` / `Xcursor.size` Xresources. No environment
|
||||
variables are required. Without those, the theme is Xcursor's `default`
|
||||
(including `default/index.theme` inheritance); size uses `Xft.dpi` when
|
||||
available, otherwise 24px. The theme supplies its nearest available size.
|
||||
|
||||
Installed cursor directories, `XCURSOR_PATH`, and theme inheritance are resolved
|
||||
by libXcursor. A missing asset/theme may fall back to the system default theme.
|
||||
If libXcursor, ARGB-capable X Render, or usable cursor images are unavailable,
|
||||
Lime falls back to built-in X cursors. `:theme :core` requests those explicitly.
|
||||
Inspect the selected settings and each role's source/fallback reason with
|
||||
`(lime.x11:backend-cursor-report backend)`; the reported theme is the requested
|
||||
search theme, not proof that an asset came from that directory.
|
||||
|
||||
Theme images retain alpha and hotspots. Animated assets use X Render's server
|
||||
animation support (or their first frame on older servers), not a WM timer.
|
||||
|
||||
Explicit `:theme` / `:size` values also become **session-local application
|
||||
defaults**. Lime publishes cursor-only XSETTINGS for GTK and compatible toolkits,
|
||||
updates `Xcursor.theme` / `Xcursor.size` in the current X screen's resources,
|
||||
and passes `XCURSOR_THEME` / `XCURSOR_SIZE` to programs launched through Lime.
|
||||
You still configure these in Lisp; no environment setup is required. Lime does
|
||||
not rewrite GTK/Qt files, dconf, the global launch environment, or another X
|
||||
session. Application-specific cursors (text, resize, drag, etc.) retain their
|
||||
roles; applications that deliberately override the theme can still do so.
|
||||
|
||||
Lime never replaces an existing XSETTINGS manager and yields if one takes over.
|
||||
The `:session` entry in `backend-cursor-report` reports `:published`,
|
||||
`:external-manager`, or `:following-system` under `:xsettings`. With an external
|
||||
manager, configure its cursor theme too if its toolkit settings disagree.
|
||||
|
||||
Only explicit values are published: a NIL field follows its ordinary defaults.
|
||||
Returning both fields to NIL, choosing `:theme :core`, or exiting Lime releases
|
||||
its manager and restores the Xresources it still owns, preserving intervening
|
||||
external edits. Some applications cache cursor settings and need reopening;
|
||||
in particular, GTK 3 can retain a removed setting until restart even though
|
||||
changes between explicit themes/sizes update live.
|
||||
|
||||
After the initial structural upgrade/restart, theme/size changes reload live,
|
||||
including a hovered widget or an open dropdown. New cursor resources are
|
||||
prepared first; a later config failure restores the old resources, while a
|
||||
successful reload releases them. Session publication participates in the same
|
||||
rollback. No activity or view is recreated.
|
||||
|
||||
## Bar appearance
|
||||
|
||||
The bar has three regions: `Lime | activities | layout` on the left,
|
||||
a left-aligned expanding title section, and optional right-aligned status
|
||||
providers. Only the title section changes colors with Head focus. Its two
|
||||
one-pixel edges are deliberately quieter than the colored text separators.
|
||||
|
||||
Bar colors and padding can be reloaded without restarting or changing a saved View:
|
||||
|
||||
```lisp
|
||||
(:bar :height 22 :padding-x 10
|
||||
:background "#060a0c" :foreground "#ffdbb0"
|
||||
:focused-background "#121f25" :focused-foreground "#ffac54"
|
||||
:unfocused-background "#0b1216" :unfocused-foreground "#a8adb0"
|
||||
:separator-color "#b88755"
|
||||
:attention-color "#ffac54"
|
||||
:attention-marker "!" :attention-indicator-p t
|
||||
:title-edge-color "#40535d")
|
||||
```
|
||||
|
||||
These are the defaults. Use `:title-edge-color nil` for the edge-free variant.
|
||||
Use `:logo-color "#768565"` to color the clickable `Lime` label independently
|
||||
of the Activity and layout labels. NIL (the default) inherits `:foreground`.
|
||||
Optional `:accent-color` separates menu key hints, hover/selection markers, and
|
||||
which-key headings from the focused title's foreground. `:menu-label-color`
|
||||
separates submenu labels from the bar's separators. Both default to NIL to
|
||||
preserve the old colors (`:focused-foreground` and `:separator-color`,
|
||||
respectively; headings retain `:foreground`). For cream titles and labels
|
||||
with yellow hints, add:
|
||||
|
||||
```lisp
|
||||
:focused-foreground "#dbc9a6"
|
||||
:accent-color "#ffb054"
|
||||
:menu-label-color "#dbc9a6"
|
||||
```
|
||||
|
||||
These new roles and separately colored status prefixes require one fresh Lime
|
||||
process when upgrading from the older renderer. Subsequent color-only config
|
||||
edits can be reloaded normally.
|
||||
|
||||
The attention indicator appears as `! N` ahead of the optional status widgets
|
||||
only while windows request attention. It counts windows across **all** Activities
|
||||
and appears on every visible Head bar, including when their Activity is parked.
|
||||
Click it, or use `Super-y → w → a`, to search requesting windows by Activity
|
||||
and current title. Activity-search details and hidden-window entries also mark
|
||||
requests. No new global keybinding is installed.
|
||||
|
||||
`:attention-color` and `:attention-marker` control that badge; the marker is
|
||||
1–16 printable characters. `:attention-indicator-p nil` hides the bar badge,
|
||||
not the protocol handling or menu. The public cached-data queries
|
||||
`backend-attention-client-ids` (optional `:activity-id`) and
|
||||
`backend-client-needs-attention-p` are available to custom Lisp UI/providers.
|
||||
`backend-open-attention-picker` opens the picker, and `backend-attend-client`
|
||||
performs the same visit without a menu (`:policy :strict` by default).
|
||||
See [attention behavior](attention.md) for acknowledgement and lease rules.
|
||||
|
||||
`:padding-x` is horizontal text padding in pixels (0–128): the outer left and
|
||||
right margins and the title section's inset
|
||||
inside its hairlines. The bar vertically centers a stable sample of the font's
|
||||
letter body, rather than its invisible line spacing. All fields share a
|
||||
baseline, so changing titles or adding descenders does not make text jump.
|
||||
Accents and descenders retain their normal positions relative to that baseline;
|
||||
increase `:height` if the chosen font or fallback glyphs need more room.
|
||||
|
||||
Colors must be `#RRGGBB`; invalid settings fail before changing the running
|
||||
configuration. A failed installation restores the old bar appearance along
|
||||
with bindings and window styling. Lisp callers can use `MAKE-HEAD-BAR-STYLE`
|
||||
and the configuration's `BAR-STYLE` slot directly.
|
||||
|
||||
Status providers still supply their own text and optional click actions.
|
||||
The bar draws contrasting ` | ` delimiters between their items; those
|
||||
delimiters and truncation dots are not clickable. Appearance settings alone
|
||||
do not enable providers; opt into the widgets above separately. Providers may use text,
|
||||
Unicode symbols, or icons covered by the configured/fallback fonts.
|
||||
|
||||
## Default layout
|
||||
|
||||
`:options :default-layout` chooses `:master-stack` (the default), `:monocle`,
|
||||
or `:manual` for a **new** working arrangement. This includes the initial
|
||||
Scratch drafts, new Activities, and newly extended portions. Bringing or resuming
|
||||
an existing draft or recalling a saved View keeps its layout. Reloading this
|
||||
option changes future drafts only; it never rearranges your current work.
|
||||
|
||||
The configuration is read before startup drafts are created. Without a config
|
||||
file, `start-lime`/`start-backend` also accept `:default-layout`; with a file,
|
||||
the file's setting wins.
|
||||
|
||||
## New-window and close policy
|
||||
|
||||
The `:options` clause also keeps client-flow preference out of the layout
|
||||
engine. `:new-client-focus` controls whether ordinary newly managed windows
|
||||
take focus. `:new-tiled-client-position` is `:back` (append after the existing
|
||||
stack) or `:front` (become master-most). `:close-focus-direction` is
|
||||
`:previous` (toward master in placement order) or `:next` (farther down the
|
||||
stack); reaching either edge falls back to the closest client on the other
|
||||
side. These are the fallback defaults for manual and master/stack: `t`,
|
||||
`:back`, and `:previous`.
|
||||
|
||||
Each layout can override only the flow choices it needs:
|
||||
|
||||
```lisp
|
||||
(:layout-flow :monocle
|
||||
:new-tiled-client-position :front
|
||||
:close-focus-direction :next)
|
||||
```
|
||||
|
||||
That is Lime's built-in monocle policy already: each new tiled client becomes
|
||||
the visible one, and closing it advances to the next underlying client. A
|
||||
`:layout-flow` clause is useful when you want a different monocle cycle, or a
|
||||
separate policy for `:manual` or `:master-stack`.
|
||||
|
||||
## Resizing a window within a column
|
||||
|
||||
Master/stack gives each tiled placement a relative weight, initially `1`.
|
||||
Two windows weighted `2` and `1` take two-thirds and one-third of their column.
|
||||
The master area and stack normalize separately, leaving their shared divider
|
||||
unchanged. This also works with several master windows, and along the other
|
||||
axis when the layout is rotated.
|
||||
|
||||
Configure the commands and step size in your own keymap, for example:
|
||||
|
||||
```lisp
|
||||
(:command grow-tile
|
||||
:label "Grow window in column"
|
||||
:action (lambda (backend)
|
||||
(lime.x11:backend-adjust-focused-tile-weight backend 1/4)))
|
||||
(:command shrink-tile
|
||||
:label "Shrink window in column"
|
||||
:action (lambda (backend)
|
||||
(lime.x11:backend-adjust-focused-tile-weight backend -1/4)))
|
||||
(:command reset-tile-size
|
||||
:label "Reset window column share"
|
||||
:action (lambda (backend)
|
||||
(lime.x11:backend-set-focused-tile-weight backend 1)))
|
||||
(:bind #\d grow-tile :modifiers '(:mod-4 :shift))
|
||||
(:bind #\n shrink-tile :modifiers '(:mod-4 :shift))
|
||||
(:bind #\o reset-tile-size :modifiers '(:mod-4 :shift))
|
||||
```
|
||||
|
||||
The adjust command defaults to weights between `1/4` and `4`; callers can
|
||||
override `:minimum-weight` and `:maximum-weight`. Reset affects the focused
|
||||
window only. A sole window still fills its column.
|
||||
|
||||
Weights belong to placements in the current draft and are checkpointed by
|
||||
normal View save/update operations. They follow windows through tile moves,
|
||||
float/unfloat, and layout changes. Monocle ignores the weights. Hiding removes
|
||||
the placement; showing the window again creates a default-weight placement
|
||||
unless a saved View restores its previous arrangement.
|
||||
|
||||
## Borders and gaps
|
||||
|
||||
The `:appearance` clause configures window styling independently of layout
|
||||
policy. Sizes are non-negative integer pixels and colors are `#RRGGBB` strings.
|
||||
`:inner-gap` is the space **between** tiled outer frames, not a margin on each
|
||||
window; `:outer-gap` is the space between tiles and the Head's work-area edge
|
||||
(below the bar). Border widths are included inside the allotted rectangle,
|
||||
so they cannot make neighboring tiles overlap.
|
||||
|
||||
The implementation lives in `src/x11/appearance.lisp`; the render planner
|
||||
combines it with ICCCM hints, while your configuration owns the actual values.
|
||||
|
||||
Only the globally focused client gets `:focused-border`; windows on other
|
||||
Heads use `:unfocused-border`. Floating windows have borders but no tiling
|
||||
gaps. Smart gaps remove both gaps and borders whenever a Head displays just
|
||||
one tiled window, in manual, master/stack, or monocle. Floats above it keep
|
||||
their borders and do not count as tiles; explicitly hidden clients and tiles
|
||||
leased to other Heads do not count either. A second visible tile restores the
|
||||
configured borders and gaps automatically. Monocle always resolves just one
|
||||
tile, so it remains undecorated even with several clients in its cycle.
|
||||
These tiles still respect the bar's work area and normal size hints. True
|
||||
fullscreen bypasses decoration, the bar reservation, and size hints. Size hints may
|
||||
leave additional unused space, but cannot enlarge a tile beyond its slot.
|
||||
On very small tiles, excessive gaps/borders shrink to leave usable content.
|
||||
|
||||
Changing these settings and reloading configuration repaints existing windows
|
||||
without changing layout weights, saved Views, leases, or draft dirty state.
|
||||
Setting a size to `0` disables that component. Without an `:appearance` clause,
|
||||
the adapter retains its borderless/gapless defaults; default border colors
|
||||
are white (`#ffffff`) and gray (`#444444`) if only a width is specified.
|
||||
|
||||
## Reloading
|
||||
|
||||
`lime.config:reload-configuration` rereads the source file selected at start.
|
||||
It validates commands, bindings, rules, appearance, and bar inputs before changing the
|
||||
running backend. If a subsequent installation operation fails, Lime restores
|
||||
the previous binding set, appearance, and bar settings; Activities, Views, Presentations,
|
||||
leases, and Viewsets are never re-created or changed by a reload. `:startup`
|
||||
hooks run only at initial `start-lime`, never on reload.
|
||||
|
||||
Because a config file is real Lisp, arbitrary top-level side effects are still
|
||||
the author’s responsibility. Keep top-level work to helper definitions and
|
||||
`defconfiguration`; place deliberate launches in a `(:startup (lambda
|
||||
(backend) ...))` clause when that behavior is wanted.
|
||||
|
||||
## Rules, deliberately for styling first
|
||||
|
||||
`(:rule ...)` supports `:name`, `:class`, and `:title` matchers (a string or a
|
||||
one-argument predicate), plus a full `:predicate` function of `(backend
|
||||
client-id icccm-info)`. Its current actions are `:float t/nil` and `:center t`.
|
||||
Centering requires floating. The first matching rule wins.
|
||||
|
||||
Ordinary clients default to tiled, except fixed-size windows (equal positive
|
||||
minimum and maximum width **and** height in `WM_NORMAL_HINTS`), which start
|
||||
centered and floating. This handles startup/updater windows without floating
|
||||
the application's resizable main window. A matching `:float nil` rule can
|
||||
override that default. These defaults apply only when a client is first
|
||||
managed; later hint updates do not reclassify existing placements. The usual
|
||||
`:new-client-focus` setting applies to fixed-size ordinary clients too.
|
||||
|
||||
Rules apply only to ordinary new clients. ICCCM transient dialogs keep their
|
||||
parent’s Head, Activity, and float behavior; dialog-like EWMH clients retain
|
||||
their floating policy too. Rules intentionally do **not**
|
||||
mount a View, change an Activity, or transfer a lease: matching an application
|
||||
class must not silently alter Lime’s task model. Activity-aware launch/routing
|
||||
can grow later as a separate policy with explicit collision semantics.
|
||||
221
docs/development.md
Normal file
221
docs/development.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Live development
|
||||
|
||||
`lime/dev` is the safe route for hacking on a live Lime session. It keeps the
|
||||
X11 event loop on one thread and starts a Slynk server bound only to
|
||||
`127.0.0.1`. A connected REPL must send backend work through the developer
|
||||
mailbox, so a source reload or command cannot race CLX event handling.
|
||||
|
||||
It is intended for a nested Xephyr server or a separate X login/session. Do
|
||||
not start Lime against the same X display already owned by dwm (or another
|
||||
window manager): X11 permits exactly one root-window substructure redirect
|
||||
owner.
|
||||
|
||||
For the first non-nested test, keep your normal dwm login intact and use a
|
||||
spare TTY/X server. A minimal `~/.config/lime/xinit-dev` can be:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
exec sbcl --non-interactive --eval '(require :asdf)' \
|
||||
--eval '(asdf:load-asd "/home/adan/code/common-lisp/lime/lime.asd")' \
|
||||
--eval '(asdf:load-system "lime/dev")' \
|
||||
--eval '(lime.dev:run-development-session
|
||||
:display-name (uiop:getenv "DISPLAY")
|
||||
:config-file "/home/adan/.config/lime/config.lisp")'
|
||||
```
|
||||
|
||||
Make that file executable, then start it from another TTY with
|
||||
`startx ~/.config/lime/xinit-dev -- :1 vt8` (adjust the display/VT if either is
|
||||
already in use). Connect SLY from your ordinary session, and keep that
|
||||
ordinary session or a second TTY as your escape hatch. Launch test clients on
|
||||
the new display with `DISPLAY=:1 your-client`, or put deliberate launch forms
|
||||
in your configuration's existing `:startup` hook.
|
||||
|
||||
## Start a session
|
||||
|
||||
The configured `:default-layout` starts fresh Head-local drafts in master/stack
|
||||
unless overridden. Existing drafts and saved Views keep their layouts on
|
||||
configuration reload. The isolated regression test for startup, multi-head
|
||||
first visits, adoption, and reload is:
|
||||
|
||||
```sh
|
||||
sbcl --non-interactive --load test/x11-default-layout-smoke.lisp
|
||||
```
|
||||
|
||||
It requires Xvfb and creates its own test display.
|
||||
|
||||
Floating drag regression (also on its own isolated Xvfb):
|
||||
|
||||
```sh
|
||||
sbcl --non-interactive --load test/x11-floating-smoke.lisp
|
||||
```
|
||||
|
||||
This checks real Super-drag input, burst coalescing and release ordering,
|
||||
checkpoint isolation, size hints/borders, and invalidated gestures. It also
|
||||
compares full-desktop versus geometry-only update timings. Timing is diagnostic;
|
||||
the regression asserts bounded rendering work rather than a hardware-dependent
|
||||
frame rate. Restart once after the drag-state change; ordinary config reload
|
||||
alone does not install the new code/structure.
|
||||
|
||||
From the Lime checkout, start SBCL without `--non-interactive` so its process
|
||||
remains available:
|
||||
|
||||
```sh
|
||||
sbcl --eval '(require :asdf)' \
|
||||
--eval '(asdf:load-asd "/home/adan/code/common-lisp/lime/lime.asd")' \
|
||||
--eval '(asdf:load-system "lime/dev")' \
|
||||
--eval '(lime.dev:start-development-session
|
||||
:display-name ":1"
|
||||
:config-file "/home/adan/.config/lime/config.lisp")'
|
||||
```
|
||||
|
||||
The default Slynk endpoint is `127.0.0.1:4005`; connect Emacs/SLY to it.
|
||||
For a nested test display, replace `:1` with your Xephyr display. The normal
|
||||
Lime configuration is loaded before the event thread starts.
|
||||
|
||||
Every live backend operation from that REPL should use the serialized boundary:
|
||||
|
||||
```lisp
|
||||
(lime.dev:with-development-backend (backend)
|
||||
(lime.x11:backend-status backend))
|
||||
|
||||
(lime.dev:reload-development-configuration)
|
||||
```
|
||||
|
||||
The second form preserves Activities, Views, Presentations, leases, windows,
|
||||
and the current physical-Head mapping. It only reloads user policy such as
|
||||
bindings, commands, rules, and bar providers.
|
||||
|
||||
You can expose the same actions in your development configuration, which makes
|
||||
the palette/key layer useful even before connecting a REPL:
|
||||
|
||||
```lisp
|
||||
(:command reload-lime-source
|
||||
:label "Reload Lime source"
|
||||
:description "Compile and install changed function-level Lime code"
|
||||
:category "Development"
|
||||
:action (lambda (backend)
|
||||
(declare (ignore backend))
|
||||
(lime.dev:reload-development-code)))
|
||||
(:command reload-lime-config
|
||||
:label "Reload Lime config"
|
||||
:description "Reload the active configuration without replacing the desk"
|
||||
:category "Development"
|
||||
:action (lambda (backend)
|
||||
(declare (ignore backend))
|
||||
(lime.dev:reload-development-configuration))
|
||||
```
|
||||
|
||||
## Reload code without resetting your desk
|
||||
|
||||
After a function-level source edit, evaluate:
|
||||
|
||||
```lisp
|
||||
(lime.dev:reload-development-code)
|
||||
```
|
||||
|
||||
This compiles and loads changed files in `lime/x11`, reconciles the current
|
||||
render plan, and returns a `development-reload-report`. A compile failure is
|
||||
reported in that object and the existing event loop/world stay alive.
|
||||
|
||||
Safe reload territory includes ordinary function changes: commands, layout
|
||||
math, renderer code, palette behavior, bar drawing, and configuration helpers.
|
||||
It deliberately does **not** promise structural live migration. Restart after
|
||||
changing `defstruct` slots, package or ASDF topology, backend-thread ownership,
|
||||
or `lime/dev` itself:
|
||||
|
||||
```lisp
|
||||
(lime.dev:restart-development-session)
|
||||
```
|
||||
|
||||
A backend restart now saves/restores the desk through the display-specific
|
||||
session file, enabled by default for `lime/dev`. Surviving windows reconnect
|
||||
with verified identities, including hidden ones. With `:session-file nil`, it
|
||||
still starts a fresh world. See [session persistence](persistence.md) for the
|
||||
logout boundary, custom paths, safe writes, and error recovery.
|
||||
|
||||
The supervisor and existing Slynk listener stay alive through a slow restart.
|
||||
A failed pre-restart save retains the old X connection; repair and retry, or
|
||||
resume the retained backend. If replacement backend startup fails after X has
|
||||
closed, the control session stays paused: repair and call restart again, not
|
||||
resume. Restart from an event-loop error uses the last good disk snapshot;
|
||||
resume instead validates the retained in-memory world.
|
||||
|
||||
Inspect the live runtime with:
|
||||
|
||||
```lisp
|
||||
(lime.dev:development-status)
|
||||
```
|
||||
|
||||
When you are finished, use `(lime.dev:stop-development-session)` from the
|
||||
control REPL. It stops the event thread before closing CLX, remaps managed
|
||||
clients, and closes the Slynk listener.
|
||||
|
||||
## Recovering from an event-loop error
|
||||
|
||||
Use the supervised `lime/dev` launcher above for this behavior. The bare
|
||||
`lime.x11:run-event-loop` API does not supply a recovery thread or REPL.
|
||||
|
||||
Expected X11 client races (a window vanishing during adoption/property reads,
|
||||
or becoming unviewable just before focus) are handled at the adapter boundary.
|
||||
They increment `:expected-x-errors` in backend diagnostics; they do not pause
|
||||
the desktop. Ownership conflicts and unrelated protocol/programming errors
|
||||
are **not** silently ignored.
|
||||
|
||||
An unexpected event-loop error pauses event dispatch, releases active keyboard
|
||||
and pointer grabs, closes the command overlay, and preserves the X connection
|
||||
and in-memory world. The original event is consumed, not automatically retried;
|
||||
the remaining queue is retained. Slynk and the serialized mailbox stay alive:
|
||||
|
||||
```lisp
|
||||
(lime.dev:development-status) ; :paused-p, :last-error-report, etc.
|
||||
|
||||
;; Repair the problem; function-level changes can still be loaded while paused.
|
||||
(lime.dev:reload-development-code)
|
||||
|
||||
(lime.dev:resume-development-session) ; explicit validation + reconciliation
|
||||
```
|
||||
|
||||
Resume validates the core and adapter registrations, retires disappeared
|
||||
clients, completes an interrupted MapRequest adoption if needed, and renders
|
||||
the retained world. It does not replace Activities, saved Views, or drafts.
|
||||
An invalid model, broken renderer, or closed connection leaves the session
|
||||
paused and returns an error to the REPL. This is recovery, not transaction
|
||||
rollback: inspect state after a partially completed command before continuing.
|
||||
|
||||
While paused, **WM key bindings, menus, and new window management do not run**;
|
||||
use SLY for repair/resume or `stop-development-session` for an intentional exit.
|
||||
Existing application windows/processes are not deliberately destroyed. No
|
||||
automatic resume loop is used, because replaying arbitrary partial mutations
|
||||
can make a bug worse.
|
||||
|
||||
Diagnostics retain the newest 32 reports in memory and overwrite one private
|
||||
(mode `0600`) last-fault file under `$XDG_STATE_HOME/lime/`, normally
|
||||
`~/.local/state/lime/last-error-<display>.log`. Display punctuation becomes `-`;
|
||||
for example `:1` becomes `last-error--1.log`. `development-status` returns the
|
||||
exact path and any log-write error. Reports include time, event/window id,
|
||||
condition, and an SBCL backtrace captured before unwinding. They omit backtrace
|
||||
arguments, but error messages can still contain private paths/data; review a
|
||||
report before sharing it. Pass `:diagnostic-file #p"/chosen/path.log"` to the
|
||||
development launcher to override it, or `:diagnostic-file nil` for memory only.
|
||||
A logging failure does not end the control session.
|
||||
|
||||
This protects ordinary signaled event-loop errors, **not** native Lisp crashes,
|
||||
process kills, deadlocks, or X-server failure. Disk session persistence is a
|
||||
[separate layer](persistence.md), not a guarantee that application processes survive.
|
||||
Initial startup failure before the developer session exists still reports a
|
||||
normal launch error. Keep a TTY escape hatch during daily-driver testing.
|
||||
|
||||
The resilience change adds backend/runtime slots: install it by stopping and
|
||||
relaunching Lime in a fresh Lisp process once, not by reloading configuration.
|
||||
|
||||
Isolated fault-injection regression (requires Xvfb and the `lime/dev` dependencies):
|
||||
|
||||
```sh
|
||||
sbcl --non-interactive --load test/x11-resilience-smoke.lisp
|
||||
```
|
||||
|
||||
It creates its own display and private Slynk port, tests stale X requests,
|
||||
mid-adoption death/failure, grab release, invalid-state rejection, diagnostic
|
||||
write failure, slow failed restart/retry, and cleanup failure. It never connects
|
||||
to the user's display or REPL. Test diagnostics remain in the printed temporary
|
||||
directory for inspection.
|
||||
308
docs/model.md
Normal file
308
docs/model.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Lime model
|
||||
|
||||
This document is the source of truth for Lime's pure Lisp engine. The small
|
||||
CLX adapter translates this model to X11; it does not redefine it.
|
||||
|
||||
## Terms
|
||||
|
||||
### Activity
|
||||
|
||||
An Activity is a durable, named task container. It owns a pool of normal
|
||||
clients and a library of saved Views. It is not a virtual desktop and it does
|
||||
not intrinsically belong to one monitor or all monitors.
|
||||
|
||||
Examples: `lime development`, `day job`, `game`.
|
||||
|
||||
Every client has exactly one **home Activity**. A shared client additionally
|
||||
has `client-shared-p` set: any Activity may request it, but it still receives
|
||||
at most one global Lease. Sharing means availability, not sticky visibility,
|
||||
cloning, or an automatic placement in every draft. `activity-client-ids` is
|
||||
the home pool; `activity-available-client-ids` includes shared guests as well.
|
||||
|
||||
`set-client-shared` changes this eligibility without granting leases. Making
|
||||
a client private again removes its requests from foreign saved Views and
|
||||
live/cached drafts, releasing any foreign lease. Home checkpoints survive;
|
||||
the client is not automatically summoned home.
|
||||
|
||||
`move-client-to-activity` changes home ownership and releases the current
|
||||
lease. For private clients it prunes obsolete saved/cached/live references
|
||||
outside the destination, including layout leaves and focus preferences. For
|
||||
shared clients, valid saved requests survive; only the lease-holder's live
|
||||
placement is removed. With `:target-head-id`, the placement is queued in the
|
||||
destination's inactive working draft for that Head (preserving float geometry
|
||||
and existing destination layout). Without it, the client is simply parked.
|
||||
It never switches Heads or silently grants a lease in another active draft.
|
||||
All removal/focus policy is prepared on copies before committing membership.
|
||||
|
||||
`activity-deletion-summary` describes deletion without mutating the model.
|
||||
`delete-activity` accepts only an Activity with no home clients and no mounted
|
||||
Presentations on any Head. Hidden clients and shared clients borrowed elsewhere
|
||||
still count as home clients: move or close them first. Deletion never closes
|
||||
applications, switches Heads, or rehomes clients implicitly.
|
||||
|
||||
Deletion removes the Activity's Views and cached/suspended drafts, prunes their
|
||||
entries from Viewsets, and removes Viewsets left empty. Cached references to
|
||||
shared guests disappear, but the actual guests retain their homes and leases
|
||||
elsewhere. An optional `:expected-revision` protects a prior confirmation from
|
||||
concurrent edits; all validation precedes mutation. There is no built-in undo.
|
||||
|
||||
### View
|
||||
|
||||
A View is a named, saved, **single-Head** checkpoint within an Activity. It
|
||||
contains the desired visible client placements, Head-relative layout data,
|
||||
and preferred focus. It is a definition, not a runtime owner of windows.
|
||||
|
||||
Examples within `lime development`: `code`, `debug`, `docs`.
|
||||
|
||||
An absent client means “this View does not request it on this Head”; it does
|
||||
not mean “hide it everywhere in the Activity.”
|
||||
|
||||
The concrete tiling representation is a binary tree of `layout-leaf` and
|
||||
`layout-split` nodes. A split is horizontal or vertical and stores a relative
|
||||
ratio. The tree contains exactly the View's `:tile` placements; `:float`
|
||||
placements carry Head-relative geometry separately.
|
||||
|
||||
A View or Presentation also records one layout policy:
|
||||
|
||||
- `:manual` renders its explicit tree. Focused-split orientation/ratio edits
|
||||
and slot swaps switch the live draft to this policy.
|
||||
- `:master-stack` derives a conventional master area and equal stack from tile
|
||||
placement order. Its options are `:master-count`, `:master-ratio`, and
|
||||
master/stack `:orientation`; new tiled clients reflow under the same policy.
|
||||
- `:monocle` retains the full tree and all leases, but renders only the focused
|
||||
tiled client at the Head's full usable rectangle. Changing focus changes the
|
||||
visible client without changing leases or the checkpoint.
|
||||
|
||||
The policy and its options are snapshot data: they are copied into a
|
||||
Presentation at activation and only reach a View through explicit save/update.
|
||||
Floating is deliberately orthogonal to these tiling policies. A float stores a
|
||||
complete `:x`, `:y`, `:width`, and `:height` rectangle as normalized fractions
|
||||
of its owning Head; it therefore adapts to Head pixel dimensions without
|
||||
putting X11 geometry in the model. `toggle-client-floating`,
|
||||
`toggle-focused-client-floating`, and `set-client-floating-geometry` modify
|
||||
only an active Presentation. Toggling a client back to tile returns it to the
|
||||
current tiling policy; master/stack reflows, while the manual tree inserts a
|
||||
new leaf. As with any live change, the rectangle reaches a View only through
|
||||
an explicit save/update.
|
||||
|
||||
### Head
|
||||
|
||||
A Head represents one independently addressable display rectangle, normally a
|
||||
physical monitor. A Head has zero or one active Presentation. Its pixel
|
||||
rectangle is backend data: at X11 startup, RandR discovery normally derives
|
||||
the non-overlapping rectangles from active CRTCs; explicit rectangles remain
|
||||
available for nested servers and unusual arrangements. RandR change events
|
||||
refresh the physical-topology snapshot. A caller then explicitly reconciles
|
||||
the model's logical Head ids: a removed Head's live Presentation can move
|
||||
intact to one chosen surviving/new Head, or suspend as an unbound cached draft.
|
||||
No output event silently changes an Activity, View, or Lease.
|
||||
|
||||
### Presentation
|
||||
|
||||
A Presentation is one Activity's **current working arrangement on one Head**.
|
||||
Together these arrangements form the Activity's live workspace across Heads.
|
||||
There is at most
|
||||
one working arrangement per `(Activity, Head)`, whether visible or inactive.
|
||||
Recalling a View replaces that arrangement in place, not an extra cached
|
||||
workspace. `Draft` means no source checkpoint, not a second navigation target.
|
||||
|
||||
Switching away keeps the arrangement and releases its visible leases.
|
||||
Returning restores current locations, unsaved windows, layout and focus.
|
||||
Explicit moves/summons remove previous live locations, including inactive
|
||||
ones; a later return cannot reclaim those windows. Hiding survives returns.
|
||||
The low-level Head-local `open-activity-draft` operation creates an empty first
|
||||
portion; it never gathers windows from another Head. Ordinary frontend
|
||||
navigation adds a portable/extended policy above this operation.
|
||||
|
||||
A **portable** Activity selects one working arrangement independent of its
|
||||
physical Head. `bring-presentation-to-head` moves/resumes that arrangement;
|
||||
if it is displayed elsewhere, it swaps with the target Head's current
|
||||
arrangement. Active leases remain attached to their Presentation objects.
|
||||
Hidden clients stay hidden, dirty layouts stay dirty, and checkpoints are not
|
||||
recalled. The displaced Activity's cached portions transpose between the same
|
||||
two Heads, preserving its full working state rather than overwriting a cache.
|
||||
An inactive incoming arrangement changes only the destination's active selection.
|
||||
|
||||
Explicitly extending an Activity enables independent Head-local portions.
|
||||
This is navigation intent, not a second Activity type or a window-count rule;
|
||||
closing/hiding the final window on a portion never automatically folds it.
|
||||
The adapter persists this policy separately from the core world and can make
|
||||
an Activity portable again after its other portions are empty and inactive.
|
||||
|
||||
Only explicit checkpoint recall requests historical placements and offers
|
||||
strict/partial/take policies. A partial recall may expose missing slots for an
|
||||
explicit reclaim command, but Activity return drops those requests instead of
|
||||
retrying them. Saved checkpoints never change through a move or partial recall.
|
||||
Recalling a checkpoint on another Head detaches its older inactive source
|
||||
label while preserving the old arrangement's remaining windows and layout.
|
||||
When topology reconciliation suspends a draft from a removed Head, its
|
||||
`head-id` is temporarily absent and it holds no leases; an explicit resume can
|
||||
bind that draft to any current Head.
|
||||
|
||||
### Lease
|
||||
|
||||
A Lease is the exclusive, runtime-only right for one active Presentation to
|
||||
show one real client. It is not persisted in a View.
|
||||
|
||||
```text
|
||||
client -> zero or one active Presentation lease
|
||||
```
|
||||
|
||||
No lease means the client is parked. One lease means it is visible in that
|
||||
Presentation. Two leases are invalid: a real X11 window is not a clone.
|
||||
|
||||
### Viewset
|
||||
|
||||
A Viewset is a deliberate whole-desk shortcut, not the default switching
|
||||
behavior. It is a saved mapping of Heads to Activity/View selections:
|
||||
|
||||
```text
|
||||
Development
|
||||
left -> Lime development / Code
|
||||
right -> Lime development / Debug
|
||||
|
||||
After work
|
||||
left -> Game / Play
|
||||
right -> Comms / Chat
|
||||
```
|
||||
|
||||
Its activation uses its own preflight/plan/commit machinery, but validates all
|
||||
target Heads together. A Viewset saves durable `Head -> View` selections—it
|
||||
does not serialize mutable Presentations, drafts, focus overrides, or Leases.
|
||||
Capturing the current desk therefore requires every selected Head to be mounted
|
||||
from a saved View; save a View first when a Head holds an unsaved draft.
|
||||
|
||||
All target Heads release their outgoing leases before any incoming View is
|
||||
mounted. That makes a genuine swap safe:
|
||||
|
||||
```text
|
||||
before: left -> Code, right -> Play
|
||||
after: left -> Play, right -> Code
|
||||
```
|
||||
|
||||
Neither incoming View conflicts with the other’s outgoing lease or mounted
|
||||
View. A client leased on an unselected Head remains an ordinary external
|
||||
collision, resolved explicitly as strict, partial, or take. If two target Views
|
||||
both request the same client, the Viewset is rejected as internally ambiguous;
|
||||
Lime never chooses a winner based on entry order.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. A client has exactly one home Activity; shared clients are eligible elsewhere.
|
||||
2. Each Head has at most one active Presentation.
|
||||
3. A named View has at most one active mounted Presentation.
|
||||
4. A normal client has at most one Lease globally.
|
||||
5. A Lease must point to an active Presentation in an eligible Activity
|
||||
(home, or any Activity for a shared client), containing a placement for it.
|
||||
6. A saved View never changes as a side effect of a lease collision.
|
||||
7. Switching one Head changes only that Head unless an explicit operation
|
||||
transfers a client lease, revokes sharing, or activates a named Viewset.
|
||||
8. Each `(Activity, Head)` has at most one current working arrangement.
|
||||
9. Each client has at most one current live location, even while undisplayed.
|
||||
Missing slots from an explicit partial recall are not live locations.
|
||||
10. A source checkpoint labels only one working arrangement. Saved View
|
||||
definitions may still contain overlapping sets of windows.
|
||||
|
||||
## Activation is a plan, not a UI monolith
|
||||
|
||||
The core deliberately separates collision detection, resolution choice, and
|
||||
state mutation:
|
||||
|
||||
```text
|
||||
preflight-view-activation
|
||||
-> Activation Report: ready / conflicts
|
||||
-> strict, partial, or take plan builder
|
||||
-> commit-activation-plan
|
||||
```
|
||||
|
||||
The menu is only one consumer of the report:
|
||||
|
||||
```text
|
||||
Docs needs Emacs, currently leased by Left / Code
|
||||
|
||||
Open reduced -> build-partial-activation-plan
|
||||
Move Emacs here -> build-taking-activation-plan
|
||||
Go to Left -> frontend focus command
|
||||
Cancel -> no plan
|
||||
```
|
||||
|
||||
This lets the same semantics work from keyboard bindings, an on-screen menu,
|
||||
IPC, or a Lisp REPL.
|
||||
|
||||
### Viewset activation is a grouped plan
|
||||
|
||||
```text
|
||||
preflight-viewset-activation
|
||||
-> Viewset Report: all selected Heads, outgoing group, conflicts
|
||||
-> strict, partial, or take Viewset plan builder
|
||||
-> commit-viewset-plan
|
||||
```
|
||||
|
||||
The grouped preflight deliberately excludes leases and mounted Views held by
|
||||
selected Heads’ outgoing Presentations, because the single commit releases all
|
||||
of them first. It still checks stale world revision and every target Head’s
|
||||
outgoing Presentation id immediately before mutation.
|
||||
|
||||
## Topology reconciliation is a plan too
|
||||
|
||||
RandR supplies the desired physical Head identifiers, but it never gets to
|
||||
choose task routing. The separate core sequence is:
|
||||
|
||||
```text
|
||||
preflight-head-topology-reconciliation
|
||||
-> Head Topology Report: retained / added / removed / live removed sources
|
||||
-> build-head-topology-reconciliation-plan with explicit source -> destination moves
|
||||
-> commit-head-topology-reconciliation-plan
|
||||
```
|
||||
|
||||
An incoming move keeps the exact live Presentation, including its unsaved
|
||||
layout, focus, and leases. If a surviving destination already has a live
|
||||
Presentation, that destination becomes a normal cached draft and releases its
|
||||
leases before the incoming Presentation mounts. A removed source omitted from
|
||||
the move list becomes an unbound, lease-free cached draft instead. Added Heads
|
||||
with no incoming move are genuinely blank; a normal Activity or View command
|
||||
can mount something there later.
|
||||
|
||||
## Collision behavior
|
||||
|
||||
The normal strict command makes no change if a requested client is leased by a
|
||||
different active Head. It does not silently use “last request wins.”
|
||||
|
||||
Two explicit alternatives exist:
|
||||
|
||||
- **Partial activation** mounts the View with free clients and records the
|
||||
contested clients as unavailable slots.
|
||||
- **Take activation** transfers the contested leases atomically to the target
|
||||
Presentation. The source Presentation becomes temporarily incomplete; its
|
||||
saved View remains unchanged.
|
||||
|
||||
A newly freed client is never auto-awarded to another Presentation. An
|
||||
explicit reclaim command fills an available slot, avoiding windows that jump
|
||||
between monitors unexpectedly.
|
||||
|
||||
## New windows and snapshots
|
||||
|
||||
New clients join the focused Head's active Activity and live Presentation.
|
||||
They do not retroactively appear in old saved Views.
|
||||
`show-client-on-presentation` changes only the live draft;
|
||||
`update-view-from-presentation` is
|
||||
the deliberate checkpoint update operation.
|
||||
|
||||
Updating a partially mounted/degraded Presentation is rejected by default so a
|
||||
missing contested client cannot be silently erased from a reliable checkpoint.
|
||||
|
||||
## Backend boundary
|
||||
|
||||
The model knows nothing about X11, CLX, pixels, or an event loop. The backend
|
||||
translates a committed logical transition into batched effects:
|
||||
|
||||
```text
|
||||
logical plan -> unmap / configure / map / stack / focus -> X11
|
||||
```
|
||||
|
||||
The backend must preserve the model's one-lease invariant and use X11 event
|
||||
feedback only to update client lifecycle state. A permanently destroyed
|
||||
client is retired from its Activity, saved Views, live/cached Presentations,
|
||||
and lease; it cannot leave a phantom tiling slot behind. Layout trees, floating
|
||||
geometry, Head work areas, RandR discovery and hotplug-migration policy, ICCCM/EWMH, and input binding
|
||||
policy all belong above or below this core rather than inside lease resolution.
|
||||
149
docs/persistence.md
Normal file
149
docs/persistence.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Session persistence
|
||||
|
||||
Lime saves the **desk**, separately from your Lisp configuration: Activities,
|
||||
named Views and Viewsets, current and cached drafts, hidden/shown membership,
|
||||
home Activity and shared eligibility, layout order/options/weights, floating rectangles, focus, dirty flags, partial
|
||||
View state, and logical Head selections. It does not serialize CLX objects,
|
||||
fonts, key bindings, callbacks, running processes, or application contents.
|
||||
|
||||
`lime/dev` enables this automatically. The normal location is
|
||||
`$XDG_STATE_HOME/lime/sessions/`, falling back to `~/.local/state/lime/sessions/`.
|
||||
Files are separate for each DISPLAY (for example `display-_3A0.sexp` for `:0`,
|
||||
`display-_3A99.sexp` for `:99`). Your nested test desk does not overwrite your
|
||||
normal desktop's desk. A lifetime writer lock prevents two backends from using
|
||||
one explicit file simultaneously.
|
||||
|
||||
Select a different file or disable persistence with launcher keywords—not an
|
||||
additional Lime environment variable:
|
||||
|
||||
```lisp
|
||||
(lime.dev:run-development-session
|
||||
:config-file #p"/home/adan/.config/lime/config.lisp"
|
||||
:session-file #p"/home/adan/.local/state/lime/my-desk.sexp")
|
||||
|
||||
;; For a disposable test:
|
||||
(lime.dev:start-development-session :display-name ":99" :session-file nil)
|
||||
```
|
||||
|
||||
The lower-level `lime.x11:start-lime` / `start-backend` APIs default to no
|
||||
persistence; pass `:session-file :default` or a pathname to enable it. An
|
||||
explicit `:world` and a session file are mutually exclusive startup sources.
|
||||
Configuration reload does not change the selected session file.
|
||||
Configured startup hooks still run normally; hooks that create named Activities
|
||||
should tolerate those Activities already existing in a restored desk.
|
||||
|
||||
`scripts/run-xephyr-dev` supports `--session-file PATH` and `--no-session`.
|
||||
Without either option it uses the display-specific default.
|
||||
|
||||
## What a restart restores
|
||||
|
||||
On a **Lime-only restart within the same X server**, surviving clients reconnect
|
||||
to their Activities, checkpoints, and drafts—including unmapped hidden clients.
|
||||
The adapter verifies a random server token plus each client's XID and random
|
||||
window token. An XID, title, WM_CLASS, or PID alone is never enough: reused ids
|
||||
must not attach an unrelated application's window to an old checkpoint.
|
||||
|
||||
On **logout, reboot, or X-server replacement**, the names and non-client layout
|
||||
settings survive, but the old windows do not. Missing clients are retired from
|
||||
the saved arrangements; an empty View remains a named checkpoint. This first
|
||||
version does **not** relaunch applications, restore terminal/browser contents,
|
||||
reserve placeholder slots for dead windows, or guess how newly launched
|
||||
windows should fill an old arrangement. Application/session-relaunch rules are
|
||||
a separate feature.
|
||||
|
||||
Current physical Heads remain authoritative. Matching logical Head ids resume
|
||||
their saved selections. Drafts from removed Heads are suspended, not moved to
|
||||
an arbitrary other monitor or deleted; new Heads get ordinary blank drafts.
|
||||
Viewsets retain their logical mappings even when an output is absent. Leases
|
||||
are reconstructed and validated, never accepted as serialized ownership objects.
|
||||
|
||||
## Saving and failures
|
||||
|
||||
Autosave observes model/focus changes on the owner thread. After 750 ms of
|
||||
quiet, or five seconds of ongoing edits, it captures a validated plain-data
|
||||
snapshot. Active pointer drags postpone capture until release. A single worker
|
||||
writes snapshots off the X event thread, keeping only the latest pending one.
|
||||
An unchanged desk produces no disk writes. A crash can lose edits since the
|
||||
last completed autosave; this is a snapshot system, not a per-operation journal.
|
||||
|
||||
Normal shutdown flushes the final valid desk. A normal developer restart saves
|
||||
**before closing X**; if that save fails, restart aborts with the old backend
|
||||
still available for repair. Restart after an event-loop fault instead uses the
|
||||
last good disk snapshot, avoiding an automatic save of partially mutated state.
|
||||
`resume-development-session` still uses the retained in-memory world, not disk.
|
||||
|
||||
For an explicit flush or inspection from SLY:
|
||||
|
||||
```lisp
|
||||
(lime.dev:save-development-session)
|
||||
(lime.dev:with-development-backend (backend)
|
||||
(lime.x11:backend-session-status backend))
|
||||
```
|
||||
|
||||
The status includes the file, restore/missing-client report, last-save time,
|
||||
submitted/completed write generations, and restore/snapshot/write errors. Disk
|
||||
write failures are nonfatal to ordinary operation and retry with five-second
|
||||
backoff; explicit flush reports the error to its caller. Invalid in-memory
|
||||
snapshots block automatic saving until repaired and explicitly saved again.
|
||||
|
||||
Files are mode `0600`. Writes use a private temporary file in the same
|
||||
directory, file `fsync`, atomic rename, and directory `fsync` on Linux. A failed
|
||||
write before rename leaves the old file untouched; readers cannot see a partial
|
||||
replacement. The format is versioned, bounded plain data. Reading never calls
|
||||
`load`/`eval`, disables read-time evaluation, and rejects reader dispatch,
|
||||
circular structures, excessive nesting, and trailing data. Unknown versions,
|
||||
invalid relationships, duplicate leases, or malformed files are rejected before
|
||||
replacing the startup world.
|
||||
|
||||
A rejected file is **not overwritten**: Lime starts a fresh usable desk with
|
||||
autosave blocked and reports the error. Inspect/back up the file first. To
|
||||
deliberately replace it with the current desk:
|
||||
|
||||
```lisp
|
||||
(lime.dev:save-development-session :replace-invalid-p t)
|
||||
```
|
||||
|
||||
This does not override another session's file lock. Stop that other session or
|
||||
choose a different path. Back up important files separately; atomic writes are
|
||||
not a backup/history system. Names and window associations can be private even
|
||||
though the file contains no application buffers or process contents.
|
||||
|
||||
## Installing and testing
|
||||
|
||||
Membership support adds a client structure slot and ASDF components. Relaunch
|
||||
Lime in a fresh Lisp process; configuration reload alone cannot install it.
|
||||
World snapshots use schema version 3. The X11 session envelope uses
|
||||
version 3, with ordered Activity positions, stable local-View shortcut slots,
|
||||
last-arrangement history, and portable/extended Activity navigation policy.
|
||||
Version-1/2 envelopes remain readable. Old Activities with one substantive
|
||||
arrangement become portable; genuinely split arrangements remain extended.
|
||||
This inference happens before missing-client pruning, preserving multi-monitor
|
||||
intent across reboot. Old empty drafts are retained, not recalled or deleted.
|
||||
Version-1 envelopes also receive initial shortcut assignments. Migration does
|
||||
not move a window or alter what the Heads display. Navigation metadata is
|
||||
validated before a saved world is installed. An explicit extended choice stays
|
||||
extended when all its windows close; it is never re-inferred from window count.
|
||||
Version 1 worlds load with all clients private. Version 1/2 worlds consolidate
|
||||
historical drafts into one current arrangement per Activity/Head. Active state
|
||||
wins, then saved navigation history, then the newest generated Presentation
|
||||
id. Actual leases win duplicated window locations; missing checkpoint slots
|
||||
are not live locations. Windows and saved Views are retained, and old source
|
||||
labels are detached where a checkpoint was recalled elsewhere. Migration is
|
||||
performed on a candidate world before installation; version 3 rejects duplicate
|
||||
live locations rather than silently repairing them. Keep a backup before
|
||||
upgrading or downgrading; older Lime builds may not read these schema versions.
|
||||
The same-X-server identity rules above still apply to moved/shared clients.
|
||||
|
||||
```lisp
|
||||
(asdf:test-system "lime/persistence")
|
||||
```
|
||||
|
||||
```sh
|
||||
sbcl --non-interactive --load test/x11-persistence-smoke.lisp
|
||||
sbcl --non-interactive --load test/x11-membership-smoke.lisp
|
||||
```
|
||||
|
||||
The smoke test uses its own Xvfb, temporary state files, and fake client windows.
|
||||
It tests graceful/abrupt restart, hidden clients, dirty checkpoints, partial
|
||||
leases, changed topology/identity, corrupt files, disk failures, and the actual
|
||||
developer restart path. It does not connect to your running desktop.
|
||||
686
docs/xephyr-demo.md
Normal file
686
docs/xephyr-demo.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user