Initial Lime window manager
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user