Initial Lime window manager
This commit is contained in:
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