487 lines
24 KiB
Markdown
487 lines
24 KiB
Markdown
# GUI zoom — QoL Stage 2
|
||
|
||
**Status: revision 5 — APPROVED, IMPLEMENTED, in review as PR #220.**
|
||
Revision 5 closes a gap review found in the shipped code: the
|
||
round-trip guarantee in §3.1 needed the step to be **quantized where it
|
||
is used**, because the registry cannot enforce precision (§3.2). Q#Z1 approved as
|
||
**(c)** (configured base, `None` preserved) and Q#Z2 as **additive**.
|
||
Revision 4 fixes a self-contradiction in §5a1: the specified parser
|
||
`^(%d+)$` rejects the newline-terminated format specified beside it.
|
||
Whole-file `^(%d+)\n$`, with a verified case table and a valid-restore
|
||
witness. Revision 3 added the contract details a second pass asked for: §1 and §7
|
||
no longer claim a keybinding this stage does not ship, §3.1 bounds and
|
||
quantizes both settings, and §5a1 defines the persisted format and what
|
||
corrupted state does. Revision 2 answered four review findings that revision 1 got wrong or
|
||
left out: Z3 was **not implementable as written** (§4), Z4 had **no
|
||
working restore seam** (§5), every size write **silently clears a
|
||
configured family** (§5a), and reset/precedence semantics were
|
||
unspecified (§5b). Reported from daily-driver use
|
||
alongside Stage 1: `Ctrl +/-` has *no effect whatsoever* in the GPU
|
||
frontend. Stage 1 (#219) fixed what zoom did to the TUI; this stage is
|
||
the other half — making the GUI zoom at all.
|
||
|
||
**Most of this already exists**, which is why it is a short stage. It
|
||
is not, however, a trivial one: relative zoom needs a starting point,
|
||
and the daemon is deliberately built never to know one (§3).
|
||
|
||
---
|
||
|
||
## 1. What is already built
|
||
|
||
The whole mechanism ships today and is exercised by tests:
|
||
|
||
| piece | where | state |
|
||
|---|---|---|
|
||
| Scale that drives **everything** | `FontMetrics { scale, advance_ratio }`, `pmacs-gpu/src/main.rs:129`+ | code size, line height, status band, divider, menu rows, minibuffer dropdown, gutter advance all derive from it |
|
||
| Daemon-side preference | `src/font_pref.rs` — family + `size_centi_px` + monotonic `epoch` | written by `pmacs.gpu.set_font`, read per-frame by the producer |
|
||
| Wire relay | `InstanceMessage::FontFacts`, protocol v17 | bufferless; v16 peers get none |
|
||
| Frontend application | `apply_font_facts`, `pmacs-gpu/src/main.rs:8132` | one transaction: validate, resolve family, re-metric all seven buffers, reshape at retained scroll, re-follow caret |
|
||
| Getter | `pmacs.gpu.font()` | fresh table, `size` in logical px, key absent when unset |
|
||
| Validation | `600..=7200` centi-px (6.0–72.0 px), checked at both ends | Lua range-check is UX; the frontend re-checks because it is wire input |
|
||
|
||
There is even a byte-identity test that `(None, None)` reproduces the
|
||
never-set frame exactly.
|
||
|
||
**So this stage adds no rendering work.** It adds **commands**, a
|
||
starting point, and remembering — **and deliberately no default
|
||
keybindings**, per Q#Z3 (§4): the keymap cannot express "GPU frontends
|
||
only", and neither a protocol variant nor a rushed keymap change is
|
||
worth a default chord.
|
||
|
||
---
|
||
|
||
## 2. Q#Z1 — where does a *relative* zoom start? (the real question)
|
||
|
||
`src/font_pref.rs` is explicit:
|
||
|
||
> *"The daemon relays a PREFERENCE: it never learns metrics, advances,
|
||
> or what resolves (the no-pixels invariant); the frontend owns
|
||
> resolution and every pixel consequence."*
|
||
|
||
And `size_centi_px: None` is **a real state, not an absence** — "the
|
||
frontend's built-in default", which Q#TH7 established must never be
|
||
inferred from silence.
|
||
|
||
Zoom-in is `current + step`. On a fresh session `current` is `None`,
|
||
and the daemon cannot ask what the frontend resolved. Three ways out:
|
||
|
||
- **(a) Hard-code the frontend's default (16.0) as the origin.** One
|
||
line, and it puts a pixel constant on the daemon side — the exact
|
||
thing the no-pixels invariant forbids. It also silently breaks for
|
||
any frontend whose default differs, which is the situation the
|
||
invariant exists to allow.
|
||
- **(b) Always send an explicit size**, sourced from a config setting
|
||
defaulting to 16.0. Simple, but it **destroys the `None` state for
|
||
everyone**: a user who never zooms now gets an explicit size, the
|
||
frontend's own default query never runs, and Q#F5's always-shipped
|
||
all-default baseline stops being reachable.
|
||
- **(c) A configured *base*, with `None` preserved until the user
|
||
actually zooms.** `ui.gpu-font-size-base` (logical px, default 16.0)
|
||
is the documented origin for the *first* zoom step. Until a zoom
|
||
happens the preference stays `None` and the frontend's default is
|
||
used exactly as today.
|
||
|
||
**Recommendation: (c).** It is the only one where the daemon still
|
||
infers nothing — the base is a *user-facing preference about zooming*,
|
||
not the daemon guessing a frontend metric. A user whose frontend
|
||
default is not 16.0 changes one setting, which is a real answer rather
|
||
than a broken assumption. And the untouched path is byte-identical to
|
||
today, so the existing `(None, None)` identity test keeps its meaning.
|
||
|
||
**Q#Z1 — DECIDED: (c)**, on review.
|
||
|
||
---
|
||
|
||
## 3. Q#Z2 — step shape
|
||
|
||
Additive (`+1.0 px`) or multiplicative (`×1.1`)?
|
||
|
||
**DECIDED: additive**, on review — via `ui.gpu-zoom-step` (logical px,
|
||
default 1.0).
|
||
|
||
### 3.1 Both settings are validated, and the bounds are not cosmetic
|
||
|
||
The registry validates at `define` time (`min`/`max`, as
|
||
`autosave.interval-ms` does), so both settings are constrained rather
|
||
than checked at use:
|
||
|
||
| setting | bounds | why |
|
||
|---|---|---|
|
||
| `ui.gpu-font-size-base` | **6.00 – 72.00** logical px | exactly the wire range (`600..=7200` centi-px). A base outside it could never be sent, so the first zoom step would fail from a value the user was allowed to set |
|
||
| `ui.gpu-zoom-step` | **0.01 – 66.00** logical px, strictly positive | see below |
|
||
|
||
**The step's lower bound is the quantizer.** `validate_font_size`
|
||
range-checks the original value and then rounds to the nearest
|
||
hundredth, so a step below `0.01` quantizes to **zero** — "zoom in"
|
||
would do nothing, forever, with no error anywhere. `0.01` is one
|
||
centi-pixel, the smallest representable step.
|
||
|
||
**Zero and negative are excluded for a stronger reason than tidiness.**
|
||
A negative step **inverts** the commands: `gpu.zoom-in` would shrink.
|
||
That is not a malfunction the user can diagnose from the outside — the
|
||
command does something coherent, just the opposite of its name.
|
||
|
||
**The upper bound is the range span** (72.00 − 6.00). A step larger
|
||
than the whole domain can only ever clamp or be rejected, so permitting
|
||
it buys a setting that cannot be used.
|
||
|
||
### 3.2 The bounds are not sufficient on their own
|
||
|
||
Review found the gap in the implementation: `ConfigKind::Number`
|
||
validates **finiteness and bounds and nothing else**
|
||
(`src/config_registry.rs`), and `on_change` listeners are notified
|
||
*after* a value is stored — they cannot veto. So `0.015` is a
|
||
perfectly settable step, and nothing in the registry can refuse it.
|
||
|
||
Used raw it breaks the guarantee below. Each operation rounds
|
||
independently, and both intermediates land on an **exact tie** —
|
||
`16.015` and `16.005` are `1601.5` and `1600.5` centi-pixels — which the
|
||
half-up quantizer sends **up**. Half-up is not symmetric under negation:
|
||
rounding up on the way in adds half a centi-pixel, rounding up on the
|
||
way out adds another, so the two errors accumulate instead of
|
||
cancelling:
|
||
|
||
```
|
||
step 0.015: 16.00 -> 16.02 -> 16.01 round trip broken
|
||
step 0.37 : 16.00 -> 16.37 -> 16.00 round trip holds
|
||
```
|
||
|
||
The original test used `0.37` — centi-pixel representable — so it could
|
||
not reach this.
|
||
|
||
**Resolution: quantize the step and the base where they are used.** Not
|
||
a workaround: sizes live in integer hundredths end to end, and
|
||
`validate_font_size` already range-checks the original and then rounds
|
||
to the nearest hundredth. Rounding the step is that same operation one
|
||
level up. A step of `0.015` is not a finer step in this domain, it is
|
||
`0.02` written imprecisely.
|
||
|
||
Enforcing at `set` time was considered and rejected: the registry
|
||
cannot express it, and a validating wrapper is bypassed by a direct
|
||
`pmacs.config.set` — the same seam `autosave` documents about its own
|
||
`interval_ms` wrapper. Quantizing at the point of use cannot be
|
||
bypassed. Both settings say "quantized to hundredths" in their
|
||
`description`, so `describe-setting` shows it.
|
||
|
||
**These bounds and that quantization are what make the round-trip claim
|
||
true.** "n steps in,
|
||
then n steps out, returns to exactly the starting value" holds because
|
||
the step is centi-pixel representable and addition is exact in that
|
||
domain — *provided no clamp occurred*, which is why §6 requires an
|
||
out-of-range zoom to leave the preference **unmutated** rather than
|
||
pinning it to the boundary. Pinning would silently break the round trip
|
||
at the edges, which is precisely where a user is most likely to be
|
||
stepping back and forth. The domain is integer hundredths of a pixel over
|
||
6.0..=72.0 — a small, bounded, quantized range where additive steps are
|
||
predictable and land on round numbers. Multiplicative stepping
|
||
accumulates rounding through the quantizer and makes "two in, two out"
|
||
fail to return to where you started, which is the property users
|
||
actually notice.
|
||
|
||
---
|
||
|
||
## 4. Q#Z3 — keys. Revision 1 was not implementable
|
||
|
||
Revision 1 said "do not install the binding on a non-GPU frontend."
|
||
**There is no such thing.** `keymap_stack::Scope` is exactly
|
||
`Buffer(BufferId) | Mode(String) | Global`, and neither `resolve()` nor
|
||
`keymap_tree` receives any frontend identity or capability. A global
|
||
binding is global — it would capture `C-+` / `C--` in the TUI, which is
|
||
the outcome §4 claimed to avoid.
|
||
|
||
Nor is there a way for the GPU to ask for a command by name:
|
||
`FrontendEvent` is `Key | Mouse | Resize | Paste | FocusGained |
|
||
FocusLost | Detach | CrdtOp`. There is **no command-invocation
|
||
variant**, so "the GPU intercepts the chord locally" cannot reach the
|
||
daemon-side preference that owns the value and its persistence.
|
||
|
||
So there are three real options, and none is free:
|
||
|
||
- **(A) Capability-aware keymap resolution.** Teach `Scope` — or
|
||
`resolve()`'s inputs — about the requesting frontend. Correct and
|
||
general, and it would serve every later frontend-specific binding.
|
||
But it touches the keymap core, every resolve call site, and the Lua
|
||
`bind` surface. That is its own lane, not a step inside a zoom lane.
|
||
- **(B) A new `FrontendEvent` for it.** Narrow and direct, but it is a
|
||
**protocol addition** — schema bump and negotiation — which §8
|
||
explicitly scopes out, and which buys a keybinding rather than a
|
||
capability.
|
||
- **(C) Ship commands now; bind later.** `gpu.zoom-in` / `-out` /
|
||
`-reset` exist and work from `M-x` and from `init.lua`, where a user
|
||
who runs the GPU can bind them themselves in one line. No core
|
||
change, no protocol change, no half-built capability.
|
||
|
||
**Recommendation: (C), with (A) recorded as the follow-on it implies.**
|
||
The value the report asked for is *zoom working at all*; the default
|
||
keybinding is the smaller half, and buying it with either a protocol
|
||
variant or a rushed keymap change trades a large permanent surface for
|
||
a small convenience. **(A) is the right eventual answer** — "this
|
||
binding applies to frontends with capability X" is a question this
|
||
project will keep asking — and it deserves its own framing rather than
|
||
being smuggled in here.
|
||
|
||
**What must NOT happen** is a global binding plus a command that
|
||
reports "not applicable" in the TUI. That is the Q#P3 fall-through
|
||
lesson and #217's flat-panel `TAB` finding: capturing a key to deliver
|
||
an apology removes a working behaviour — and here the working
|
||
behaviour is *the terminal's own zoom*, which is exactly what the user
|
||
is pressing the key for.
|
||
|
||
*Tempting and rejected:* "terminals swallow `C-+` anyway, so a global
|
||
binding is harmless in practice." Most do. Relying on most is how a
|
||
binding becomes a bug report from whoever runs the terminal that
|
||
doesn't.
|
||
|
||
## 5. Q#Z4 — remembering it, and the seam revision 1 did not have
|
||
|
||
The config registry has **no write-back** — settings are declared, not
|
||
saved. The project's remembered-state mechanism is `pmacs.state.read` /
|
||
`write` / `available` (`docs/persistence-framing.md`), used by
|
||
`saveplace` and `recentf`. The split is the right one:
|
||
|
||
- **`pmacs.config`** — what you *declared*: base size, step.
|
||
- **`pmacs.state`** — what you *arrived at*: the current zoom level.
|
||
|
||
### 5.1 Reading at module load is inert, and revision 1 assumed it was not
|
||
|
||
`EditorState::new()` / `open()` load the builtins and run `init.lua`.
|
||
**`install_state_dirs()` runs after that** — `src/editor.rs:3838` on
|
||
the local path, `src/daemon.rs:480` on the daemon path. A
|
||
`pmacs.state.read` at Lua module load therefore returns nothing, always.
|
||
|
||
`saveplace` and `recentf` never hit this because **both read lazily**,
|
||
inside functions called long after startup (`recentf.lua:27` in
|
||
`load_list`, `saveplace.lua:33` behind its `available()` guard). **Zoom
|
||
cannot be lazy**: its whole purpose is to apply with no user action,
|
||
before the first frame the GPU paints. It is this project's **first
|
||
eager state consumer**, which is why the seam has to be named rather
|
||
than assumed.
|
||
|
||
### 5.2 The seam
|
||
|
||
Restore must run **after `install_state_dirs()` on both startup paths**.
|
||
Neither existing post-install hook qualifies: `restore_desktop_if_armed`
|
||
is local-only by design (Q#DS9 keeps desktop restore out of the daemon,
|
||
which has a layout per attached frontend and none at construction).
|
||
|
||
**Recommendation: restore inside `install_state_dirs()` itself**, at its
|
||
end. Both paths already call it, exactly once, and it is by definition
|
||
the moment state becomes readable — so the restore cannot be ordered
|
||
wrongly and cannot be forgotten by a future third startup path. The
|
||
alternative, a new call added beside both existing call sites, is two
|
||
places a fourth path can miss.
|
||
|
||
**This must be tested against production ordering, not a direct call.**
|
||
A test that calls the restore helper itself proves nothing about when
|
||
it runs — the same shape as the `prepare_startup` note at
|
||
`src/editor.rs:3820`, where splitting the sequence was what kept a
|
||
deleted call from leaving every direct-call test green while shipping
|
||
nothing. The witness asserts that a state file written before startup
|
||
is reflected in `pmacs.gpu.font()` after the real startup sequence.
|
||
|
||
### 5a. Every size write must preserve the family
|
||
|
||
`pmacs.gpu.set_font` replaces **both** fields unconditionally
|
||
(`src/lua_bindings/mod.rs`: `pref.family = family; pref.size_centi_px =
|
||
size_centi_px;`). So `set_font { size = 18 }` **silently clears a
|
||
configured family** — a user with `family = "Iosevka"` in `init.lua`
|
||
loses it the first time they zoom, and gets it back only by restarting.
|
||
|
||
Zoom must therefore read the current family via `pmacs.gpu.font()` and
|
||
pass it back with every write. **Pinned by a custom-family → zoom →
|
||
`FontFacts` case** asserting the relayed message still carries the
|
||
family. This is a property of the setter's whole-message semantics, not
|
||
a zoom bug, and the test belongs with zoom because zoom is what makes
|
||
it reachable.
|
||
|
||
### 5a1. The persisted format, and what corrupted state does
|
||
|
||
**Format**, following `saveplace` and `recentf` rather than inventing
|
||
one: a single line of plain text holding the size in **centi-pixels**
|
||
as decimal digits, newline-terminated.
|
||
|
||
```
|
||
1800
|
||
```
|
||
|
||
Centi-pixels, not logical px, because that is the wire unit and the
|
||
already-quantized one — writing `18.0` would reintroduce a
|
||
float-parsing step and a second place for rounding to disagree with
|
||
`validate_font_size`.
|
||
|
||
No version prefix. The existing state files carry none, and a single
|
||
integer has no forward-compatibility question that a prefix would
|
||
answer; a future format change can be detected by the strict parse
|
||
below failing, which lands in exactly the same handling as corruption.
|
||
|
||
**Only the size is persisted.** Not the family — that is declared
|
||
config, and §5b's precedence rule applies to size alone.
|
||
|
||
**Validity** is two checks, both required:
|
||
|
||
1. **Strict whole-file parse:** `^(%d+)\n$` against the **entire file
|
||
contents**, not against a line pulled out of it.
|
||
|
||
Revision 2 specified `^(%d+)$`, which **contradicts the
|
||
newline-terminated format two paragraphs above it**: in Lua, `$`
|
||
anchors to end-of-subject, so `("1800\n"):match("^(%d+)$")` is
|
||
`nil` and the parser would reject every file it had itself written.
|
||
|
||
Borrowing `saveplace`'s `gmatch("([^\n]+)")` line iterator fixes
|
||
that but introduces a different hole — it happily returns the
|
||
**first** line of a multi-line file, so trailing garbage would be
|
||
accepted silently. That is tolerable for `recentf`, where each line
|
||
is an independent entry and skipping a bad one loses one path. It is
|
||
not tolerable here, where the file *is* one value and extra content
|
||
means the file is not what we wrote.
|
||
|
||
Whole-file matching closes both, verified against Lua 5.4:
|
||
|
||
| contents | `^(%d+)$` | `^(%d+)\n$` |
|
||
|---|---|---|
|
||
| `"1800\n"` — what we write | **nil** ✗ | `1800` ✓ |
|
||
| `"1800"` — no trailing newline | `1800` | nil ✓ |
|
||
| `"1800\n1900\n"` — multi-line | nil | nil ✓ |
|
||
| `"18.0\n"` — decimal | nil | nil ✓ |
|
||
| `" 1800\n"` — leading space | nil | nil ✓ |
|
||
| `"\n"` — empty | nil | nil ✓ |
|
||
|
||
The multi-line case needs **no separate line-count check**: the
|
||
trailing `$` after `\n` already requires the file to end there.
|
||
2. **Range:** the parsed value is within `600..=7200`. A syntactically
|
||
fine but out-of-range number — a hand-edited file, or a file written
|
||
by a future version with a wider range — must never reach
|
||
`set_font`, which would reject the whole message and leave the user
|
||
with neither the saved zoom nor an explanation.
|
||
|
||
**Malformed, out-of-range, empty, or multi-line state is treated as
|
||
absent.** Startup proceeds exactly as if nothing had been saved: the
|
||
`init.lua` size applies if there is one, otherwise `None`.
|
||
|
||
**Silently, and deliberately so.** It matches the precedent — both
|
||
existing consumers skip unparseable lines without comment — and the
|
||
failure is already self-evident and self-healing: the user sees their
|
||
zoom did not restore, and the next zoom rewrites the file correctly.
|
||
A startup diagnostic for a recoverable, self-announcing condition is
|
||
noise on every launch for a problem that fixes itself on the next
|
||
keystroke.
|
||
|
||
**The corrupt file is not rewritten on read.** It is left alone until
|
||
the next successful zoom overwrites it, so a user who wants to look at
|
||
what went wrong still can. Truncating it at startup would destroy the
|
||
only evidence of a bug we would then have no way to reproduce.
|
||
|
||
### 5b. Reset, clearing, and precedence
|
||
|
||
Revision 1 left all three unspecified. As recommended in review:
|
||
|
||
- **Reset returns size to `None`**, not to `ui.gpu-font-size-base`.
|
||
`None` is the real "frontend's own default" state (Q#F5/Q#TH7); the
|
||
base is only the *origin for the first step*. Resetting to the base
|
||
would send an explicit size that merely happens to equal the default,
|
||
making the untouched state unreachable once a user has ever zoomed —
|
||
and quietly breaking the `(None, None)` identity property.
|
||
- **Reset clears the saved zoom state**, so it does not resurrect on
|
||
the next launch. A reset that leaves the state file behind means
|
||
"reset until restart", which is not what the word says.
|
||
- **Valid saved state wins over an `init.lua` size.** The saved value
|
||
is a later, deliberate user action; the init value is the standing
|
||
default it was chosen against. This is the same precedence
|
||
`saveplace` already applies — a remembered position overrides where
|
||
opening the file would otherwise land.
|
||
- **The family is retained throughout**, per §5a: precedence applies to
|
||
*size only*, and an `init.lua` family is never overridden by restored
|
||
zoom state, because zoom state does not carry one.
|
||
|
||
## 6. Verification
|
||
|
||
- **No binding is installed at all** (Q#Z3 = C), so the witness is that
|
||
`C-+` / `C--` resolve to nothing in a default TUI session — the
|
||
terminal's own zoom is left alone.
|
||
- **Restore runs at the production seam.** Asserted through the real
|
||
startup sequence, not by calling the helper directly: a state file
|
||
written beforehand must be visible in `pmacs.gpu.font()` afterwards,
|
||
on **both** the local and daemon paths.
|
||
- **A zoom preserves a configured family** — custom family → zoom →
|
||
the relayed `FontFacts` still carries it (§5a).
|
||
- **Reset returns `None`, not the base**, and clears the saved state;
|
||
a subsequent launch is back to the frontend's own default.
|
||
- **Saved state beats an `init.lua` size, and never its family.**
|
||
- **Config bounds are enforced at `define`** — a zero, negative, or
|
||
sub-centi-pixel step is refused by the registry, not discovered as a
|
||
zoom that does nothing or runs backwards (§3.1).
|
||
- **A valid newline-terminated file restores.** `"1800\n"` — exactly
|
||
the bytes the writer produces — must round-trip through the real
|
||
startup sequence. This is the case revision 2's own parser would have
|
||
rejected, so it is asserted rather than assumed.
|
||
- **Malformed, out-of-range, empty, multi-line, and
|
||
missing-trailing-newline saved state each behave as absent**, and the
|
||
file is left intact for inspection (§5a1). The multi-line case is
|
||
pinned specifically: it is the one a line-iterator parser would have
|
||
accepted.
|
||
- **The round trip holds only where no clamp occurred**, so the
|
||
out-of-range case asserts the preference is left **unmutated** rather
|
||
than pinned to the boundary.
|
||
- **The round trip holds for a step that is NOT representable** —
|
||
`0.015`, which the registry accepts and which the original `0.37`
|
||
case could not reach. Bitten: with the raw value it lands on `16.01`
|
||
instead of `16.00` (§3.2).
|
||
- **Zoom from the untouched state uses the configured base**, not a
|
||
hardcoded 16.0 — bitten by changing the base and asserting the first
|
||
step follows it.
|
||
- **Clamping at both ends of `6.0..=72.0`** reports rather than
|
||
silently pinning, and a zoom that would leave the range does not
|
||
mutate the preference at all (the `apply_font_facts` whole-message
|
||
rejection precedent).
|
||
- **`n` steps in then `n` steps out returns to the exact starting
|
||
value** — the property that fails under multiplicative stepping and
|
||
the reason Q#Z2 recommends additive.
|
||
- **The untouched path stays byte-identical**: with no zoom performed,
|
||
the existing `(None, None)` identity test must still hold.
|
||
- **Persistence round-trips**, and writes nothing when
|
||
`pmacs.state.available` is false.
|
||
|
||
---
|
||
|
||
## 7. Coherence impact (§20 requirement)
|
||
|
||
**Concern served: `COHERENCE.md` §16, Productize the Semantic Frontend
|
||
Architecture**, which names **frontend-specific typography** in its own
|
||
list of properties the semantic protocol should make visible as a
|
||
product advantage. A GPU frontend that cannot change its own type size
|
||
is that concern under-delivered.
|
||
|
||
**No scorecard change.** Row 16 reads **Strong** (`COHERENCE.md:112`);
|
||
this neither earns nor forfeits it — it uses the mechanism the row
|
||
already credits. Per §25 the row moves only with a PR that changes what
|
||
it asserts.
|
||
|
||
- **Journey steps touched:** none.
|
||
- **Interaction islands added:** none, and **no keybinding either**.
|
||
Revision 1 said "an ordinary global keymap binding, scoped by
|
||
frontend capability"; no such scoping exists (§4), so this stage
|
||
ships commands only, reachable from `M-x` and bindable by the user in
|
||
`init.lua`. Capability-aware binding is deferred to its own framing
|
||
and named in §8.
|
||
- **Config registry:** **yes, two settings** —
|
||
`ui.gpu-font-size-base` and `ui.gpu-zoom-step`. This is the "place
|
||
for GUI settings" the report asked for; it already exists and this
|
||
stage is its next adopter.
|
||
- **Background-work attribution:** none.
|
||
|
||
---
|
||
|
||
## 8. Not in scope
|
||
|
||
Long lines (Stage 3). **Capability-aware keymap resolution** — Q#Z3's
|
||
option (A), which this stage defers rather than half-builds, and which
|
||
should be its own framing because it decides how every future
|
||
frontend-specific binding is expressed. A default keybinding for zoom,
|
||
which waits on it. Per-buffer or per-window zoom — this is one
|
||
global preference, as `font_pref` already is. Family selection, which
|
||
`set_font` already exposes and no one has asked to bind. Any change to
|
||
`FontFacts`, the protocol, or `apply_font_facts` — this stage drives
|
||
the existing mechanism and adds nothing to the wire. Config write-back,
|
||
which does not exist and which Q#Z4 deliberately routes around rather
|
||
than inventing.
|