# 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-.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.