# G-001 Pluggable console backends for non-detectable terminals Status: proposed 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 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.