* docs: frame QoL Stage 5, GPU horizontal scroll Stage 4 merged as #222, so the lane advances to its last stage. Rule 4 still does not apply — the arc closes when Stage 5 merges, not before. THE FRAMING'S FIRST FINDING CORRECTS STAGE 4'S. §1.3 there said the GPU "needs a mechanism that does not exist", named it the fact most likely to invert the cost estimate, and I endorsed the Stage 4/5 split partly on that basis. Half of it holds: `Scroll::horizontal` really is discarded throughout, because glyphon 0.11 never applies it when placing glyphs — three doc sites and three asserting tests. But that is not the only mechanism. The document `TextArea` already carries an explicit `left` origin and a `TextBounds` clip whose `left` is `gutter_clip_left`, and horizontal scroll is `left: text_left - offset_px` with the clip unchanged. glyphon then drops what falls left of the gutter — the same "paint from column 0, clip at the edge" shape the grid renderer uses, expressed in pixels. It is machinery the file already depends on, not new machinery. The split stays right for the reason that survives: the three consumers Stage 4 named — caret (`code_byte_px`), decoration geometry (`push_glyph_extent_rects`), hit testing (`gutter_aware_rel_x`) — each produce x relative to `text_left()` and each need the same offset, applied ONCE or they disagree. Shipping that inside Stage 4 would have made one reviewable change into two unreviewable ones. But it was justified partly by an overstatement, and saying so is cheaper than letting a future reader inherit it. No wire, no version bump: the GPU owns its viewport locally, exactly as it owns `scroll_top` and `code_scroll_residual`. The parallel with `ui.line-wrap` is misleading and the doc says why — the MODE is buffer state and needed v22, the OFFSET is viewport state and needs nothing. Five questions, each with my vote. Q#G3 is the one I am least sure of: the GPU can resolve a proportional family, where "column" has no fixed pixel width, so column-for-column parity with the TUI is unachievable. I lean to defining the behavior in pixels and accepting imprecise correspondence rather than gating a navigation feature on a font choice — but that is a product call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * docs: Stage 5 revision 2 — a clip, not just an offset Two functional findings and two record repairs. Q#G3 WAS BUILT ON A FALSE PREMISE, and the correction makes the lane stricter rather than looser. Revision 1 said the GPU can resolve a proportional family and proposed accepting a new TUI/GPU divergence to accommodate it. It cannot: `family_is_monospace_everywhere` gates the family across all four weight/style combinations, `apply_font_facts` falls back when that fails, and `unresolvable_and_proportional_families_fall_back` REQUIRES the fallback. Answered as monospace-only by the font contract that already exists — and the consequence is that the TUI-parity witness becomes UNCONDITIONAL for every font the GPU supports. Revision 1 would have introduced a font-dependent behavior difference to solve a problem the codebase had already solved, in the lane whose purpose is removing unchosen divergence. "THREE CONSUMERS" WAS INCOMPLETE IN A WAY THAT WOULD HAVE SHIPPED A DEFECT. Shifting the `TextArea` clips glyphon's text because glyphon honors `TextBounds`. The manual quad and squiggle renderers have no code-area scissor at all — nothing stops them painting into the gutter, and today nothing needs to, because no code-relative x can be negative. Scrolling makes that false. So the framing now requires TWO shared things: one screen↔code transform, and one code clip rectangle every code-relative painter intersects with. The paths are tabulated with sites — caret rect (`:9698`), caret-painted predicate (`:9734`), glyph extent rects (`:9766`), inline math origins (`:9434`), completion anchor (`:7606`). The two caret sites are the sharpest, and one of them falsifies a claim revision 1 made: `:9734` has no left-edge test, so "the scroll indicator inherits the fix" was false — `code_byte_painted` reuses it and would call an off-left byte painted. And `:9698` does not merely lack a check, it DOCUMENTS the absence as safe ("the caret x can't precede `text_left`"). A comment asserting an invariant this lane deletes is worse than silence. Q#G2: "inert under wrap" was too weak. The offset must be RESET to zero on the wrap transition, as the TUI already does — `horizontal_follow` assigns `view_left = 0` on the wrap branch. Inertness hides a stale value that reappears the moment the buffer toggles back to `truncate`, before any cursor motion. G5 gains a witness that an inertness-only implementation fails. RECORDS. Rule 4's Stage-5 removal precondition was not actually met: the handoff still described Stage 4 as upcoming work. Stage 4's durable facts are now transferred — the unsnapped per-window column with a per-line effective edge, the line-absolute walk, the three-way cell designation, `Viewport::visible_cols` and its five adopters, the wrap-branch reset, the `#[serde(default)]` persistence, and the absence of any wire. The ledger's "Stage 4 ahead" / "Stage 4 plan" text is corrected to Stage 5, and its Rule 4 note now says the removal is legitimate BECAUSE those bullets exist. And the journey-step claim is withdrawn. Revision 1 said this lane completes journey step 4; step 4 is scored on welcome/help/tutorial discoverability and COHERENCE.md:395 holds it Partial for reasons this lane does not touch (`C-h` deletes a word, no tutorial). Restated as preserving interface comprehension with no scorecard movement. §16 is the direct target. Writing an unearned mark into a scorecard is how a coherence document stops being ground truth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * docs: Stage 5 revision 3 — four corrections, one of them impossible Q#G1 CONTRADICTED THE Q#G3 ANSWER IN THE SAME DOCUMENT. It still said the GPU's font "need not be monospace" and that Q#G3 makes "column" ill-defined — both falsified by the answer two sections below, in the same revision that wrote it. The pixel-storage vote is unchanged, but its reasons narrow to the ones that survive, and the conversion is now stated as EXACT: columns × the supported monospace advance. That is what makes the unconditional parity witness checkable at all. Also removed `follow_cursor`, which I invented. The GPU's pass is `ensure_caret_painted`, and it is now named rather than cited by line — robust against the transposition that put these two sites at each other's line numbers in review. Q#G2 WAS MISSING THE BUFFER-SNAPSHOT RESET. The GPU zeroes `scroll_top` and `code_scroll_residual` when a snapshot installs a new buffer; the horizontal offset must reset there for the same reason. Without it a buffer switch INHERITS the previous document's leftward viewport, showing the new buffer scrolled sideways until a cursor motion repairs it — a worse symptom than the wrap case, because nothing about the new buffer explains it. THE GUTTER ASSERTION WAS IMPOSSIBLE, not merely imprecise. Revision 2 proposed asserting that nothing paints left of `gutter_clip_left`. With line numbers on, the gutter DELIBERATELY holds digit glyphs and diagnostic-sign quads, so that assertion fails on a correct implementation — a test that can only be satisfied by removing the gutter. Replaced with the checkable form of the same intent: the gutter rectangle is byte-identical before and after a horizontal scroll, and the left-edge rule is checked against code-relative geometry only. It still catches a code painter bleeding into the gutter, because that changes those pixels. THE COMPLETION ANCHOR HIDES, IT DOES NOT CLOSE. `completion_anchor_px` already returns `None` when the anchor scrolls out, so nothing draws while the daemon-owned completion state and its key handling are retained; actual closure is `CompletionPopup { anchor: None }`, which is the daemon's to send. Revision 2 said "closes", which would have had a viewport-geometry lane quietly redefining when a completion ends. Specified as: no completion paint while the anchor is off-left, popup reappears when it scrolls back, session semantics unchanged. Ledger drift fixed: it still called the framing revision 1 with five questions open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * docs: Stage 5 revision 4 — witnesses for the two rules that had none Both additions cover requirements the framing had already stated and then left untested, which is how a rule becomes a comment. THE SNAPSHOT RESET (Q#G2). Revision 3 added the buffer-snapshot reset and tested only the wrap one. The witness now scrolls buffer A to a non-zero offset, installs a buffer B snapshot, and asserts the offset is zero and B renders at its code origin BEFORE any `CursorByte` arrives. The pre-cursor scoping is the entire test. A later cursor motion repairs the offset regardless, so a witness that waits for one cannot distinguish "reset on snapshot" from "repaired on first motion" — and the second is the defect. Same shape as the wrap witness, which is also scoped to before any motion, and for the same reason. THE MINIMAP (Q#G4). The vote is "no movement", and the implementation already supports it: the minimap derives from the summary, the surface dimensions and `scroll_top`, with no horizontal input. So the witness pins an existing property rather than requesting work — which is exactly why it is worth writing. An offset threaded one seam too far would break it silently, and nothing else in G5 would notice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * docs: Stage 5 approved, five questions resolved G1 pixels with exact conversion via the supported monospace advance; G2 automatic cursor-follow only, zeroing on both the wrap transition and BufferSnapshot; G3 monospace-only by the existing font contract; G4 minimap unchanged; G5 accepted whole, including the snapshot-reset and minimap-stability witnesses. The scope boundary is restated in both documents because it is what keeps this lane small: local GPU viewport state, no wire message, no protocol bump, no command surface, no minimap movement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * feat(gpu): horizontal scroll — the transform, the clip, and both resets Stage 5, partial. The mechanism and lifecycle are in; two consumers and the whole verification set are NOT yet done — see the tail of this message, which is a status, not a summary. WHAT IS IN. The offset, `code_scroll_left`, in pixels (Q#G1). Column parity stays exact because the code font is monospace by contract, so `columns × advance` is a definition rather than an approximation. Local viewport state: no wire, no version bump. One screen↔code transform (`code_x_to_screen` / `screen_x_to_code`) and one code clip (`code_clip_left` / `survives_code_clip_left`), which is the pair framing §1.1 requires. Written before any consumer moved, because five sites deriving the same offset independently is how the caret and the glyphs it sits among come to disagree. The glyph-side mechanism is one line: the document `TextArea`'s `left` shifts while its `bounds.left` stays at the gutter, so glyphon clips and the gutter keeps its own pixels. BOTH LIFECYCLE RESETS (Q#G2), which were the two rules most likely to be left as comments. The wrap transition zeroes the offset in `apply_line_wrap` — inertness would park a stale value that reappears the instant the buffer toggles back to `truncate`. The buffer snapshot zeroes it beside `scroll_top` and `code_scroll_residual`, or a buffer switch inherits the previous document's leftward viewport and shows the new buffer scrolled sideways until a cursor motion repairs it. `code_caret_rect_in_clip` gains its left-edge test, and its comment is REWRITTEN rather than extended: it used to assert "the caret x can't precede `text_left`", an invariant this stage deletes. A comment asserting something a later stage falsifies is worse than silence. That also repairs `code_byte_painted`, which reuses it — revision 1's claim that the scroll indicator "inherits the fix" was false precisely here. `gutter_aware_rel_x` is now the exact inverse of the transform, with the gutter clamp applied in screen space first: a click in the gutter band means "the first visible column", which after scrolling is the offset, not column 0. The completion anchor HIDES when scrolled off-left and does not close — the daemon owns completion state and its key handling, and closure is `CompletionPopup { anchor: None }`, which is the daemon's to send. `horizontal_follow` mirrors the TUI's: automatic only, scroll just far enough, so a caret already visible never moves the view. It runs after `normalize_code_scroll` because it reads the caret's laid-out x, which vertical normalization can change. WHAT IS NOT IN, and must land before this is reviewable: - `push_glyph_extent_rects` — washes, squiggles and selection extents still paint at unshifted x and are not cropped at the gutter. - Inline math origins (`:9434`) — same. - Every Q#G5 witness. The 228 existing GPU tests pass, which says only that nothing regressed at offset 0; not one of them exercises a non-zero offset. Gates so far: fmt; clippy --workspace --all-targets -D warnings; PMACS_REQUIRE_GPU=1 -p pmacs-gpu 228/0; git diff --check. The full two-configuration sweep is deliberately not claimed — the lane is not finished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * feat(gpu): the last two painters move, and twelve witnesses say so Completes Stage 5. `62fb93e` landed the transform, the clip and both resets but left two code-relative painters at unshifted x and the whole Q#G5 witness set unwritten; its 228 green tests said only that nothing regressed at offset 0. The two painters: - `push_glyph_extent_rects` — selection/search washes, peer presence and diagnostic squiggles. Shifted through `code_x_to_screen`, then CROPPED at the gutter rather than dropped: a selection running in from off the left edge must paint the part that is visible. That is the same boundary Stage 4's review caught the TUI painter getting wrong, and it would have been easy to reproduce here. - Inline math. The glyph mini-buffers only needed their origin moved — their layer already carries the code area's `TextBounds`. The fraction rules are quads in the background batch with no scissor of their own, so those are cropped by hand. `crop_to_code_clip_left` is the crop, and `survives_code_clip_left` now delegates to it, so a caret the crop would discard is never painted. One boundary rule, not two that agree today. TWELVE WITNESSES, EACH MUTATION-TESTED. Eleven production mutations — unshifted wash x, uncropped wash, unshifted math origin, uncropped math rule, untested caret left edge, missing snapshot reset, missing wrap reset, unhidden completion anchor, unscrolled glyphs, inverted hit-test sign, pixel-instead-of-column snap — each fail the intended witness as an ASSERTION failure, not a compile error. The minimap-stability witness was mutation-tested separately by threading the offset into `minimap_vertex_bytes`. That battery earned its keep immediately. The gutter byte-identity test's "the code area must actually have moved" assertion is satisfied by a decoration wash and the caret alone, so it PASSED with `TextArea.left` pinned to `text_left` — the entire glyph-side mechanism was unwitnessed and nothing in review would have shown it. Its replacement isolates the glyph layer: no decorations, and a source line carrying no caret, whose band is blank at offset 0 and inked after. ONE DELIBERATE STEP OUTSIDE THE APPROVED SCOPE, and it needs a ruling. Q#G5 asks for frontend agreement that is "checkable rather than asserted". Two tests in two crates asserting the same literal is not that; it is the structural duplication `pmacs-protocol::scroll`'s own module docs condemn, and that module exists because THIS ARC already shipped that defect — the scroll indicator, fixed in one copy and left wrong in the other. So the follow rule moved to `pmacs_protocol:📜:follow_left`, beside `classify`, and both frontends call it: `src/editor.rs::horizontal_follow` delegates, and the GPU converts px <-> columns around it, exact by Q#G3. The cost is that Stage 5 now touches `src/editor.rs`, which "local GPU viewport state" does not cover. No wire message and no version bump — the same argument `classify` already makes. If rejected, reverting is small: restore the four-line conditional, drop `follow_left` and its four protocol tests, rewrite the parity witness as a two-sided pin. GATES, both configurations, five ambient roots isolated: fmt; clippy `--workspace --all-targets -D warnings`; `--lib` 1920 and `--lib --features crdt` 2105; horizontal_scroll 11, long_line_readable 3, line_wrap 6, full_grid_resync 1; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu` 239; `-p pmacs-protocol --lib` 29; both full workspace sweeps; `git diff --check`. TWO SWEEP FAILURES, NEITHER THIS LANE'S, both logged: - R8, new row: `flat_listview_consumers_render_byte_identically...` fails DETERMINISTICALLY, and the merge-base control is done — it fails identically on `main`. The row renders with a leading directory stripped; it is a prefix strip, not width truncation, and the mechanism is NOT diagnosed. Deliberately not fixed here. - U3: the R7 selector failed once and passed on rerun. Recorded as a new incident, NOT an R7 match — different flavor, and its fragments are unverified because I filtered the sweep output before reading it. U2 records me making that exact mistake already. Sweeps go to a file from now on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * fix(gpu): the completion anchor is a point, and the witness now says where Review round 1. One defect, and a lesson about the witnesses that missed it. THE DEFECT. `completion_anchor_px` reused `survives_code_clip_left` and passed `line_height` as the horizontal extent — a VERTICAL dimension standing in for a horizontal one. The predicate is `screen_x + w > code_clip_left()`, so an anchor up to a whole line height left of the gutter "survived". `completion_dropdown_rect` bounds `ax` against the right margin only, so that x reached the popup's left edge and painted over the line numbers. An anchor is a position between glyphs. It has no width, and the popup it places is drawn to its right. So the predicate is a point: `screen_x < code_clip_left()`. The absent left clamp downstream stays absent, deliberately. This predicate is what guarantees `ax >= code_clip_left()`; a second clamp would be a duplicate of the same rule, which is the failure mode this stage's shared-transform design exists to avoid. It is witnessed instead. THE LESSON, which is the more useful half. The existing test placed the anchor 200px off-left — and 200px off-left fails a width-based predicate too, so it stayed green straight through the defect. The mutation battery agreed with it, because every mutation asked only whether REMOVING a check was caught, never whether the check had the right shape. A boundary must be tested AT the boundary. The new witness straddles it by ±0.05px — the same anchor either side of the edge, which no width-based predicate can separate — and additionally asserts the popup's own left edge stays out of the gutter, making "no left clamp needed downstream" a checked claim rather than a comment. Verified both ways: the new witness fails against the original predicate, the old one passes against it. THE AUDIT that finding prompted. Stage 5 has one other left-edge predicate, the caret's. Its use of `survives_code_clip_left(rect.x, rect.w)` is correct — a caret quad genuinely is `CARET_WIDTH` wide — and it was also only tested far from the edge. It is now walked ACROSS the boundary a column at a time, asserting painted carets are wholly inside the code area and hidden ones wholly outside. That pins an argument that was load-bearing and invisible: because `horizontal_follow` snaps to whole columns, a caret is never partly behind the gutter, since `CARET_WIDTH` (2px) is far below any code advance. Substituting `rect.h` for `rect.w` — the exact error above — fails it. An over-width smaller than one advance does not, and that is the invariant rather than a gap. SCOPE. `follow_left` recorded as the one approved exception to "local GPU viewport state" in the framing doc, new §1.2a: what it is, why the Q#G5 parity witness cannot be real without it, and what it does not do — no viewport state moved, no wire message, no version bump. GATES, both configurations, five ambient roots isolated, sweeps redirected to files per U3's lesson: fmt; clippy `--workspace --all-targets -D warnings`; `--lib` 1920 and crdt 2105; `-p pmacs-protocol --lib` 29; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu` 241; horizontal_scroll 11, long_line_readable 3, line_wrap 6, full_grid_resync 1; both full workspace sweeps; `git diff --check`. The only sweep failure is R8, confirmed by its recorded fragments — pre-existing, deterministic, merge-base controlled against `main`, and not this lane's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * docs: Stage 5 is PR #223, head 55faa45 The ledger said "no PR opened yet", which stopped being true the moment it was. Records the PR, its head SHA, and the standing do-not-merge. Rule 4 still applies at merge, not now: the long-lines lane stays until #223 lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai * docs: the tip is the ref, not a SHA the commit itself invalidates The previous commit wrote "head 55faa45" into the ledger and, by existing, made it false — recording the PR moved the head to 4902048. A SHA pinned in a document that the act of writing it stales is a trap, not a record. The ledger already states the correct convention two paragraphs down ("the authoritative tip — the ref, not a SHA"); this follows it, and says to verify CI against the PR's live headRefOid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .github/workflows | ||
| audit | ||
| builtin | ||
| docs | ||
| pmacs-gpu | ||
| pmacs-protocol | ||
| proptest-regressions | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| COHERENCE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE-APACHE | ||
| LICENSE-MIT | ||
| README.md | ||
| TEST_IMPROVEMENT.md | ||
| build.rs | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
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 enabled — luajit 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. -
coreutilsmore broadly. The M6 process-supervisor tests spawncat,yes, andwhich; absent these the test suite (not the editor itself) degrades.whichis also used by the M6.5 shell-locator helper to findbash/zsh/fishfor per-shell integration tests. The M7.2 fetcher's timeout test usessleep. -
setsid(util-linux, Linux only, optional). The process teardown-deadlock test usessetsid --forkto orphan a grandchild, which is the only way to reproduce that deadlock without depending on shell&semantics (they differ betweenbashanddash). The test skips whensetsidis absent, so a minimal or BusyBox environment still runscargo test --lib; setPMACS_REQUIRE_SETSID=1to 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, whichbash -mproduces by running a foreground job in its own process group. The path matters: the test spawns/bin/bashdirectly rather than resolvingbashonPATH, and skips when that path is absent. SetPMACS_REQUIRE_BASH=1to 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 -mwas 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 togitto clone, fetch, and resolve refs, with a deterministic environment (GIT_TERMINAL_PROMPT=0,GIT_CONFIG_NOSYSTEM=1,LC_ALL=C, inheritedGIT_*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 forpmacs.packages.install: the installer materializes a snapshot viagit archive --format=tarpiped intotar -x -C <dest>, which keeps the on-disk install directory self-contained (no.gitlinkage back to the bare cache, no working-tree state). GNU tar and bsdtar both work. Pre-M7 builds and any path that doesn't callpmacs.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:
- MIT License (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option.