Go to file
Levi Neuwirth 2fc2985029
fix(window): refuse the mutation that would invalidate a panel commit
Revision 8 of `docs/destination-capture-framing.md`, replacing the
revision-7 design at `ca72461`, plus the invalid-UTF-8 profile hole.
The framing now carries §3's enumeration, performed.

THE BLOCKER, unchanged. The `"panel"` commit profile skips preflight
checks 2-4 on the claim that a panel result never touches a document
window. Panel placement FALLS BACK into an ordinary document window
when the frontend is not `panel_capable` or its one side slot is
dedicated elsewhere, and installs the result there --- so a `"panel"`
commit could replace a NEWER document with every stale-intent guard
skipped.

TWO REJECTED SHAPES, kept in the framing as the record of why not
those. Revision 6 predicted the fallback at preflight and argued the
body could not change it; false, because refusing `await` stops another
COROUTINE interleaving, not the body's own synchronous statements.
Revision 7 (`ca72461`) moved enforcement to the placement boundary;
that breaks the invariant `commit_to` exists for --- `docs/agent-handoff.md`
requires it to preflight BEFORE the callback, because a body creates
buffers, handles and paint long before it asks to display anything, so
"validating at display time is four mutations too late". A refusal
arriving after all of that is a partial commit with an error return.

REVISION 8 DOES NEITHER. The preflight stays exactly where it was, and
the mutations that would invalidate it are REFUSED AT THE ATTEMPT ---
the same shape as `Handle:await` being refused inside a commit scope,
for the identical reason: something that would invalidate the scope's
guarantee is rejected outright rather than predicted around or caught
late. With them refused, the fallback never comes into existence.

THE ENUMERATION, PERFORMED --- this is the load-bearing part, and it is
closed for a structural reason rather than because inspection ran out
of ideas. Full working in the framing §3.

`resolve_placement` reaches `Ordinary` from a side request through
exactly two branches, so only two pieces of state are levers at all:
`panel_capable`, and the one side window's `dedicated`.

`panel_capable` is UNREACHABLE from a body: written only where a
`FrontendView` is constructed, and nothing in `src/lua_bindings/`
constructs, registers or unregisters one --- `register_frontend_view`
has callers only in `daemon.rs` and core unit tests.

`dedicated` has eight writes. Five are reachable: `apply_placement`'s
`Side` created, replacing and non-replacing arms, and `set_params`. Two
`Ordinary` arms are harmless --- every `Ordinary` target is filtered
`!is_side`, and one only ever clears the flag. One is a unit test.

Closing the side window is NOT a route, checked rather than assumed:
with no side leaf `side_window_for` returns `None` and placement
CREATES a fresh panel instead of falling back. `panel_hidden` is not
consulted by placement, and `params.side` is unreachable.

`quit_window`'s `QuitAction::Restore { dedicated: true }` is
UNREACHABLE, and this was the surprise --- it looked like a route with
no `dedicated` argument at the call site at all. `Restore` is stored
only on a REPLACING side placement, and a dedicated slot can never be
the target of one: a side request with a different buffer falls through
to `Ordinary`, and an exact-target request is refused by
`window_accepts_buffer`. Guarded anyway, labelled defensive, because
its unreachability is emergent from two rules in another function.

GUARDS SITED WHERE THE PROPERTY CONVERGES. All three `Side` arms are
reached through `apply_placement`, which has EXACTLY ONE caller --- so
one guard in `display_buffer` covers every request-driven dedication,
including spellings that do not exist yet. `set_params` is a genuinely
separate write and is guarded separately; dedication does NOT converge
before the field itself, and that is stated rather than papered over.
`Window::params.dedicated` is a public field, so the compiler does not
enforce the funnel --- the acceptance rows are what would catch a new
direct writer.

WHAT IS DELIBERATELY NOT REFUSED. The document profile is untouched:
constraining its body would newly refuse dired's own documented panel
path, a preservation-suite stop signal. Dedicating a DOCUMENT window is
still allowed, since it cannot change which of panel-or-document a side
request resolves to. And falling back is still allowed --- a frontend
that cannot render a panel degrades gracefully exactly as today,
because this refuses the mutation that MANUFACTURES a fallback, never
the fallback itself.

THE SECOND HOLE. `commit_profile` did `name.to_str()?`, but Lua strings
are BYTE strings, so `string.char(255)` hit mlua's generic UTF-8 error
before `BAD_COMMIT_PROFILE` was constructed --- the same reachability
class as the `Option<String>` defect revision 5 fixed, one layer down.
Bytes now, with the row asserting on message content.

TESTS: 12 pins. The inside-the-body test is ONE ROW PER REACHABLE WRITE
SITE, not per call spelling, because one spelling reaches three
different writes: `set_params`, and `display{side, dedicated}` in each
of the created, replacing and non-replacing arms. Each asserts the
three things revision 8 requires --- the dedication call is refused, the
slot is still undedicated afterwards, and nothing partial was installed
(no `*result*` buffer, panel unchanged, document unchanged).

Mutation-checked per guard: deleting the `display_buffer` guard fails
all three display rows, verified INDIVIDUALLY by rotating each to the
front so the first failure cannot mask the rest; deleting the
`set_params` guard fails only that row.

THREE FRAMING CORRECTIONS ride along, all of them cases of the document
teaching something it later argues against. Section 3 stated the
disproved premise unconditionally --- "the panel case would inherit a
check about a window it never touches" --- a hundred lines before
correcting it, so a reader met the wrong claim first; it is now
qualified at the point of the claim, and section 2 carried the same
unconditional form one section earlier ("it lands in the bottom panel")
and now says it REQUESTS one. The handoff citation was written "section
748" twice when it is LINE 748, and this document's authority is that
its citations can be followed. And the "not asserted exhaustive" hedge
on the route list is retired: the enumeration is closed structurally,
because `resolve_placement` reaches `Ordinary` from a side request
through exactly two branches.

`journey_acceptance` (47) and `dired_acceptance` (31) pass UNCHANGED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
.github/workflows feat(release): binaries on tag — Distribution Stage 1 2026-08-01 14:40:47 -04:00
audit V0.2-prerequisite pull-forward + M10.11 clean audit round 2026-05-18 10:31:31 -04:00
builtin fix(lsp): a `/` search boundary no longer disables the LaTeX marker walk 2026-08-10 10:48:49 +02:00
docs fix(window): refuse the mutation that would invalidate a panel commit 2026-08-10 14:11:43 +02:00
pmacs-gpu feat(gpu): horizontal scroll — QoL Stage 5, closing the long-lines arc (#223) 2026-08-08 10:55:49 +00:00
pmacs-protocol feat(gpu): horizontal scroll — QoL Stage 5, closing the long-lines arc (#223) 2026-08-08 10:55:49 +00:00
proptest-regressions M10.10 ship gate 2026-05-13 16:28:46 -04:00
scripts docs: record the witness as closed, and what the audit found next door 2026-08-09 18:18:34 +02:00
src fix(window): refuse the mutation that would invalidate a panel commit 2026-08-10 14:11:43 +02:00
tests fix(window): refuse the mutation that would invalidate a panel commit 2026-08-10 14:11:43 +02:00
.gitignore audit remediation: workspace clippy gate + stale metadata + cruft (F-001/F-013/F-015) 2026-07-03 10:31:17 -04:00
AGENTS.md docs: add COHERENCE.md as a required doc, audited against the codebase 2026-07-25 11:37:21 -04:00
CHANGELOG.md docs(changelog): remove dangling prerequisite links 2026-07-22 19:54:07 -04:00
CLAUDE.md docs: add COHERENCE.md as a required doc, audited against the codebase 2026-07-25 11:37:21 -04:00
COHERENCE.md fix(listview): flat panels keep their TAB, and a selection test that bites 2026-08-05 17:17:33 +02:00
Cargo.lock feat(release): binaries on tag — Distribution Stage 1 2026-08-01 14:40:47 -04:00
Cargo.toml feat(release): binaries on tag — Distribution Stage 1 2026-08-01 14:40:47 -04:00
LICENSE-APACHE Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00
LICENSE-MIT Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00
README.md docs: absorb the v1.1.0 release, and correct what it made stale 2026-08-01 18:09:59 -04:00
TEST_IMPROVEMENT.md review round 2: arm the required-checks name-coupling trap, restore m6 2026-07-29 14:16:27 -04:00
build.rs Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00
rust-toolchain.toml rust-toolchain.toml: add rust-analyzer component (pin regression fix) 2026-05-18 11:58:46 -04:00
rustfmt.toml Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00

README.md

Pmacs

Parallel Emacs --- a Rust-cored, Lua-scripted editor in the Emacs tradition.

Pmacs runs the editor's hot path (rope, buffers, views, async runtime, process supervision) in Rust, and exposes the rest --- commands, keymaps, hooks, packages --- through an embedded Lua VM. The design follows Emacs in shape (configurable, introspectable, programmable from inside) but discards the single-threaded substrate; workers, message bus, and a coroutine-based async surface are core primitives, not bolt-ons.

The editor is partitioned into a long-lived instance (the daemon that owns buffers, processes, and language services) and thin frontends that attach over a typed protocol (currently v20). Two frontends ship today:

  • a TUI (crossterm cell grid), attachable locally over a Unix socket or remotely over SSH, with reconnect-on-drop modeled on mosh; and
  • pmacs-gpu, a GPU frontend (wgpu + winit + glyphon) that renders from a semantic projection of editor state --- style spans, decorations, inlay adornments --- rather than a character grid, and edits optimistically against a local CRDT replica for latency-free typing.

Buffers are optionally CRDT-backed (loro, behind --features crdt), so multiple frontends --- TUI and GPU, local and remote --- can edit the same buffers concurrently with live cursor/selection presence.

Status

v1.1.0 --- stable core, active development, and the first release with prebuilt binaries. The v1.0 gate (the instance/frontend partition, the Lua surface, and a REPL package audited to use zero direct Rust core access) shipped some time ago. Development since has expanded the semantic frontend protocol from v6 through v21, brought the GPU frontend near input/render parity with the TUI, and completed the LSP, editing, persistence, themes, and terminal arcs. Recent work added major modes and modeline detection, a typed configuration registry, composable statuslines, multi-language syntax processing, cross-frontend tab-width parity, a directory browser, a describe/list command family, and a bottom panel on both frontends.

Current direction lives in COHERENCE.md (the product-coherence thesis and its audited priority order) and docs/agent-handoff.md (durable project state). docs/roadmap-2026-07.md is a historical planning snapshot and is no longer the authority.

Public contributions are open: use, evaluate, file issues, and send pull requests.

Highlights

Editing & UI. CUA-style region editing plus Emacs kill/yank and kill-ring bindings; linear undo/redo; query-replace; incremental substring and regex search (C-s / C-r / C-M-s); comment, auto-indent, auto-pair, transpose, case, line, and region operations; line-number gutter with absolute, relative, and hybrid modes; diagnostic signs; context menu; OS clipboard integration (OSC 52 in the TUI, native in the GPU); minibuffer completion with persisted history; buffer-list and compilation modes; self-navigable help. Named ui.* theme faces, live GPU font selection, and composable per-window statusline providers keep chrome and modelines runtime-configurable. Saves are atomic (temp + rename + parent fsync, mode-preserving).

Language intelligence. The async LSP client provides diagnostics, rename with prepareRename, cross-file definitions, hover, signature help, references, document symbols, code actions, formatting, semantic tokens, and inline inlay hints. Preconfigured servers cover Rust, C/C++, Python, Go, JavaScript/TypeScript, Lua, Bash, TOML, Zig, Dockerfile, CMake, JSON, and YAML. Bundled tree-sitter grammars include those languages plus Markdown, Make, and CUDA; nested Markdown fences and frontmatter use multi-language injections, and locals-query processing distinguishes shadowed builtins. Bounded Emacs and Vim modelines join extensions, exact filenames, and shebangs in one fresh-load language decision. That decision initializes the buffer's major mode, drives syntax/LSP/pairing/comment behavior, and enables mode-scoped keymaps. A persistent project-symbol index (.pmacs/index.json) rides the same worker infrastructure.

Collaboration & frontends. With --features crdt, buffers are CRDT-backed and any number of frontends attach to one daemon and edit concurrently; peers see each other's cursors and selections as translucent washes. The TUI and GPU frontends both host owned full-screen terminal sessions; protocol-v19 terminal frames preserve the fixed-cell VT screen while each frontend owns its scroll/selection/input context. The GPU frontend also provides a live minimap, wavy diagnostic squiggles, a status band, and optimistic local editing that rebases in-flight edits through authoritative frames. Buffer text, syntax, diagnostics, carets, hits, and minimap geometry now share one eight-column tab projection without mutating source bytes.

Extensibility. The pmacs.* Lua namespaces cover buffers, windows, commands, global/mode/buffer keymaps, hooks, themes, statusline providers, tree-sitter, LSP stores, async workers, and a PTY-aware process supervisor. The typed, introspectable pmacs.config registry supports global and buffer-local values, listeners, startup-only settings, and describe-setting. A package manager installs from git (github:owner/repo, version/branch/commit pins) with transitive dependency resolution and a SHA-256 lockfile. Pmacs is also an MCP client: packages can spawn MCP servers and consume their tools, resources, and prompts --- AI integrations are packages over a transport, not a built-in feature. The bundled REPL package is written entirely against the public Lua API.

Running

Single-process TUI:

pmacs [FILE]                 # TUI; -nw reserved for when a GUI default lands

GPU frontend (one command; the root binary starts or reuses the daemon):

pmacs --gpu                         # default instance; no initial file
pmacs --gpu README.md               # default instance; open one file
pmacs --gpu --socket NAME FILE      # named instance; bare NAME →
                                    #   <runtime>/pmacs/NAME.sock
pmacs --gpu -- --leading-dash       # `--` ends option parsing

pmacs --gpu requires the root pmacs binary to be built with the crdt feature. It discovers a sibling pmacs-gpu binary first, then falls back to pmacs-gpu on PATH. When FILE is present, the daemon loads or creates it and completes startup hooks before the GPU window appears. Closing the window detaches only that frontend; the daemon remains available for later GPU or TUI attaches.

Daemon + attached TUI frontends:

pmacs --daemon --socket NAME        # foreground daemon
pmacs --attach --socket NAME        # TUI frontend; F12 detaches
pmacs --attach user@host            # remote TUI over SSH

For debugging an already-running daemon, the low-level GPU command stays available and never auto-starts or replaces anything:

pmacs-gpu --attach /absolute/path/to/pmacs.sock

pmacs --attach also understands ssh:user@host/instance, local:/path.sock, and bare hostnames (treated as SSH). See pmacs --help for the full matrix.

User configuration is plain Lua at $XDG_CONFIG_HOME/pmacs/init.lua (default ~/.config/pmacs/init.lua), loaded after the builtin runtime so plain assignments override defaults --- keybindings, pmacs.lsp.config, theme overrides, and package installs all live there.

Install

Download an archive from the releases page, unpack it, and put both binaries somewhere on your PATH.

Keep pmacs and pmacs-gpu together. pmacs --gpu looks for pmacs-gpu beside itself first and only then falls back to a PATH lookup, so an unpacked release is self-contained as long as the two stay in the same directory.

Verify a download:

sha256sum -c SHA256SUMS --ignore-missing
platform built on notes
Linux x86_64 Ubuntu 22.04 requires glibc ≥ 2.35 — Ubuntu 22.04+, Debian 12+. RHEL 9 (glibc 2.34) is not supported yet.
macOS arm64 macOS 15 Apple Silicon only; Intel is not built yet. Binaries are unsigned and not notarized, so Gatekeeper will quarantine them until you allow them explicitly.

Releases carry binaries only — there is no in-place update, rollback, or package-manager distribution yet. Build from source for any platform not listed, and see "Runtime dependencies" below for what the editor assumes is present.

Build

Builds on the toolchain pinned in rust-toolchain.toml (Rust 1.95.0, edition 2024); rustup selects it automatically.

# Coherent root + GPU release build. The package-qualified feature keeps
# the separate pmacs-gpu package feature-free while enabling CRDT in pmacs.
cargo build --release --workspace --features pmacs/crdt

target/release/pmacs --gpu README.md # one-command managed GPU file launch
cargo run --release -- --version     # default-run selects the pmacs binary
cargo test --workspace              # unit + integration tests (all crates)
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings   # incl. pmacs-gpu

Feature matrix

Cargo features fall into two independent axes. Do not use --all-features — it enables both Lua flavors at once, which cannot build (see below).

Feature Axis Notes
luajit Lua flavor Default. LuaJIT backend via mlua (vendored).
lua54 Lua flavor Lua 5.4 fallback for hosts without LuaJIT (big-endian, …).
crdt Buffer Opt-in CRDT-backed buffer mode (adds the loro dep). v1.0 builds enable it; orthogonal to the flavor.

Exactly one Lua flavor must be enabledluajit or lua54, never both (and never neither). They map to mlua's mutually-exclusive Lua backends, so --all-features (or --features luajit,lua54, or --no-default-features with no flavor) fails in the mlua-sys build script with "You can enable only one of the features: …". That check lives in a dependency cargo builds first, so pmacs can't replace it with a friendlier error — the fix is to build a specific flavor. Supported build lines:

cargo build --release                                   # luajit (default)
cargo build --release --no-default-features --features lua54
cargo build --release --features crdt                   # luajit + crdt
cargo build --release --no-default-features --features lua54,crdt

CI, cargo hack, and distro tooling should iterate the flavors explicitly (--no-default-features --features <flavor>[,crdt]) rather than reaching for --all-features. Both flavors pass the full test suite; CI runs the matrix on every push.

Release-only perf gates (M5 keystroke-to-render, M6 ingest/RSS/cancel and scrollback navigation/search) are #[ignore]'d during normal test runs and exercised in CI under dedicated jobs. The GPU frontend has headless render tests (offscreen wgpu, pixels read back) that run in CI under lavapipe and skip gracefully on machines without a Vulkan adapter (PMACS_REQUIRE_GPU=1 turns a missing adapter into a hard failure).

Runtime requirements

The pmacs binary depends on a small set of POSIX command-line tools at runtime. The dependency exists because the project enforces #![forbid(unsafe_code)] everywhere, including in tests; calls that would otherwise need unsafe (PTY raw-mode setup, signal name translation) are routed through trampolines that exec these tools.

  • /bin/sh (POSIX shell). Used for the PTY raw-mode trampoline: /bin/sh -c 'stty raw -echo </dev/tty 2>/dev/null; exec "$@"' -- configures the controlling TTY's line discipline before exec'ing the actual subprocess. Required by the REPL package and any other caller that spawns a process in raw PTY mode.

  • stty (coreutils). The line-discipline configurator invoked by the trampoline above.

  • coreutils more broadly. The M6 process-supervisor tests spawn cat, yes, and which; absent these the test suite (not the editor itself) degrades. which is also used by the M6.5 shell-locator helper to find bash / zsh / fish for per-shell integration tests. The M7.2 fetcher's timeout test uses sleep.

  • setsid (util-linux, Linux only, optional). The process teardown-deadlock test uses setsid --fork to orphan a grandchild, which is the only way to reproduce that deadlock without depending on shell & semantics (they differ between bash and dash). The test skips when setsid is absent, so a minimal or BusyBox environment still runs cargo test --lib; set PMACS_REQUIRE_SETSID=1 to make that skip a failure, as CI does on Linux.

  • /bin/bash (optional, Linux only). The signal diagnostic's job-control corroboration test needs a terminal whose foreground process group is not the spawned leader, which bash -m produces by running a foreground job in its own process group. The path matters: the test spawns /bin/bash directly rather than resolving bash on PATH, and skips when that path is absent. Set PMACS_REQUIRE_BASH=1 to make the skip a failure, as CI does on Linux.

    It is deliberately not armed on macOS, which ships bash 3.2 but where a non-interactive bash -m was measured in CI to keep the terminal on the leader — so the divergence the test needs never happens there. The divergent case is pinned on every platform by injecting the foreground group instead.

  • git (added in M7.2). Required for any package operation: the package fetcher shells out to git to clone, fetch, and resolve refs, with a deterministic environment (GIT_TERMINAL_PROMPT=0, GIT_CONFIG_NOSYSTEM=1, LC_ALL=C, inherited GIT_* variables stripped). Authentication for private repositories rides the user's existing git configuration (credential helpers, SSH agent), so packagers do not need a separate auth story. Pre-M7 builds without package operations do not need git.

  • tar (added in M7.3). Required for pmacs.packages.install: the installer materializes a snapshot via git archive --format=tar piped into tar -x -C <dest>, which keeps the on-disk install directory self-contained (no .git linkage back to the bare cache, no working-tree state). GNU tar and bsdtar both work. Pre-M7 builds and any path that doesn't call pmacs.packages.install{...} do not need tar.

Distribution packagers should ensure these are runtime dependencies of the pmacs package. On a typical Linux distribution, busybox or GNU coreutils plus a shell of any kind satisfies the requirement; on macOS the system shell and /usr/bin/stty are both standard.

The Lua VM (LuaJIT or Lua 5.4) is statically vendored via mlua's vendored feature, so there is no external Lua dependency at runtime.

The GPU frontend additionally needs a Vulkan-capable driver stack (any real GPU driver, or lavapipe for software rendering); its font (JetBrains Mono, OFL-licensed) is bundled into the binary.

Layout

The workspace has three first-party crates:

src/                 pmacs — the core + TUI + daemon
  rope.rs              persistent byte-sequence backing every buffer
  buffer.rs            buffer + view chain + undo/redo
  editor_core.rs       cursor + commands + edit dispatch
  crdt.rs              loro-backed CRDT buffer state (feature `crdt`)
  daemon.rs            instance side of the frontend partition
  attach.rs            frontend side; transports + reconnect
  semantic_render.rs   semantic-frame producer (StyleSpans, Decorations, …)
  lsp.rs               language-server client
  diag.rs, highlight.rs  diagnostic + syntax/semantic-token rendering
  syntax.rs            tree-sitter integration
  search.rs            incremental search (substring + regex)
  minibuffer.rs        prompt, completion, persisted history
  menu.rs              context-menu model
  file_io.rs           atomic saves + external-modification detection
  async_runtime.rs     worker pool + message bus
  process.rs           PTY-aware process supervisor
  ansi.rs              ECMA-48 parser
  project.rs, project_index.rs  project detection + symbol index
  packages/            resolver, fetcher, installer, lockfile, loader
  mcp.rs               MCP client (packages speak to MCP servers)
  lua_bindings/        pmacs.* Lua surface installers
  text_view.rs         cell-grid renderer
  frontend.rs          crossterm TUI
  main.rs              entry point (TUI / daemon / attach modes)

pmacs-protocol/      wire types + framing codec shared by all frontends
pmacs-gpu/           the GPU frontend (wgpu + winit + glyphon)

builtin/             Lua runtime shipped with the binary
  commands/default.lua  named commands for every editor primitive
  keymaps/default.lua   default key bindings
  hooks/default.lua     built-in hook definitions
  menus/default.lua     context-menu items
  runtime/              async, lsp, syntax, mcp, fs runtimes
  packages/repl/        the bundled REPL package

docs/                design notes, framing docs, and the roadmap
tests/               integration tests (acceptance gates per milestone)

License

Dual-licensed under either of:

at your option.