309 lines
14 KiB
Markdown
309 lines
14 KiB
Markdown
# 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.
|