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.
 
 
 
 
 
 

8.0 KiB

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.