Repository navigation
Implement OSC 9;4 per-pane progress state and UI #507
Description
Activity
Recon findings and implementation map
Where things stand today
The #502 fix (
b47fde7b,183a2189) recognises valid progress reports and drops them entirely —handle_osc_notify_9atfreminal-terminal-emulator/src/ansi_components/osc_notify.rs:40-84returns without emitting anyTerminalOutputwhenis_conemu_progress_report(osc_notify.rs:86-110) matches. Thestateandprogressdigits are validated and then discarded. Nothing reachesTerminalHandler,TerminalSnapshot, or the GUI.The transport pattern to use (and the one to avoid)
The OSC 9 / OSC 777 notification path is a fire-once event queue:
WindowManipulation::Notificationpushed ontohandler.window_commands, drained once perbuild_snapshot()cycle (freminal-terminal-emulator/src/state/internal.rs:505-506), consumed by the GUI pump atfreminal/src/gui/pty.rs:636. That is the wrong shape here — progress is level-triggered state that must be readable on every subsequent frame until it changes.The correct precedent is
cursor_color_override/pointer_shape(OSC 12 / OSC 22):Step Location Field on TerminalHandlerterminal_handler/mod.rs:254,259(init:430-431)Mutated in handle_oscterminal_handler/osc.rs:150-152const fnaccessorterminal_handler/mod.rs:489-503Cleared by RIS terminal_handler/mod.rs:512-538Snapshot field snapshot.rs:328-338, defaults:417-418Read in build_snapshot()interface.rs:963-964GUI reads it directly off pane.arc_swap.load()Pane exit then needs no explicit teardown:
PaneTree::close(freminal/src/gui/panes/mod.rs:1209) andprocess_dead_panes(freminal/src/gui/frame_drain.rs:220+) drop the wholePane, which owns theArcSwap. This holds only as long as the state is not also cached GUI-side outsidePane's lifetime.Parser change is not cosmetic
Per the ConeEmu/ghostty spec, states 2 (Error) and 4 (Paused) keep the previous numeric value when
vis omitted. So parsing can no longer be a stateless free function —handle_osc_notify_9(osc.rs:53) currently takes onlyraw_params/seq_trace/outputand has no access to handler state. Its signature and call site have to change.The three existing tests at
osc_notify.rs:320-347assert "output is empty"; they become assertions about a typed payload. That is a deliberate behaviour change to existing tests, not an addition.Staleness timeout
Direct precedent exists:
SYNC_UPDATES_TIMEOUT_MS(interface.rs:135) +apply_sync_updates_timeout()(interface.rs:1034-1057), driven off anOption<Instant>checked on everybuild_snapshot()(interface.rs:801).Decision: hardcode 15s as a
const, matching ghostty; no config knob. The clock tracks "time since last progress report", not "time since progress became active", so every OSC 9;4 update resets it.Rendering
Decision: pane-local bar (ghostty's own presentation), not a tab-bar badge.
Rationale beyond preference:
ChromeDamage(freminal-windowing/src/lib.rs:174-183) is a binaryChanged/Unchangedwith no region concept, so any tab-bar indicator change forces the whole frame toFullviacompose_with_chrome_damage(freminal/src/gui/frame_damage.rs:220). For an actively-ticking build that is a full repaint per update. A pane-local bar participates inPaneFrameDamage::Regionand stays bounded.The implementation site is the command-block gutter's painter pass,
freminal/src/gui/terminal/widget.rs:3990-4055— the existing precedent for a per-pane overlay painted inside the pane's own damage accounting. Semantic colour followsChromeRole/gutter_color_for(freminal-common/src/themes.rs:118-160).Decision: aggregation is worst-state-wins — Error > Paused > Indeterminate > Normal > Inactive, percentage taken from the winning pane. Recorded now because it is needed the moment anything tab-level is added later, even though the pane-local decision defers the need.
Config
Decision: gate display behind a config option. Protocol recognition stays unconditional (as #502 made it); only rendering is gated. Modelled on
CommandBlocksConfig/GutterPosition(freminal-common/src/config.rs:797-894). Wiring must follow thefreminal-config-optionschecklist — theConfigPartial/apply_partiallegs are the ones that get forgotten and silently make the option a no-op.Open question: DECSTR does not exist
This issue asks to clear progress state on "terminal reset, RIS and DECSTR". DECSTR (
CSI ! p) is not implemented anywhere in freminal — noDECSTR,SoftReset, orsoft_resetsymbol exists, andfull_reset()documents itself as RIS-only.So this requirement cannot be satisfied as written. Implementing DECSTR means implementing a full soft-reset with its own specified list of affected modes, which is a distinct escape-sequence feature that deserves its own issue and its own test matrix rather than riding along on a progress indicator. Recommend: clear on RIS here, and file DECSTR separately. Flagging rather than silently choosing.
Damage / typing constraints
- The state must be a named enum, not a bool pair — it crosses the snapshot boundary (
freminal-state-representation).TerminalSnapshotalready carries astruct_excessive_boolsallow atsnapshot.rs:52; do not add to it. - An animating indeterminate indicator needs a bounded
Regionrect per tick, following thehas_blinking_textprecedent (snapshot.rs:128-133) that drives repaint scheduling only while something is actually animating.
Docs and benches
Mandatory dual-doc update:
Documents/ESCAPE_SEQUENCE_COVERAGE.md:267and:432,Documents/ESCAPE_SEQUENCE_GAPS.md:141and:281, plus both "Last updated" headers.bench_parse_osc9(freminal-terminal-emulator/benches/buffer_benches.rs:211-238) already includes a\x1b]9;4;1;50\x1b\\case, so before/after parser numbers are available without writing a new benchmark.- The state must be a named enum, not a bool pair — it crosses the snapshot boundary (
Context
Issue #502 exposed Neovim emitting the ConEmu progress protocol around file writes:
The #502 compatibility fix recognizes valid progress reports and silently consumes them so they are not routed as OSC 9 desktop notifications. Freminal still does not expose the reported progress state visually.
Protocol reference: https://ghostty.org/docs/vt/osc/conemu
Required behavior
OSC 9;4;<state>[;<progress>]into typed state rather than notification text.state=0, terminal reset, and pane exit.