You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

11 KiB

G-001 Pluggable console backends for non-detectable terminals

Status: active Scope: src/modules/opunk/console-999999.0a1.0.tm, src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/lib/app-punkshell/punkshell.tcl Goal: an interactive REPL can be launched against a non-detectable terminal-like device (ssh channel, tk text widget) via an ::opunk::Console subclass, with no edits to the base class or punk::console. Acceptance: a subshell started with an ssh-channel-backed and a tk-widget-backed ::opunk::Console subclass runs an interactive REPL that reads/writes through that console; size, at_eof, and can_respond are answered by the subclass overrides; the base ::opunk::Console and punk::console module are unchanged.

Context

The Punk REPL talks to its user through a console object. The base ::opunk::Console class (src/modules/opunk/console-999999.0a1.0.tm) represents a console as an in/out channel pair with settled capability facts, and is designed from the outset for subclassing: it is declared -virtual so that "channel environments that respond like terminals but aren't platform consoles can subclass (-extends ::opunk::Console) and override the capability/size methods, with existing holders of console values dispatching correctly." The size method comment names the example explicitly: "Non-channel subclasses (e.g a tk text widget acting as a terminal) override this method entirely."

Standard terminal detection (is_console_or_tty, lines 151-176 of opunk/console-999999.0a1.0.tm) uses twapi::GetConsoleMode on Windows, chan configure -inputmode/-mode cross-platform, and environment hints (MSYSTEM, TERM_PROGRAM) as a fallback for mintty-without-winpty. The class comment already flags the limitation: this is a heuristic that trades false positives (piped-but-open input treated as a terminal) for false negatives, and only works for devices that either expose channel mode flags or set those environment variables.

Terminal-like devices that don't expose any of those signals cannot be auto-detected. Two concrete cases motivate this goal:

  1. SSH-channel consoles. A Punk subshell reachable over an ssh connection presents a socket channel, not a platform tty. There is no -inputmode/-mode, no twapi handle, and no inherited MSYSTEM/TERM_PROGRAM. Standard detection returns 0, so ANSI cursor-report queries are suppressed and the REPL degrades to a default 80x24 with no capability settling.
  2. Tk-widget consoles. A Tk text widget acting as a terminal is not a channel at all. The base size/at_eof/is_console_or_tty/can_respond methods are all channel-shaped; the class comment concedes these subclasses "override this method entirely - none of the channel/provider logic is imposed on them."

The seam for both already exists: punk::console::console_spec_resolve accepts an ::opunk::Console object value as a console spec, so a REPL can in principle be pointed at any subclass instance. What is missing is (a) the concrete subclasses and (b) the launch-time wiring that selects a non-default console for a given REPL/subshell.

Pluggable hooks already in place that backends may use or ignore:

  • ::opunk::console::waiting_chunks_arrayvar — redirects probe-consumed bytes to a cooperative store so active readers see them. Channel-backed subclasses reusing the base at_eof probe benefit from this; non-channel subclasses ignore it.
  • ::opunk::console::size_query_provider — a command prefix registered by punk::console to give the base class access to ANSI cursor-report mechanisms. Channel-backed subclasses gain this for free when the integrating layer registers it; non-channel subclasses override size and never touch it.

Approach

Add two reference ::opunk::Console subclasses, each in its own module under src/modules/opunk/ following the -999999.0a1.0.tm naming convention:

  1. opunk::console::ssh — wraps an ssh socket channel pair as a console. Overrides is_console_or_tty to return 1 (the backend is constructed knowing the channel is a terminal), can_respond to settle to 1, at_eof to check the channel's eof without the base class's pipe-probe (the channel is a known terminal, not a probed pipe), and size to either delegate to the registered size_query_provider (ANSI cursor report over the ssh channel) or query a backend-specific size if the ssh transport exposes one. Reuses waiting_chunks_arrayvar redirection if the probe path is exercised.
  2. opunk::console::tk — wraps a Tk text widget as a console. Overrides size (widget dimensions), at_eof (a backend-specific eof marker, not a channel eof), is_console_or_tty (returns 1), and can_respond (returns 1). Does not touch waiting_chunks_arrayvar or size_query_provider — non-channel subclass.

Then add launch-time console selection so a REPL or subshell can be started against a non-default console without hardcoding stdin/stdout. The selection seam lives at REPL init (repl::init in src/modules/punk/repl-999999.0a1.0.tm, lines ~3109-3128) where the "default" anchored console instance is currently constructed. The -console convention already being migrated through punk::console (per src/modules/punk/AGENTS.md "Work Guidance") is the natural extension point: accept a console spec (channel pair, instance name, or object value) at repl::init and resolve it via punk::console::console_spec_resolve, settling the resulting object's capability as appropriate for the backend.

Alternatives considered

  • Auto-detect ssh/tk consoles in is_console_or_tty. Rejected: the class comment already explains detection is deliberately heuristic because non-detectable devices don't expose the signals detection relies on. Adding more env-var hints or channel-shape sniffing for ssh/tk would compound the false-positive risk the comment warns about. Subclassing with explicit construction-time capability is what the -virtual design is for.
  • Put the ssh/tk subclasses inside punk::console rather than opunk. Rejected: punk::console is the integrating layer that registers providers with the base class; putting backends there would invert the dependency direction the class comment insists on ("the class depending on punk::console" is explicitly avoided). Backends belong in opunk alongside the base class, or in their own namespaces.
  • Make size pluggable per-instance instead of per-class. Rejected: the size_query_provider hook already covers the per-instance pluggable case for channel-based consoles. Non-channel subclasses need full method override, which is the -virtual dispatch path. A second per-instance hook would duplicate the existing mechanism.
  • Detect tk widgets by checking for a Tk window path. Rejected: a Tk text widget is not a channel and shouldn't be forced through channel detection. The subclass is constructed knowing what it wraps; detection is the wrong tool.

Notes

  • The -virtual voo dispatch means existing holders of an opunk::Console value (e.g. punk::console::console_spec_resolve callers, REPL init) dispatch to the subclass methods automatically once the value carries the subclass namespace tag. No call-site changes should be needed beyond passing the new console spec at launch.
  • punk::console::ensure_object_integration registers the size provider and redirects the probe-byte store. Channel-backed ssh subclass inherits this; tk subclass ignores it. Confirm during implementation that the tk subclass's size override does not accidentally fall through to the provider path.
  • The base class at_eof does a non-blocking 1-byte probe on pipe-like channels and parks the byte in waiting_chunks_waiting. The ssh subclass should override at_eof to use chan eof on the ssh channel directly — probing a socket would consume a byte the protocol layer may need.
  • The "first subshell asymmetry" TODO at repl-999999.0a1.0.tm:3130-3132 is G-002's concern, not G-001's, but G-001's launch-time console selection is a prerequisite for G-002's "target a named console" acceptance criterion. Sequence G-001 before G-002.
  • No persisted prior chat on this topic was found in project sessions; the motivation comes from the existing class design comments and the user's stated intent.

Progress (activated 2026-07-11)

Additional context since authoring: the G-044-detail repl characterization work (2026-07-11) identified this goal as the ENABLING refactor for repl/editbuf unit testing - class_editbuf is console-coupled and needs a deterministic console double. A third reference subclass (the test double) was therefore added ahead of ssh/tk in the build order.

Landed (increment 1)

Three backend modules under src/modules/opunk/console/, base ::opunk::Console and punk::console UNCHANGED (verified - no diffs):

  • opunk::console::test / ::opunk::TestConsole - deterministic channel-pair double: fixed size (-columns/-rows at construction, stored in the inherited default-size field), is_console_or_tty/can_respond 1 (settled wins), at_eof = plain chan eof with NO probe (a pending byte is never consumed - pinned by test).
  • opunk::console::ssh / ::opunk::SshConsole - per the Approach: construction-time capability, chan-eof without probe, size via the registered size_query_provider. Flagship verification: with a scripted "remote terminal" answering CSI 6n over a socket pair, ::opunk::Console::size on the subclass value resolves 100x30 through punk::console's ANSI cursor-report provider QUERYING OVER THE SOCKET - the entire value proposition proven without a real ssh connection.
  • opunk::console::tk / ::opunk::TkConsole - widget path in the in/out slots, terminal_class tk-text, size from widget char dims, at_eof via backend marker (::opunk::console::tk::set_eof) or widget destruction; no Tk require at module load. Verified live under punk91 src (the tk-loading experiment kit): size/eof flag/clear/destroy all correct.

voo -extends findings recorded in src/modules/opunk/AGENTS.md: child classes inherit public accessors + field INDEX variables but not parent-private my.* accessors - subclass constructors initialise inherited private fields via the index variables; method bodies use the parent's public accessor methods.

Tests: src/tests/modules/opunk/console/testsuites/console/backends.test (8 tests; 7 green + tk env-gated on tcl 9.0.3 and 8.7; tk case verified under punk91).

Remaining for acceptance

  • repl launch wiring: -console at repl::init (resolve via console_spec_resolve), repl::start default input from the console, and OUTPUT parameterization - rputs/ doprompt (and the code-interp channel shares) are stdout/stderr-hardwired today; routing them via repl-level channel variables derived from the selected console is the remaining engineering (repl.tm and app-punkshell are in scope for edits).
  • Interactive-repl-through-backend verification for the ssh and tk cases (the acceptance sentence). The tk case pairs naturally with the punkwish91 textwidget virtual-console exploration (mapvfs notes); the ssh case can be simulated with the scripted-remote-terminal socket technique before a real sshd session.