6.1 KiB
G-008 Scoped console state for same-console subshells (activatable state sets)
Status: proposed Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm Goal: a subshell on the shared console can opt into scoped console state - its terminal mutations (tabstops, modes, cursor style, palette, title) are captured as an activatable per-subshell state set and the prior state is re-established on quit or switch-away, with today's shared/persistent behaviour remaining the default. Acceptance: with a scoped subshell: tabstops, at least one DEC/ANSI mode and at least one palette entry changed inside the subshell are restored for the parent on quit (verified by terminal query where queryable) and the facts store matches the terminal; the default (unscoped) launch behaviour is unchanged; activation is set-based, proven by applying state set A, activating set B, then re-activating A and observing A's state; irreversible outputs (cleared screen/scrollback, emitted text) are documented as out of scope.
Context
Motivating observation: in a punk repl, subshell punk then punk::console::set_tabstop_width 20
then quit leaves the parent repl with 20-wide tabstops. This is only partly a data problem -
get_tabstops queries the terminal (DECTABSR), and the terminal genuinely was reprogrammed by the
subshell's emission. No amount of per-interp data scoping restores the parent's tabstops; restore
is inescapably an active re-emission problem: something must know the prior state and write
sequences to reinstate it. The per-console facts store (punk::console 0.3.0) is deliberately keyed
per console, not per scope, because facts mirror terminal reality - rolling back a fact without
re-emitting would make the store lie.
Two future requirements shape the design (see G-009, G-010):
- Themed subshells will declare terminal effects (e.g. palette changes from poshinfo schemes) that must be applied on entry and removed on exit alongside journaled ad-hoc mutations.
- Subshell switching will be tree-shaped and non-LIFO (grandchild to grandparent, cross-branch), so a pure undo-stack model dies at the first cross-branch jump. State must be organised as activatable per-subshell state sets: activating a subshell (re-)applies its set; quitting activates the parent's set. LIFO restore-on-quit is then just the degenerate case.
The punk::console 0.2.0-0.5.0 migration and the G-007 broker provide the hook sites: every
state-mutating operation funnels through a small set of choke points (the emit helpers, the mode
setters, console_fact_set), including mutations originating in code interps once G-007 routes
them through the owning context.
Approach
- Per-subshell console-state set = inherited baseline + declared entries (themes, G-009) + journaled runtime mutations. Journal-on-write at the choke points: at a subshell's first mutation of a given aspect, record the prior value (from the facts store, or one terminal query where needed), then record the new value in the subshell's set.
- Activation, not undo. Activating a state set (re-)emits its entries (idempotent, or as a
diff against the currently-applied set). Deactivation is implicit in activating another set.
quit= activate the parent's set. This is what makes G-010's arbitrary switching possible. - Opt-in per launch (e.g.
subshell punk -consolestate scoped|shared), defaulting to today's shared/persistent behaviour - persisting is sometimes exactly what the user wants when configuring their terminal from a subshell. - Restorable classification (each with its capture/re-emission mechanism):
- tabstops - DECTABSR query / clear-all + HTS per recorded column
- DEC and ANSI modes - DECRQM query / DECSET-DECRST, SM-RM
- cursor style - DECRQSS(DECSCUSR) / DECSCUSR
- colour palette and default fg/bg - OSC 4/10/11 (query support varies; journal what punk set; OSC 104/110/111 reset only reaches terminal defaults, which may differ from the parent's own customised baseline - prefer journaled priors)
- title - journal only (queryability unreliable); mouse/paste reporting, bracketed paste, vt52 mode, raw mode - journal punk-side state Irreversible and documented out of scope: screen contents, scrollback, emitted output.
- Facts consistency: state-set activation updates the per-console facts store in the same
step as re-emission, so
console_fact_getkeeps matching terminal reality.
Alternatives considered
- LIFO frame/undo stack - rejected as the core model: G-010's cross-branch switching cannot be expressed as pops. Retained conceptually as the behaviour observed in the simple launch/quit case.
- Snapshot-everything on subshell entry - rejected as primary mechanism: depends on the terminal answering every query (DECTABSR/DECRQSS/OSC-with-? support varies widely); journal-on-write is exact, cheap, and only touches aspects actually mutated. Entry snapshot remains useful for the root baseline where queryable.
- Scope the facts store per subshell without re-emission - rejected: the terminal is the
ground truth; data-only scoping makes the facts store lie (the motivating
get_tabstopsobservation would be unchanged). - Alt-screen as the scoping mechanism - insufficient: alt screen scopes display content but not tabstops, palette, modes or cursor style.
Notes
- Sequence after G-007 (achieved 2026-07-05 - prerequisite in place): the journal hooks belong at the same choke points the broker instruments, and journaling must catch mutations originating in code interps - exactly what the broker funnels. Composes with G-002: a subshell targeting a different console needs no scoping (its terminal is its own); this goal covers the common shared-console case.
- G-009 consumes this by declaring theme effects into the launch-time state set; G-010 consumes the activation primitive for switching. Neither is required for this goal's acceptance.
- The A/B/A acceptance clause exists precisely to force the set-based design - a pure stack implementation passes restore-on-quit but fails A/B/A.