Initial Lime window manager

This commit is contained in:
2026-10-02 14:14:47 -05:00
commit 785c2e8968
103 changed files with 27259 additions and 0 deletions

308
docs/model.md Normal file
View 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.