13 KiB
Journey Stage 1b-3 — say something when the editor opens
Status: framing, rev 1 — awaiting approval.
Serves COHERENCE.md §2 (the golden journey, step 4), §18
(onboarding), §19, §20 Priority 1.
0. Revision history
- rev 1 (2026-07-30) — first framing. Scouted against
githubsucks/main@1f290d5(Journey Stage 1b-1, #203).
1. What this stage is
The last of the 1b split. COHERENCE.md §20 named three things:
1b-1 (compile binding + defaults, landed as #203), 1b-2 (LSP
spawn guidance, PR #204), and this — the welcome buffer.
§18 is unusually specific about the size of it:
the cheap floor — a welcome buffer in
*scratch*namingM-x, the keybinding cheat sheet as a help buffer, and a help prefix decision — has no prerequisites at all.
This stage takes the first of those three. §2's step-4 row lists all three as what is missing, so §5 is explicit that step 4 does not reach Works here, and says what remains.
2. Ground truth
Verified in the tree at 1f290d5.
2.1 A fresh launch says nothing, and the audit's citation is right
EditorCore::new sets status: String::new() (src/editor_core.rs:659)
and creates *scratch* (:629). A user who runs pmacs with no
arguments and no config gets an empty buffer, an empty status line, and
no indication that M-x exists.
builtin/keymaps/default.lua's own header comment calls M-x the
"command palette" — the sole discovery affordance in the product is
knowing to press it.
2.2 The step-2 ratchet pin will collide with any status-line welcome
This is the finding that most shapes the design.
tests/journey_acceptance.rs:
fn journey_step2_launches_unconfigured_into_scratch() {
let s = EditorState::new();
assert_eq!(active_name(&s), "*scratch*");
assert!(
status(&s).is_empty(),
"a clean launch reports no error; got {:?}", status(&s)
);
}
The assertion's message says "reports no error"; the assertion itself says the status is empty. Those are the same predicate only while nothing ever writes a non-error status at startup — which is exactly what a welcome would do.
So a status-line welcome does not merely need a new row, it needs that existing pin's predicate corrected to what its own message already claims. §4 treats that as a deliberate, named amendment rather than a silent edit, because the ratchet's rule is stages add rows, none removes them and an assertion change deserves the same scrutiny.
2.3 C-h is not free, and the reason is load-bearing
§18 lists "a help prefix decision" as part of the cheap floor. It is cheap to decide and not cheap to get wrong:
-- builtin/keymaps/default.lua:78-86
-- Why we also bind C-h: most terminals (anything not implementing the
-- kitty keyboard protocol) cannot disambiguate Ctrl+Backspace from
-- Ctrl+H — both legacy paths produce byte 0x08 …
bind("C-h", "buffer.delete-word-backward")
Rebinding C-h to a help prefix would break Ctrl+Backspace on every
non-kitty terminal, because those terminals cannot tell the two apart.
§2's step-4 row says "C-h deletes a word" as though it were an
oversight; it is a deliberate trade with a stated rationale.
This stage therefore does not touch C-h (§5), and the framing
records the constraint so the discovery arc inherits the reason rather
than rediscovering it.
2.4 There is already a *help* buffer mechanism
builtin/commands/default.lua:1226-1260 has show_help_text(text): a
single reused *help* buffer, found by name, with a buffer-local q →
buffer.kill-this. editor.describe-command and
editor.describe-setting both render through it. src/help.rs supplies
render_command / render_key / render_buffer / render_mode /
render_view plus link resolution (link_at, follow_link_at).
Two facts about it that matter to any consumer:
- It writes with
buf:delete+buf:insert, notpmacs.buffer.set_generated_contents. It is one of the writer mechanisms that has not adopted the generated-buffer write invariant, so its buffer stays ordinarily editable and keeps its undo history. Riding it inherits that; fixing it is not this stage's job, but a stage that renders into it should not claim read-only guarantees it does not have. - It is found by name, which is the adoption hazard
listviewand dired both refused. A foreign*help*buffer would be cleared.
2.5 A modified *scratch* does not block quitting
editor.quit runs the editor.before-quit hook and then ed.quit()
(builtin/commands/default.lua:250-256). There is no unsaved-buffer
confirmation, so welcome text left in *scratch* cannot strand a user
at exit. That removes the strongest objection to putting text in the
buffer, and it is stated because it is the kind of thing that would
otherwise be assumed in either direction.
3. Design
3.1 What the welcome says
Three lines, no more. The floor is "this editor is not inert and here is the one key that opens everything":
Welcome to pmacs. M-x run a command C-x C-f open a file
C-c c build C-c t terminal
This buffer is *scratch* — type to edit it, or M-x help for more.
Every key named must be bound in the default keymap and verified by the acceptance suite, or the welcome becomes documentation drift with a user attached. §4 pins that as a property over the message, not a hardcoded list.
3.2 Where it goes — *scratch*, not the status line
The status line is the wrong vehicle even though §2's audit cites it:
it is overwritten by the next status write, and a user who blinks loses
the only pointer to M-x. §18 says "a welcome buffer in *scratch*",
and that is right for the reason the audit itself gives — the empty
*scratch* buffer is what greets the user.
So: welcome text is rendered into *scratch*, under three
conditions, all necessary:
- No arguments.
pmacs FILEandpmacs DIRboth put something else on screen; a welcome would be noise, and for the directory case it would fight Stage 1a's dired listing. *scratch*is empty. Never overwrite content — including a restored session's scratch, or anything aninit.luawrote.- It leaves the buffer unmodified, so nothing about the greeting looks like unsaved work. §2.5 shows quitting is not blocked either way; this is about not lying in the modeline.
3.3 What it must not do
- Not read-only. Step 5 is "edit immediately"; a read-only greeting would break the journey one step after it starts. The user types over it, as in Emacs.
- Not a new buffer. A
*welcome*buffer displayed instead of*scratch*would change what step 2 asserts and give the user a buffer whose only purpose is to be closed. - Not
set_generated_contents. That lifts the rope's read-only, discards history, and marks the buffer generated — all wrong for a buffer the user is meant to edit immediately. This is the one place where not adopting the generated-buffer invariant is correct, and it is stated so a later audit does not "fix" it.
3.4 M-x help
The welcome names M-x help, so that command has to exist. It renders
the keybinding cheat sheet through the existing show_help_text
mechanism (§2.4).
This is the smallest possible version of §18's second item, and it
is included only because the welcome would otherwise point at nothing.
The full cheat sheet, where-is, describe-key and the help-prefix
question belong to §20 Priority 4's discovery arc (§5).
4. Acceptance
N = new behaviour, must fail on full revert. P = preservation, falsified by a named mutation.
- N — journey step 4, through the real entry point.
EditorState::new()— the same constructionpmacswith no arguments performs — leaves*scratch*active and non-empty, and its text namesM-x. This is the ratchet row. - N — every key the welcome names is actually bound. Parse the key
sequences out of the rendered text and assert each resolves through
pmacs.keymap.lookup. A property over the message, so the pin cannot rot when the wording changes — and so the welcome cannot advertise a binding that a later stage removes. - N —
M-x helprenders the cheat sheet into*help*, containing at least the keys the welcome names. Asserts content produced. - P — the buffer is editable and unmodified. Typing a character
into the greeted
*scratch*inserts it (step 5 still works from the first frame), and the buffer reports unmodified before that keystroke. Targeted mutation: rendering throughset_generated_contents, which would make the buffer read-only and fail the insert. - P — a file argument suppresses the welcome.
EditorState::openon a file leaves that file active with no greeting anywhere. Targeted mutation: greeting unconditionally inEditorCore::new. - P — a directory argument suppresses it too, so Stage 1a's dired listing is what the user sees. Same mutation; separate pin because the directory path reaches scratch differently (the bootstrap replaces the buffer rather than never creating it).
- P — a non-empty
*scratch*is never overwritten. Write to scratch, then trigger the greeting path: the content survives. - P (amended pin) — step 2 still reports no error.
journey_step2_launches_unconfigured_into_scratchcurrently assertsstatus.is_empty()while its message says "reports no error" (§2.2). The assertion is corrected to the message's claim — no error text on the status line — rather than deleted or weakened. This is the one existing assertion this stage changes, it is called out here rather than buried in the diff, and the row itself is kept.
5. Deferred, and why
- The help prefix /
C-h. §2.3: rebinding it breaks Ctrl+Backspace on every non-kitty terminal. The decision belongs with §20 Priority 4's discovery arc, which can weigh a prefix (F1?C-c ??) across the whole command family instead of one key at a time. - The full cheat sheet and the discovery command family —
where-is,describe-key, richer M-x rows: §20 Priority 4, called "the best payoff-per-effort in this document". - Making
show_help_textadopt the generated-buffer write invariant (§2.4). A real gap, with its own lane. - Onboarding proper — §18's ten-step teaching sequence. This stage is the floor beneath it, not a down payment on it.
Step 4 therefore stays Partial, and the PR must say so: §2's row
names a welcome, a cheat sheet and C-h, and this closes the first
plus a minimal version of the second.
6. Coherence impact
- Journey steps touched: 4 (Partial → still Partial, with the welcome half closed); 2 indirectly, whose pin is amended (§4.8). No grade flips on merge, which makes this the first 1b stage with no §25 landed-evidence obligation.
- Interaction islands: none added.
M-x helprenders through the existing*help*mechanism rather than inventing a second help surface — though §2.4 records that mechanism's own two gaps rather than pretending it is exemplary. - Config registry: not adopted. A
welcome.enabledscalar would be expressible, and is deliberately not added — a greeting the user can delete by typing does not need a setting, and §11's adoption work should not gain a token entry from a stage this small. - Background-work attribution: unchanged; nothing here is asynchronous.
- Docs riding the PR:
COHERENCE.md§2's step-4 row and §18's ground-truth grade (both stay Partial, with the closed half named);docs/keybindings.mdgainsM-x help;docs/agent-handoff.md§1; the ledger.
7. Questions
- Q#W1 — three lines, or one? One line ("
M-xruns a command") is the true floor and never wraps at 80 columns. Three teaches more but risks looking like chrome the user must clear. Recommended: three, because the whole complaint in §18 is that the editor teaches nothing. - Q#W2 — should the welcome name
C-c candC-c t? They are real, bound, and journey steps 8 and 9 — but naming them means the welcome must be updated whenever the default map changes. Acceptance 2 turns that from a risk into a caught failure, which is the argument for naming them. - Q#W3 — is
M-x helpthe right name?helpis short and guessable. The discovery arc may wanthelp.keys/help.commandsas a family, and renaming later costs a deprecation.
8. Ledger
Branch journey-stage1b3-welcome, worktree ../pmacs-journey-1b3,
based on githubsucks/main @ 1f290d5. Framing only; no code, no PR.
#204 is open and touches COHERENCE.md, docs/agent-handoff.md and
docs/active-work.md. This lane will conflict there; integrate late
(at PR time), never by opening a refresh PR.
git fetch githubsucks
git worktree add ../pmacs-journey-1b3 \
-b journey-stage1b3-welcome \
githubsucks/journey-stage1b3-welcome