Browse Source

G-001 increment 1: pluggable ::opunk::Console backends - test double, ssh-channel, tk-widget (punkshell 0.9.0)

- three new backend modules under src/modules/opunk/console/, base ::opunk::Console and punk::console UNCHANGED (the G-001 acceptance constraint - verified diff-clean):
  * opunk::console::test / ::opunk::TestConsole - deterministic channel-pair test double: fixed size via -columns/-rows, is_console_or_tty/can_respond 1 (settled value wins), at_eof = plain chan eof with NO probe (pending bytes never consumed - pinned). The console seam identified by the G-044 repl characterization work
  * opunk::console::ssh / ::opunk::SshConsole - socket-carried terminal sessions: construction-time capability (detection is the wrong tool - per goal detail), chan-eof without byte-consuming probes, size via the registered ANSI size-query provider over the connection. Flagship test: a scripted remote terminal answers CSI 6n over a socket pair and size resolves 100x30 through punk::console machinery querying the socket
  * opunk::console::tk / ::opunk::TkConsole - Tk text widget as terminal: widget path in the inherited in/out slots (documented non-channel reuse), 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)
- voo -extends subclassing pattern recorded in opunk/AGENTS.md: children inherit public accessors + field INDEX variables (not parent-private my.* accessors) - constructors initialise inherited private fields via index vars, method bodies use parent public accessor methods; virtual dispatch via the slot-0 tag needs no base changes
- tests: modules/opunk/console backends.test (8 tests: dispatch, spec_resolve acceptance of subclass values, probe-free eof, settled-capability precedence, ssh capability/eof/size-over-socket/settled-0-no-emission, tk gated behind env PUNK_TEST_TK=1) - 7 green + tk skip on tcl 9.0.3 and 8.7; tk case verified standalone under punk91
- goal detail records progress + remaining work (repl -console launch wiring, output-channel parameterization, interactive acceptance verification); goal stays active

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
aec77ca0d1
  1. 4
      CHANGELOG.md
  2. 2
      GOALS.md
  3. 48
      goals/G-001-pluggable-console-backends.md
  4. 2
      punkproject.toml
  5. 2
      src/modules/opunk/AGENTS.md
  6. 111
      src/modules/opunk/console/ssh-999999.0a1.0.tm
  7. 4
      src/modules/opunk/console/ssh-buildversion.txt
  8. 120
      src/modules/opunk/console/test-999999.0a1.0.tm
  9. 4
      src/modules/opunk/console/test-buildversion.txt
  10. 154
      src/modules/opunk/console/tk-999999.0a1.0.tm
  11. 4
      src/modules/opunk/console/tk-buildversion.txt
  12. 1
      src/tests/modules/AGENTS.md
  13. 224
      src/tests/modules/opunk/console/testsuites/console/backends.test

4
CHANGELOG.md

@ -5,6 +5,10 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy. "Project Versioning" section for the bump policy.
## [0.9.0] - 2026-07-11
- G-001 (in progress) increment 1: three pluggable `::opunk::Console` backend modules, with the base class and punk::console untouched — `opunk::console::test` (`::opunk::TestConsole`: deterministic channel-pair test double with fixed size and probe-free eof — the console seam the repl/editbuf characterization work needs), `opunk::console::ssh` (`::opunk::SshConsole`: socket-carried terminal sessions — construction-time capability, chan-eof without byte-consuming probes, and size resolved by punk::console's ANSI cursor-report provider *querying over the connection*, proven against a scripted remote terminal answering `CSI 6n` over a socket pair), and `opunk::console::tk` (`::opunk::TkConsole`: a Tk text widget as terminal — widget char dimensions as size, backend eof marker via `opunk::console::tk::set_eof`, verified live under the tk-capable punk91 kit). Subclass values dispatch through existing base-class holders and `console_spec_resolve` virtually. Remaining for the goal: repl launch-time console selection and output-channel parameterization.
## [0.8.2] - 2026-07-11 ## [0.8.2] - 2026-07-11
- G-054 achieved: tclcore moduledoc 0.2.0 — the `string is` class choices shown by `i string is` (and the per-class docids like `i string is digit`) are now harvested from the running interpreter at define time instead of a hand-maintained list, so the documented/parsed class set always matches what the interpreter accepts (previously the static Tcl 9.0 list wrongly accepted `dict` under 8.6 and rejected 8.7's `unicode`). Version notes render on classes that differ across releases; unrecognized future classes get a generic label instead of vanishing. Behavioural parity pinned by `tclcoreparity.test` with expectations derived from the live interpreter — green on Tcl 8.6.13, 8.7a6 and 9.0.3. - G-054 achieved: tclcore moduledoc 0.2.0 — the `string is` class choices shown by `i string is` (and the per-class docids like `i string is digit`) are now harvested from the running interpreter at define time instead of a hand-maintained list, so the documented/parsed class set always matches what the interpreter accepts (previously the static Tcl 9.0 list wrongly accepted `dict` under 8.6 and rejected 8.7's `unicode`). Version notes render on classes that differ across releases; unrecognized future classes get a generic label instead of vanishing. Behavioural parity pinned by `tclcoreparity.test` with expectations derived from the live interpreter — green on Tcl 8.6.13, 8.7a6 and 9.0.3.

2
GOALS.md

@ -53,7 +53,7 @@ When the user asks to "write a goal for X" or "help me draft a goal for Y", do t
<!-- Append new goals below using the format above. Keep the list ordered by G-<id>. --> <!-- Append new goals below using the format above. Keep the list ordered by G-<id>. -->
### G-001 [proposed] Pluggable console backends for non-detectable terminals ### G-001 [active] Pluggable console backends for non-detectable terminals
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 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
Detail: goals/G-001-pluggable-console-backends.md Detail: goals/G-001-pluggable-console-backends.md
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. 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.

48
goals/G-001-pluggable-console-backends.md

@ -44,3 +44,51 @@ Then add launch-time console selection so a REPL or subshell can be started agai
- 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 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. - 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. - 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 <spec> 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.

2
punkproject.toml

@ -1,3 +1,3 @@
[project] [project]
name = "punkshell" name = "punkshell"
version = "0.8.2" version = "0.9.0"

2
src/modules/opunk/AGENTS.md

@ -20,6 +20,8 @@ Modules under the `opunk::*` namespace explore value-based class implementations
- Query-based mechanisms enter the class the same way: `::opunk::console::size_query_provider` is a command prefix (invoked with the console object) the base `size` method consults after the -winsize fast path and capability gating; punk::console registers `console_size_provider` via `ensure_object_integration`. Non-channel subclasses override the relevant `-virtual` methods and never touch providers. - Query-based mechanisms enter the class the same way: `::opunk::console::size_query_provider` is a command prefix (invoked with the console object) the base `size` method consults after the -winsize fast path and capability gating; punk::console registers `console_size_provider` via `ensure_object_integration`. Non-channel subclasses override the relevant `-virtual` methods and never touch providers.
- Anchor lifecycle is observable the same way: `::opunk::console::lifecycle_callback` is a command prefix invoked as `{*}$cb created|forgotten <name> {in out}` after `create` anchors / `forget` releases an instance; punk::console registers its handler via `ensure_object_integration` to maintain the G-007 console ownership registry. Empty by default; callback errors propagate to the create/forget caller. - Anchor lifecycle is observable the same way: `::opunk::console::lifecycle_callback` is a command prefix invoked as `{*}$cb created|forgotten <name> {in out}` after `create` anchors / `forget` releases an instance; punk::console registers its handler via `ensure_object_integration` to maintain the G-007 console ownership registry. Empty by default; callback errors propagate to the create/forget caller.
- Method-entry validity guards use `[llength $this] < N` (not `!= N`) so field-adding subclasses keep working. - Method-entry validity guards use `[llength $this] < N` (not `!= N`) so field-adding subclasses keep working.
- Subclassing (`voo::class <Child> -extends ::opunk::Console {...}`): the child inherits public accessors and the field INDEX variables, but not the parent's private `my.*` accessors - a subclass CONSTRUCTOR initialises inherited private fields via the index variables (`variable o_in` then `lset obj $o_in $value` - the sanctioned exception to the avoid-`variable o_<field>` rule, which still applies in method bodies where the parent's public accessor METHODS should be used instead, e.g `[::opunk::Console::in $this]`). Overridden `-virtual` methods are plain `method` declarations in the child; base-class calls dispatch to them via the slot-0 tag.
- G-001 backend subclasses (console/ subfolder): `opunk::console::test` (`::opunk::TestConsole` - deterministic channel-pair test double: fixed size, no probes; the console seam for repl/editbuf characterization), `opunk::console::ssh` (`::opunk::SshConsole` - socket-carried terminal sessions: construction-time capability, chan-eof without probe, size via the ANSI size-query provider over the connection), `opunk::console::tk` (`::opunk::TkConsole` - Tk text widget as terminal: widget path in the in/out slots, size from widget char dims, backend eof marker via `::opunk::console::tk::set_eof`; no Tk package require at load). Tests: `src/tests/modules/opunk/console/testsuites/console/backends.test` (tk case gated behind env `PUNK_TEST_TK=1` - loading Tk into the shared runtests testinterp has event-loop side effects).
- Module filenames use the `-999999.0a1.0.tm` magic suffix with `<modulename>-buildversion.txt` per parent conventions. - Module filenames use the `-999999.0a1.0.tm` magic suffix with `<modulename>-buildversion.txt` per parent conventions.
- `opunk::console` (with its dependency `voo`) is included in bootsupport via `src/bootsupport/modules/include_modules.config` for eventual boot/repl-startup console objects; after releasing a new version, rebuild with `make.tcl modules` then refresh the snapshot with `make.tcl bootsupport`. - `opunk::console` (with its dependency `voo`) is included in bootsupport via `src/bootsupport/modules/include_modules.config` for eventual boot/repl-startup console objects; after releasing a new version, rebuild with `make.tcl modules` then refresh the snapshot with `make.tcl bootsupport`.

111
src/modules/opunk/console/ssh-999999.0a1.0.tm

@ -0,0 +1,111 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console::ssh 999999.0a1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
package require Tcl 8.6-
package require voo
package require opunk::console
#opunk::SshConsole - an ::opunk::Console backend for terminal sessions carried over a
#socket-like channel pair (G-001) - the ssh-channel case: a remote user's terminal is on
#the other end of the connection, but the local channel is a plain socket exposing none
#of the signals terminal detection relies on (-inputmode/-mode, twapi handles, env hints).
#
#The backend is CONSTRUCTED knowing the far end is a terminal, so:
# - is_console_or_tty: always 1 (explicit construction-time knowledge, not detection)
# - can_respond: settled value if set, else 1 (the remote terminal answers ANSI
# queries carried over the connection)
# - at_eof: plain [chan eof] on the channel - deliberately NO base-class
# pipe-probe: probing a socket would consume a byte the protocol
# layer/reader needs (see goals/G-001 detail)
# - size: the registered ::opunk::console::size_query_provider when
# available (punk::console's ANSI cursor-report mechanisms operate
# over the channel pair like any other console), else the
# configured default size. No -winsize attempt (sockets have none).
#
#No edits to the base ::opunk::Console or punk::console are needed (G-001 acceptance).
voo::class ::opunk::SshConsole -extends ::opunk::Console {
public {
method is_console_or_tty {} {
return 1
}
method can_respond {} {
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return 1
}
method at_eof {} {
set in [::opunk::Console::in $this]
if {[catch {chan eof $in} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
return $is_eof
}
method size {} {
if {![can_respond $this]} {
return [::opunk::Console::default_size $this]
}
if {$::opunk::console::size_query_provider ne ""} {
if {![catch {{*}$::opunk::console::size_query_provider $this} sized]} {
if {[dict size $sized] && [string is integer -strict [dict get $sized columns]] && [string is integer -strict [dict get $sized rows]]} {
return $sized
}
}
}
return [::opunk::Console::default_size $this]
}
}
constructor {in out} {
#in/out: the channel pair of the connection (a single socket channel may be
#passed for both). Parent fields are private - set via imported index variables.
variable o_in
variable o_out
set obj [new()]
lset obj $o_in $in
lset obj $o_out $out
return $obj
}
}
namespace eval ::opunk::SshConsole {
namespace ensemble create
}
tcl::namespace::eval ::opunk::console::ssh {
tcl::namespace::export {[a-z]*}
variable PUNKARGS
lappend PUNKARGS [list {
@id -id "(package)opunk::console::ssh"
@package -name "opunk::console::ssh" -help\
"voo class ::opunk::SshConsole - an ::opunk::Console backend for terminal sessions
carried over socket-like channels (e.g an ssh connection): constructed knowing the
far end is a terminal, eof via chan eof (no byte-consuming probe), size via the
registered ANSI size-query provider over the connection (G-001)."
}]
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::opunk::console::ssh ::opunk::SshConsole
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console::ssh [tcl::namespace::eval ::opunk::console::ssh {
variable pkg opunk::console::ssh
variable version
set version 999999.0a1.0
}]
return

4
src/modules/opunk/console/ssh-buildversion.txt

@ -0,0 +1,4 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.
#0.1.0 - G-001: initial ::opunk::Console backend subclass

120
src/modules/opunk/console/test-999999.0a1.0.tm

@ -0,0 +1,120 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console::test 999999.0a1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
package require Tcl 8.6-
package require voo
package require opunk::console
#opunk::TestConsole - a scripted/deterministic ::opunk::Console backend (G-001).
#
#The first client of the pluggable-backend seam and the console TEST DOUBLE the repl
#characterization work needs (see goals/G-044 detail - class_editbuf and raw-mode
#behaviours are console-coupled and cannot be unit-tested against a live terminal).
#
#Wraps a caller-supplied channel pair (typically [chan pipe] ends or reflected channels
#owned by a test harness) and answers the capability/size questions DETERMINISTICALLY:
# - is_console_or_tty: always 1 (constructed knowing it plays the terminal role)
# - can_respond: settled value if set, else 1
# - at_eof: plain [chan eof] on the input channel - NEVER probes (a probe
# would consume a byte the test harness scripted)
# - size: the configured size - no -winsize, no query emission, no
# size_query_provider consultation. Set at construction
# (-columns/-rows) and stored in the inherited o_default_size.
#
#No edits to the base ::opunk::Console or punk::console are needed (G-001 acceptance):
#values carry this class's namespace tag at slot 0 and existing holders (e.g
#punk::console::console_spec_resolve callers) dispatch to these overrides virtually.
voo::class ::opunk::TestConsole -extends ::opunk::Console {
public {
method is_console_or_tty {} {
return 1
}
method can_respond {} {
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return 1
}
method at_eof {} {
set in [::opunk::Console::in $this]
if {[catch {chan eof $in} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
return $is_eof
}
method size {} {
return [::opunk::Console::default_size $this]
}
}
constructor {in out args} {
#args: optional -columns <int> -rows <int> (default 80x24 from the base field).
#Parent fields are private, so their imported INDEX variables are used to set
#slots on the child-tagged default value (the voo -extends pattern for
#initialising inherited private fields).
variable o_in
variable o_out
variable o_default_size
set obj [new()]
lset obj $o_in $in
lset obj $o_out $out
set size [lindex $obj $o_default_size]
foreach {k v} $args {
switch -- $k {
-columns {
dict set size columns $v
}
-rows {
dict set size rows $v
}
default {
error "opunk::TestConsole::new unknown option '$k'. Known options: -columns -rows"
}
}
}
lset obj $o_default_size $size
return $obj
}
}
namespace eval ::opunk::TestConsole {
namespace ensemble create
}
tcl::namespace::eval ::opunk::console::test {
tcl::namespace::export {[a-z]*}
variable PUNKARGS
lappend PUNKARGS [list {
@id -id "(package)opunk::console::test"
@package -name "opunk::console::test" -help\
"voo class ::opunk::TestConsole - a scripted/deterministic ::opunk::Console backend
wrapping a caller-supplied channel pair (e.g chan pipe ends) with fixed capability
answers and a configured size. The console test double for harness-driven repl and
editbuf testing (G-001)."
}]
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::opunk::console::test ::opunk::TestConsole
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console::test [tcl::namespace::eval ::opunk::console::test {
variable pkg opunk::console::test
variable version
set version 999999.0a1.0
}]
return

4
src/modules/opunk/console/test-buildversion.txt

@ -0,0 +1,4 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.
#0.1.0 - G-001: initial ::opunk::Console backend subclass

154
src/modules/opunk/console/tk-999999.0a1.0.tm

@ -0,0 +1,154 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
#
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console::tk 999999.0a1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
package require Tcl 8.6-
package require voo
package require opunk::console
#NOTE: deliberately NO 'package require Tk' here - the class definition is loadable
#without Tk; construction errors usefully if the widget doesn't exist. This keeps the
#backend requirable in non-GUI processes (introspection, docs) and leaves Tk loading
#policy to the application (punk runtimes load Tk as an extension - punkbin convention).
#opunk::TkConsole - an ::opunk::Console backend for a Tk text widget acting as a
#terminal (G-001). A widget is not a channel at all, so every channel-shaped base
#method is overridden - none of the channel/provider logic applies (the class comment
#in opunk/console anticipates exactly this subclass).
#
# - o_in/o_out carry the WIDGET PATH (documented reuse of the inherited channel slots -
# holders calling in/out/channels get the widget path; a non-channel consumer must
# know its backend, which is what is_console_or_tty/terminal_class are for)
# - is_console_or_tty: always 1 (constructed as a terminal)
# - can_respond: settled value if set, else 1 (the widget side can answer -
# whatever integrating layer renders to the widget also responds)
# - at_eof: a backend eof MARKER, not a channel eof: 1 when the widget no
# longer exists (winfo exists - requires Tk loaded) or when the
# integrating layer has flagged eof via ::opunk::console::tk::set_eof
# (e.g the user closed the console frame but the widget remains)
# - size: the widget's character dimensions ([$w cget -width/-height] -
# the text widget's requested size in chars), falling back to the
# inherited default size if the widget can't be queried.
#
#No edits to the base ::opunk::Console or punk::console are needed (G-001 acceptance).
tcl::namespace::eval ::opunk::console::tk {
#backend eof markers keyed by widget path - set via set_eof, consulted by at_eof
variable eof_flags
if {![array exists eof_flags]} {
array set eof_flags {}
}
}
voo::class ::opunk::TkConsole -extends ::opunk::Console {
public {
method is_console_or_tty {} {
return 1
}
method can_respond {} {
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return 1
}
method at_eof {} {
set w [::opunk::Console::in $this]
if {[info exists ::opunk::console::tk::eof_flags($w)] && $::opunk::console::tk::eof_flags($w)} {
return 1
}
if {[catch {winfo exists $w} exists]} {
#Tk not loaded (or not usable) in this interp - the widget cannot be
#driven from here; report eof rather than pretending liveness
return 1
}
return [expr {!$exists}]
}
method size {} {
set w [::opunk::Console::in $this]
if {![catch {list [$w cget -width] [$w cget -height]} wh]} {
lassign $wh cols rows
if {[string is integer -strict $cols] && [string is integer -strict $rows] && $cols > 0 && $rows > 0} {
return [dict create columns $cols rows $rows]
}
}
return [::opunk::Console::default_size $this]
}
}
constructor {widget} {
#widget: path of a Tk text widget playing the terminal role. Stored in both the
#inherited in/out slots (documented non-channel reuse). Parent fields are
#private - set via imported index variables.
variable o_in
variable o_out
variable o_terminal_class
set obj [new()]
lset obj $o_in $widget
lset obj $o_out $widget
lset obj $o_terminal_class tk-text
return $obj
}
}
namespace eval ::opunk::TkConsole {
namespace ensemble create
}
tcl::namespace::eval ::opunk::console::tk {
tcl::namespace::export {[a-z]*}
variable PUNKARGS
#flag (or clear) backend eof for a widget-backed console - e.g when the hosting
#frame/toplevel is being torn down but holders may still consult the console value
proc set_eof {widget {value 1}} {
variable eof_flags
set eof_flags($widget) [expr {bool($value)}]
return
}
lappend PUNKARGS [list {
@id -id "(package)opunk::console::tk"
@package -name "opunk::console::tk" -help\
"voo class ::opunk::TkConsole - an ::opunk::Console backend wrapping a Tk text
widget acting as a terminal: size from the widget's character dimensions, eof via
a backend marker (::opunk::console::tk::set_eof) or widget destruction - a
non-channel subclass overriding all channel-shaped base methods (G-001)."
}]
lappend PUNKARGS [list {
@id -id ::opunk::console::tk::set_eof
@cmd -name opunk::console::tk::set_eof\
-summary\
"Flag or clear backend eof for a widget-backed console."\
-help\
"Sets the backend eof marker consulted by ::opunk::TkConsole at_eof.
Use when the hosting frame/toplevel is being torn down (or reopened) while
holders may still consult console values wrapping the widget."
@leaders -min 1 -max 1
widget -type string -help\
"Tk widget path the console wraps"
@values -min 0 -max 1
value -type boolean -default 1 -optional 1 -help\
"1 to flag eof (default), 0 to clear"
}]
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::opunk::console::tk ::opunk::TkConsole
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console::tk [tcl::namespace::eval ::opunk::console::tk {
variable pkg opunk::console::tk
variable version
set version 999999.0a1.0
}]
return

4
src/modules/opunk/console/tk-buildversion.txt

@ -0,0 +1,4 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.
#0.1.0 - G-001: initial ::opunk::Console backend subclass

1
src/tests/modules/AGENTS.md

@ -37,6 +37,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
## Child DOX Index ## Child DOX Index
- `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src <script>`)
- `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack) - `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack)
- `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify). ANSI codes in these tests are literal escape strings so results are colour-state independent - `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify). ANSI codes in these tests are literal escape strings so results are colour-state independent
- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure) - `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure)

224
src/tests/modules/opunk/console/testsuites/console/backends.test

@ -0,0 +1,224 @@
package require tcltest
package require opunk::console
package require opunk::console::test
package require opunk::console::ssh
package require opunk::console::tk
package require punk::console
#G-001: pluggable ::opunk::Console backends. These tests verify the subclassing seam
#the base class was designed for: values carry the subclass namespace tag at slot 0 and
#existing holders (BASE-class method calls, punk::console::console_spec_resolve) dispatch
#to the subclass overrides - with the base ::opunk::Console and punk::console modules
#UNCHANGED (the G-001 acceptance constraint).
#
#Backends covered:
# - ::opunk::TestConsole (opunk::console::test) - deterministic test double over a
# caller-supplied channel pair; the console seam for repl/editbuf characterization
# - ::opunk::SshConsole (opunk::console::ssh) - socket-carried terminal sessions;
# includes the flagship test: size resolved by punk::console's ANSI cursor-report
# provider QUERYING OVER THE SOCKET, answered by a scripted "remote terminal"
# - ::opunk::TkConsole (opunk::console::tk) - Tk text widget as terminal (gated on Tk)
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
#Tk-backed test is opt-in (env PUNK_TEST_TK=1): loading Tk into the shared runtests
#testinterp creates a main window and event-loop side effects that outlive this file
if {[info exists ::env(PUNK_TEST_TK)] && [string is true -strict $::env(PUNK_TEST_TK)]
&& ![catch {package require Tk}]} {
catch {wm withdraw .}
testConstraint tkavailable 1
} else {
testConstraint tkavailable 0
}
proc socketpair {} {
#returns {localend farend} of a connected tcp pair
set srv [socket -server {apply {{ch args} {set ::testspace::accepted $ch}}} 0]
set port [lindex [chan configure $srv -sockname] 2]
set cli [socket 127.0.0.1 $port]
vwait ::testspace::accepted
set local $::testspace::accepted
unset ::testspace::accepted
close $srv
foreach ch [list $local $cli] {
chan configure $ch -translation binary -blocking 0 -buffering none
}
return [list $local $cli]
}
test backends_testconsole_virtual_dispatch {TestConsole values dispatch base-class method calls to the subclass overrides}\
-setup {
set result ""
lassign [chan pipe] rd wr
}\
-body {
set obj [::opunk::TestConsole::new $rd $wr -columns 100 -rows 30]
lappend result [lindex $obj 0]
lappend result [::opunk::Console::size $obj]
lappend result [::opunk::Console::is_console_or_tty $obj]
lappend result [::opunk::Console::can_respond $obj]
lappend result [::opunk::Console::at_eof $obj]
lappend result [expr {[::opunk::Console::channels $obj] eq [list $rd $wr]}]
}\
-cleanup {
close $wr
close $rd
}\
-result [list ::opunk::TestConsole {columns 100 rows 30} 1 1 0 1]
test backends_testconsole_at_eof_never_probes {TestConsole at_eof does not consume pending input (no base-class pipe probe)}\
-setup {
set result ""
lassign [chan pipe] rd wr
}\
-body {
set obj [::opunk::TestConsole::new $rd $wr]
puts -nonewline $wr "X"
flush $wr
lappend result [::opunk::Console::at_eof $obj]
chan configure $rd -blocking 0
lappend result [read $rd 1]
close $wr
read $rd
lappend result [::opunk::Console::at_eof $obj]
}\
-cleanup {
close $rd
}\
-result [list 0 X 1]
test backends_testconsole_settled_can_respond_wins {a settled o_can_respond overrides the TestConsole default of 1}\
-setup {
set result ""
lassign [chan pipe] rd wr
}\
-body {
set obj [::opunk::TestConsole::new $rd $wr]
::opunk::Console::set.o_can_respond obj 0
lappend result [::opunk::Console::can_respond $obj]
}\
-cleanup {
close $wr
close $rd
}\
-result [list 0]
test backends_spec_resolve_accepts_subclass_values {punk::console::console_spec_resolve (unchanged by G-001) accepts subclass object values}\
-setup {
set result ""
lassign [chan pipe] rd wr
}\
-body {
set obj [::opunk::TestConsole::new $rd $wr]
set resolved [punk::console::console_spec_resolve $obj]
lappend result [expr {[dict get $resolved in] eq $rd}]
lappend result [expr {[dict get $resolved out] eq $wr}]
lappend result [lindex [dict get $resolved object] 0]
}\
-cleanup {
close $wr
close $rd
}\
-result [list 1 1 ::opunk::TestConsole]
test backends_sshconsole_capabilities_and_eof {SshConsole reports terminal capability by construction and eof via chan eof after far-end close}\
-setup {
set result ""
lassign [socketpair] local far
}\
-body {
set obj [::opunk::SshConsole::new $local $local]
lappend result [lindex $obj 0]
lappend result [::opunk::Console::is_console_or_tty $obj]
lappend result [::opunk::Console::can_respond $obj]
lappend result [::opunk::Console::at_eof $obj]
close $far
read $local
lappend result [::opunk::Console::at_eof $obj]
}\
-cleanup {
close $local
}\
-result [list ::opunk::SshConsole 1 1 0 1]
test backends_sshconsole_size_via_ansi_query_over_socket {SshConsole size is answered by punk::console's ANSI cursor-report provider querying over the socket - a scripted remote terminal replies}\
-setup {
set result ""
punk::console::ensure_object_integration
lassign [socketpair] local far
#simulated remote terminal (100x30): answer any CSI 6n cursor-position
#request as if the cursor sits at row 30, col 100 (the provider's move-to-
#large-position size probe)
chan event $far readable [list apply {{ch} {
set data [read $ch]
if {[string first "\x1b\[6n" $data] >= 0} {
puts -nonewline $ch "\x1b\[30;100R"
flush $ch
}
}} $far]
}\
-body {
set obj [::opunk::SshConsole::new $local $local]
lappend result [::opunk::Console::size $obj]
}\
-cleanup {
close $far
close $local
}\
-result [list {columns 100 rows 30}]
test backends_sshconsole_size_default_when_unanswered {SshConsole size falls back to the default size when the far end does not answer (and when can_respond is settled 0 no query is attempted)}\
-setup {
set result ""
punk::console::ensure_object_integration
lassign [socketpair] local far
}\
-body {
set obj [::opunk::SshConsole::new $local $local]
#settled 0: no emission path at all - must return default immediately
::opunk::Console::set.o_can_respond obj 0
set before [clock millis]
lappend result [::opunk::Console::size $obj]
lappend result [expr {([clock millis] - $before) < 400}]
}\
-cleanup {
close $far
close $local
}\
-result [list {columns 80 rows 24} 1]
test backends_tkconsole_widget_size_and_eof {TkConsole reports widget character dimensions and backend/destroyed eof}\
-constraints tkavailable\
-setup {
set result ""
set w [text .g001_tkconsole_test -width 100 -height 30]
}\
-body {
set obj [::opunk::TkConsole::new $w]
lappend result [lindex $obj 0]
lappend result [::opunk::Console::terminal_class $obj]
lappend result [::opunk::Console::size $obj]
lappend result [::opunk::Console::is_console_or_tty $obj]
lappend result [::opunk::Console::can_respond $obj]
lappend result [::opunk::Console::at_eof $obj]
#backend eof marker
::opunk::console::tk::set_eof $w
lappend result [::opunk::Console::at_eof $obj]
::opunk::console::tk::set_eof $w 0
lappend result [::opunk::Console::at_eof $obj]
#widget destruction
destroy $w
lappend result [::opunk::Console::at_eof $obj]
}\
-cleanup {
catch {destroy $w}
catch {unset ::opunk::console::tk::eof_flags($w)}
}\
-result [list ::opunk::TkConsole tk-text {columns 100 rows 30} 1 1 0 1 0 1]
}
tcltest::cleanupTests ;#needed to produce test summary line.
Loading…
Cancel
Save