Browse Source

punk::console active settling probe (settle_can_respond) + raw-mode cycling guard

punk::console (0.1.5):
- settle_can_respond: layered active settling of a console's response capability, persisted on
  the anchored opunk::console instance. Layer 1 certainty (input eof -> 0, no emission),
  layer 2 strong-negative heuristic (plain pipe, no terminal hints -> 0, no emission - piped/CI
  output streams stay clean), layer 3 active CSI 6n probe with timeout, only in the ambiguous
  zone (real consoles / pipe-presenting terminals such as mintty without winpty). -force
  unsettles and re-probes; -timeout_ms overrides can_respond_probe_timeout_ms (default 500).
  Unmatched input consumed while waiting is preserved via input_chunks_waiting.
- get_size triggers first-use settling for persistable specs only (anchored instance name or
  the auto-attached default) so unanchored object values cannot cause per-call probes
- get_ansi_response_payload only cycles console raw mode when the input channel is a
  console/tty: pipe-targeted probes no longer flip the real console's mode as a side effect,
  and a repeat-cycle deadlock under captured-channel environments (runtests runx) is fixed.
  (Latent enableRaw_twapi/disableRaw_twapi restore asymmetry noted for future attention.)

punk::repl (0.1.5) / opunk::console (0.1.1): docs reference the active settling mechanism
(no behaviour change; probe machinery stays in punk::console so the class remains free of
that dependency)

tests: 5 new settle_can_respond cases in consolespec.test - certainty and clean-pipe settling
with zero-emission asserts, ambiguous probe timeout with emission and input-preservation
asserts, response arrival settling 1 with realistic event-loop delivery then -force
transition to 0, and get_size first-use settling. Console suites 23/23; full suite green
except pre-existing exec-14.3.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 weeks ago
parent
commit
f6ed50eb0c
  1. 361
      src/bootsupport/modules/opunk/console-0.1.1.tm
  2. 4761
      src/bootsupport/modules/punk/console-0.1.5.tm
  3. 4223
      src/bootsupport/modules/punk/repl-0.1.5.tm
  4. 6
      src/modules/opunk/console-999999.0a1.0.tm
  5. 3
      src/modules/opunk/console-buildversion.txt
  6. 124
      src/modules/punk/console-999999.0a1.0.tm
  7. 4
      src/modules/punk/console-buildversion.txt
  8. 3
      src/modules/punk/repl-999999.0a1.0.tm
  9. 3
      src/modules/punk/repl-buildversion.txt
  10. 361
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/opunk/console-0.1.1.tm
  11. 4761
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.5.tm
  12. 4223
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.5.tm
  13. 361
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/opunk/console-0.1.1.tm
  14. 4761
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.5.tm
  15. 4223
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.5.tm
  16. 146
      src/tests/modules/punk/console/testsuites/console/consolespec.test
  17. 361
      src/vfs/_vfscommon.vfs/modules/opunk/console-0.1.1.tm
  18. 4761
      src/vfs/_vfscommon.vfs/modules/punk/console-0.1.5.tm
  19. 4223
      src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.5.tm

361
src/bootsupport/modules/opunk/console-0.1.1.tm

@ -0,0 +1,361 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console 0.1.1
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package require Tcl 8.6-
package require voo
package require punk::args
#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
#belong to one responding endpoint, together with settled capability facts about that endpoint.
#
#Motivation (see punk::console get_size / get_ansi_response_payload history):
#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
#replies land in some other process's input. A console object makes the pairing an explicit,
#construction-time property instead of a per-query-site guess.
#
#Design notes:
# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
# are anchored at well-known namespace variables (::opunk::console::instances::<name>) and the value
# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
# the lowercase ::opunk::console wrapper procs.
# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so 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.
# - This first cut is standalone and passive: capability detection is heuristic (channel options,
# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
# query probing arrives with punk::console integration (which will delegate here rather than the
# class depending on punk::console).
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
#well-known cooperative store for bytes consumed by probe reads on pipe-like channels
#dict keyed by channel name -> list of chunks. Readers taking over a channel should consume
#and clear any entry for that channel first. (analogous to punk::console::input_chunks_waiting)
variable waiting_chunks
if {![info exists waiting_chunks]} {
set waiting_chunks [dict create]
}
}
voo::class ::opunk::Console -virtual {
private {
#o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
#and matches tclOO-style implementations elsewhere for easier code movement).
#Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
string_t o_in stdin
string_t o_out stdout
string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
dict_t o_default_size [dict create columns 80 rows 24]
}
public {
#settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
#public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
#::opunk::console can settle it without reaching into private my.* accessors.
int_t o_can_respond -1
method in {} {
my.get.o_in $this
}
method out {} {
my.get.o_out $this
}
method channels {} {
list [my.get.o_in $this] [my.get.o_out $this]
}
method terminal_class {} {
my.get.o_terminal_class $this
}
method default_size {} {
my.get.o_default_size $this
}
#Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
#A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
#get a non-blocking 1-byte probe. A probed byte is preserved in ::opunk::console::waiting_chunks
#for cooperating readers. Console/tty channels are never probed (must not consume a keystroke).
method at_eof {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan eof $in} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
if {$is_eof} {
return 1
}
if {[catch {chan configure $in} conf]} {
return 1
}
if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
return 0
}
if {[catch {
set prior_blocking [dict get $conf -blocking]
chan configure $in -blocking 0
set probe [read $in 1]
chan configure $in -blocking $prior_blocking
}]} {
return 1
}
if {$probe ne ""} {
dict lappend ::opunk::console::waiting_chunks $in $probe
}
return [chan eof $in]
}
#Heuristic: could the input channel be connected to a terminal able to answer queries?
#Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
#terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
#negatives there at the cost of false positives for genuinely piped-but-open input.
method is_console_or_tty {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan configure $in} conf]} {
return 0
}
if {[dict exists $conf -inputmode]} {
return 1
}
if {[dict exists $conf -mode]} {
return 1
}
if {[at_eof $this]} {
return 0
}
if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
return 1
}
return 0
}
#Response capability: settled value if known, else computed heuristically (without settling -
#this method is pure; use ::opunk::console::can_respond <name> to settle an anchored instance
#heuristically, or punk::console::settle_can_respond <spec> for active-probe settling - the
#probe machinery lives in punk::console so this class stays free of that dependency).
method can_respond {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return [is_console_or_tty $this]
}
#Size of the console: chan -winsize on the output channel when available (fast, no query),
#else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
#this standalone class - they arrive via punk::console integration and subclasses.
method size {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set out [my.get.o_out $this]
if {![catch {chan configure $out} oconf]} {
if {[dict exists $oconf -winsize]} {
lassign [dict get $oconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
}
}
}
return [my.get.o_default_size $this]
}
}
constructor {in out} {
#new() supplies the class defaults including the virtual class tag at slot 0
set obj [new()]
my.set.o_in obj $in
my.set.o_out obj $out
return $obj
}
}
#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
namespace eval ::opunk::Console {
namespace ensemble create
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Anchored instances - the 'consoles are entities' layer
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval ::opunk::console {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
namespace eval instances {
#one variable per anchored console instance - the canonical location of each console object.
#Access via the sibling procs; methods needing to mutate an instance go through these anchors.
}
proc create {name in out} {
if {![string is wordchar -strict $name]} {
error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
}
set [namespace current]::instances::$name [::opunk::Console::new $in $out]
return [namespace current]::instances::$name
}
proc instancevar {name} {
set v [namespace current]::instances::$name
if {![info exists $v]} {
error "opunk::console: no anchored console instance named '$name' (known: [names])"
}
return $v
}
proc instance {name} {
set [instancevar $name]
}
proc names {} {
set result [list]
foreach v [info vars [namespace current]::instances::*] {
lappend result [namespace tail $v]
}
return $result
}
proc forget {name} {
unset [instancevar $name]
return
}
#settle-and-cache response capability on the anchored instance.
#The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
proc can_respond {name} {
upvar #0 [instancevar $name] obj
set settled [::opunk::Console::get.o_can_respond $obj]
if {$settled != -1} {
return $settled
}
set r [::opunk::Console::can_respond $obj]
::opunk::Console::set.o_can_respond obj $r
return $r
}
proc size {name} {
::opunk::Console::size [instance $name]
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === ===
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS [list {
@id -id "(package)opunk::console"
@package -name "opunk::console" -help\
"voo class ::opunk::Console representing a console as an in/out channel pair with settled
response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
}]
namespace eval argdoc {
proc package_name {} {
return opunk::console
}
proc about_topics {} {
set topic_funs [info commands [namespace current]::get_topic_*]
set about_topics [list]
foreach f $topic_funs {
set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end]
}
return [lsort $about_topics]
}
proc default_topics {} {return [list Description *]}
proc get_topic_Description {} {
punk::args::lib::tstr [string trim {
package opunk::console
voo class for consoles as channel-pair objects with anchored canonical instances.
} \n]
}
proc get_topic_License {} {
return "BSD"
}
proc get_topic_Version {} {
return "$::opunk::console::version"
}
proc get_topic_Contributors {} {
set authors {{"Julian Noble" "julian@precisium.com.au"}}
set contributors ""
foreach a $authors {
append contributors $a \n
}
if {[string index $contributors end] eq "\n"} {
set contributors [string range $contributors 0 end-1]
}
return $contributors
}
proc get_topic_classes {} {
punk::args::lib::tstr -return string {
opunk::Console "virtual class for consoles as in/out channel pairs"
}
}
}
set overrides [dict create]
dict set overrides @id -id "::opunk::console::about"
dict set overrides @cmd -name "opunk::console::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About opunk::console
}] \n]
dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1
dict set overrides topic -default [opunk::console::argdoc::default_topics]
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
lappend PUNKARGS [list $newdef]
proc about {args} {
package require punk::args
set argd [punk::args::parse $args withid ::opunk::console::about]
lassign [dict values $argd] _leaders opts values _received
punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
}
}
# end of sample 'about' function
# == === === === === === === === === === === === === === ===
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
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 ::opunk::Console
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console [tcl::namespace::eval ::opunk::console {
variable pkg opunk::console
variable version
set version 0.1.1
}]
return

4761
src/bootsupport/modules/punk/console-0.1.5.tm

File diff suppressed because it is too large Load Diff

4223
src/bootsupport/modules/punk/repl-0.1.5.tm

File diff suppressed because it is too large Load Diff

6
src/modules/opunk/console-999999.0a1.0.tm

@ -154,9 +154,9 @@ voo::class ::opunk::Console -virtual {
}
#Response capability: settled value if known, else computed heuristically (without settling -
#this method is pure; use ::opunk::console::can_respond <name> to settle an anchored instance).
#TODO: active probe query (device attributes with timeout) when punk::console query machinery
#is integrated - heuristics can then be replaced by one settling probe at first use.
#this method is pure; use ::opunk::console::can_respond <name> to settle an anchored instance
#heuristically, or punk::console::settle_can_respond <spec> for active-probe settling - the
#probe machinery lives in punk::console so this class stays free of that dependency).
method can_respond {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"

3
src/modules/opunk/console-buildversion.txt

@ -1,3 +1,4 @@
0.1.0
0.1.1
#First line must be a semantic version number
#all other lines are ignored.
#0.1.1 - can_respond method docs reference punk::console::settle_can_respond for active-probe settling (no behaviour change)

124
src/modules/punk/console-999999.0a1.0.tm

@ -454,11 +454,21 @@ namespace eval punk::console {
set waitvarname "::punk::console::ansi_response_wait($callid)"
#Only cycle raw mode when the input channel is actually a console/tty.
#enableRaw/disableRaw operate on the REAL process console (twapi/stty) regardless of the
#channel arg - cycling it for a query on unrelated channels (e.g pipe-pair probes from
#settle_can_respond) flips the user's console mode as a side effect, and repeated cycles
#have been observed to deadlock under stacked/captured channel environments (runtests runx).
#For mintty-without-winpty (terminal presenting pipes) the twapi raw cycle was already a
#no-op via its get_console_handle early-return, so nothing is lost by skipping.
set input_is_console_or_tty [expr {[dict exists $previous_input_state -inputmode] || [dict exists $previous_input_state -mode]}]
if {![tsv::get console is_raw]} {
set was_raw 0
punk::console::enableRaw
#after 0 [list chan event $input readable [list $this_handler $input $callid $capturingendregex]]
incr expected 50 ;#review
if {$input_is_console_or_tty} {
punk::console::enableRaw
#after 0 [list chan event $input readable [list $this_handler $input $callid $capturingendregex]]
incr expected 50 ;#review
}
set timeoutid($callid) [after $expected [list set $waitvarname timedout]]
#puts stdout "sending console request [ansistring VIEW $query]"
} else {
@ -544,7 +554,7 @@ namespace eval punk::console {
puts stderr "timeout (timediff [expr {[clock millis] - $tslaunch($callid)}]ms) in get_ansi_response_payload. callid $callid Ansi request was:'[ansistring VIEW -lf 1 -vt 1 $query]'"
}
if {$was_raw == 0} {
if {$was_raw == 0 && $input_is_console_or_tty} {
punk::console::disableRaw
}
@ -1677,6 +1687,104 @@ namespace eval punk::console {
return default
}
#default timeout for the active can_respond settling probe (see settle_can_respond)
#short enough not to stall first use badly on non-responding consoles - long enough for real
#terminals (incl. moderate ssh latency) to answer a cursor position report.
variable can_respond_probe_timeout_ms
if {![info exists can_respond_probe_timeout_ms]} {
set can_respond_probe_timeout_ms 500
}
punk::args::define {
@id -id ::punk::console::settle_can_respond
@cmd -name punk::console::settle_can_respond -summary\
"Actively settle a console's response capability."\
-help\
"Determines whether the console specified can answer ANSI queries, settling the result
on the governing opunk::console instance when one is anchored (instance name spec, or a
channel pair governed by the anchored 'default' instance). Layered determination:
1. certainty - input at eof/closed -> 0 (no query emitted)
2. heuristic - input provably not a console/tty and no terminal env hints -> 0
(no query emitted - piped/CI output streams stay clean)
3. active probe - a cursor position report (CSI 6n) is emitted and 1 settled only if a
response arrives within -timeout_ms. Only reached in the ambiguous zone
(real consoles, or pipe-presenting terminals such as mintty without
winpty) where queries are already routine.
Unmatched input consumed while waiting is preserved in punk::console::input_chunks_waiting.
For an unanchored object-value or plain channel-pair spec the result is returned but cannot
be persisted."
@opts
-force -type none -help\
"Re-probe and re-settle even if capability is already settled."
-timeout_ms -type integer -default "" -help\
"Probe timeout in ms. Empty uses punk::console::can_respond_probe_timeout_ms."
@values -min 1 -max 1
consolespec -type list -help\
"Console specification: 2-element {in out} channel list, anchored opunk::console instance
name, or ::opunk::Console object value."
}
proc settle_can_respond {args} {
variable can_respond_probe_timeout_ms
set argd [punk::args::parse $args withid ::punk::console::settle_can_respond]
lassign [dict values $argd] leaders opts values received
set opt_force [dict exists $received -force]
set timeout_ms [dict get $opts -timeout_ms]
if {$timeout_ms eq ""} {
set timeout_ms $can_respond_probe_timeout_ms
}
set consolespec [dict get $values consolespec]
set cinfo [console_spec_resolve $consolespec]
set in [dict get $cinfo in]
set out [dict get $cinfo out]
set cobj [dict get $cinfo object]
#determine the anchor variable (persistence point) if any
set anchorvar ""
if {[llength $consolespec] == 1} {
set anchorvar [opunk::console::instancevar $consolespec]
} elseif {$cobj ne "" && [info exists ::opunk::console::instances::default] && $cobj eq [set ::opunk::console::instances::default]} {
set anchorvar ::opunk::console::instances::default
}
if {$cobj ne ""} {
set settled [::opunk::Console::get.o_can_respond $cobj]
if {$settled != -1 && !$opt_force} {
return $settled
}
if {$opt_force && $anchorvar ne ""} {
#unsettle first so the probe's own query is not refused by get_ansi_response_payload
upvar #0 $anchorvar reprobe_obj
::opunk::Console::set.o_can_respond reprobe_obj -1
}
}
#layer 1 - certainty: an eof/closed input can never deliver a response
if {[input_at_eof $in]} {
set verdict 0
} elseif {![is_input_console_or_tty $in]} {
#layer 2 - strong negative heuristic: plain pipe, no terminal indicators or env hints.
#No query is emitted - piped output streams stay clean.
set verdict 0
} else {
#layer 3 - active probe (ambiguous zone only): cursor position report
set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$}
if {[catch {
punk::console::internal::get_ansi_response_payload -expected_ms $timeout_ms -console [list $in $out] "\033\[6n" $capturingregex
} payload]} {
set verdict 0
} else {
set verdict [expr {$payload ne "" ? 1 : 0}]
}
}
if {$anchorvar ne ""} {
upvar #0 $anchorvar settle_obj
::opunk::Console::set.o_can_respond settle_obj $verdict
}
return $verdict
}
punk::args::define {
@id -id ::punk::console::get_size
@cmd -name punk::console::get_size -summary\
@ -1734,6 +1842,14 @@ namespace eval punk::console {
set can_respond -1
if {$cobj ne ""} {
set can_respond [::opunk::Console::get.o_can_respond $cobj]
if {$can_respond == -1} {
#first-use active settling - only for persistable specs (anchored instance name, or
#the auto-attached default instance) so unanchored object values can't cause a probe
#per call. settle_can_respond only emits a query in the ambiguous zone.
if {[llength $consolespec] == 1 || ([info exists ::opunk::console::instances::default] && $cobj eq [set ::opunk::console::instances::default])} {
set can_respond [settle_can_respond $consolespec]
}
}
}
if {$can_respond == 0} {
#console object is settled as unable to answer queries - its default_size is authoritative.

4
src/modules/punk/console-buildversion.txt

@ -1,6 +1,8 @@
0.1.4
0.1.5
#First line must be a semantic version number
#all other lines are ignored.
#0.1.5 - settle_can_respond: layered active settling of console response capability (certainty/heuristic without emission, CSI 6n probe with timeout in the ambiguous zone); get_size triggers it at first use for anchored/default console specs; new variable can_respond_probe_timeout_ms
#0.1.5 - get_ansi_response_payload only cycles console raw mode when the input channel is a console/tty - pipe-targeted probes no longer flip the process console mode (also fixes a repeat-cycle deadlock under captured-channel environments)
#0.1.4 - new default_console proc; console_spec_resolve auto-attaches the anchored opunk::console instance (name: default) for matching channel pairs
#0.1.3 - new console_spec_resolve: -console (and legacy positional inoutchannels) accept a channel pair, an anchored opunk::console instance name, or an ::opunk::Console object value
#0.1.3 - get_size converted to manual-parse hybrid signature with PUNKARGS documentation; a supplied console object with settled can_respond skips/permits ANSI mechanisms authoritatively and supplies the fallback default size

3
src/modules/punk/repl-999999.0a1.0.tm

@ -3113,7 +3113,8 @@ namespace eval repl {
#We deliberately do NOT settle 0 at init: the negative heuristic involves a probe read whose
#consumed byte would be parked in ::opunk::console::waiting_chunks where the repl reader would
#not find it - risking loss of the first byte of piped input. Ambiguous/non-interactive cases
#stay unsettled (-1) so per-call heuristics and query timeouts apply as before.
#stay unsettled (-1) and are settled at first console use by punk::console::settle_can_respond
#(layered: certainty/heuristic without emission, active CSI 6n probe only in the ambiguous zone).
if {![catch {package require opunk::console}]} {
if {![info exists ::opunk::console::instances::default]} {
opunk::console::create default stdin stdout

3
src/modules/punk/repl-buildversion.txt

@ -1,6 +1,7 @@
0.1.4
0.1.5
#First line must be a semantic version number
#all other lines are ignored.
#0.1.5 - repl::init comment updated: ambiguous capability settles at first console use via punk::console::settle_can_respond (no behaviour change)
#0.1.4 - repl::init constructs the process-default console object (opunk::console instance: default) and settles can_respond 1 for interactive stdin (negative settle deliberately not done at init - see comment)
#0.1.3 - repl::start now cancels the deferred (after idle) stdin reader registration when the repl finishes, preventing a stale registration re-attaching a reader to a completed repl
#0.1.3 - repl_process_data drops input that arrives after codethread teardown instead of raising 'invalid thread handle ""' from thread::send

361
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/opunk/console-0.1.1.tm

@ -0,0 +1,361 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console 0.1.1
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package require Tcl 8.6-
package require voo
package require punk::args
#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
#belong to one responding endpoint, together with settled capability facts about that endpoint.
#
#Motivation (see punk::console get_size / get_ansi_response_payload history):
#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
#replies land in some other process's input. A console object makes the pairing an explicit,
#construction-time property instead of a per-query-site guess.
#
#Design notes:
# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
# are anchored at well-known namespace variables (::opunk::console::instances::<name>) and the value
# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
# the lowercase ::opunk::console wrapper procs.
# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so 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.
# - This first cut is standalone and passive: capability detection is heuristic (channel options,
# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
# query probing arrives with punk::console integration (which will delegate here rather than the
# class depending on punk::console).
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
#well-known cooperative store for bytes consumed by probe reads on pipe-like channels
#dict keyed by channel name -> list of chunks. Readers taking over a channel should consume
#and clear any entry for that channel first. (analogous to punk::console::input_chunks_waiting)
variable waiting_chunks
if {![info exists waiting_chunks]} {
set waiting_chunks [dict create]
}
}
voo::class ::opunk::Console -virtual {
private {
#o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
#and matches tclOO-style implementations elsewhere for easier code movement).
#Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
string_t o_in stdin
string_t o_out stdout
string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
dict_t o_default_size [dict create columns 80 rows 24]
}
public {
#settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
#public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
#::opunk::console can settle it without reaching into private my.* accessors.
int_t o_can_respond -1
method in {} {
my.get.o_in $this
}
method out {} {
my.get.o_out $this
}
method channels {} {
list [my.get.o_in $this] [my.get.o_out $this]
}
method terminal_class {} {
my.get.o_terminal_class $this
}
method default_size {} {
my.get.o_default_size $this
}
#Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
#A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
#get a non-blocking 1-byte probe. A probed byte is preserved in ::opunk::console::waiting_chunks
#for cooperating readers. Console/tty channels are never probed (must not consume a keystroke).
method at_eof {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan eof $in} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
if {$is_eof} {
return 1
}
if {[catch {chan configure $in} conf]} {
return 1
}
if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
return 0
}
if {[catch {
set prior_blocking [dict get $conf -blocking]
chan configure $in -blocking 0
set probe [read $in 1]
chan configure $in -blocking $prior_blocking
}]} {
return 1
}
if {$probe ne ""} {
dict lappend ::opunk::console::waiting_chunks $in $probe
}
return [chan eof $in]
}
#Heuristic: could the input channel be connected to a terminal able to answer queries?
#Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
#terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
#negatives there at the cost of false positives for genuinely piped-but-open input.
method is_console_or_tty {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan configure $in} conf]} {
return 0
}
if {[dict exists $conf -inputmode]} {
return 1
}
if {[dict exists $conf -mode]} {
return 1
}
if {[at_eof $this]} {
return 0
}
if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
return 1
}
return 0
}
#Response capability: settled value if known, else computed heuristically (without settling -
#this method is pure; use ::opunk::console::can_respond <name> to settle an anchored instance
#heuristically, or punk::console::settle_can_respond <spec> for active-probe settling - the
#probe machinery lives in punk::console so this class stays free of that dependency).
method can_respond {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return [is_console_or_tty $this]
}
#Size of the console: chan -winsize on the output channel when available (fast, no query),
#else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
#this standalone class - they arrive via punk::console integration and subclasses.
method size {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set out [my.get.o_out $this]
if {![catch {chan configure $out} oconf]} {
if {[dict exists $oconf -winsize]} {
lassign [dict get $oconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
}
}
}
return [my.get.o_default_size $this]
}
}
constructor {in out} {
#new() supplies the class defaults including the virtual class tag at slot 0
set obj [new()]
my.set.o_in obj $in
my.set.o_out obj $out
return $obj
}
}
#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
namespace eval ::opunk::Console {
namespace ensemble create
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Anchored instances - the 'consoles are entities' layer
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval ::opunk::console {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
namespace eval instances {
#one variable per anchored console instance - the canonical location of each console object.
#Access via the sibling procs; methods needing to mutate an instance go through these anchors.
}
proc create {name in out} {
if {![string is wordchar -strict $name]} {
error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
}
set [namespace current]::instances::$name [::opunk::Console::new $in $out]
return [namespace current]::instances::$name
}
proc instancevar {name} {
set v [namespace current]::instances::$name
if {![info exists $v]} {
error "opunk::console: no anchored console instance named '$name' (known: [names])"
}
return $v
}
proc instance {name} {
set [instancevar $name]
}
proc names {} {
set result [list]
foreach v [info vars [namespace current]::instances::*] {
lappend result [namespace tail $v]
}
return $result
}
proc forget {name} {
unset [instancevar $name]
return
}
#settle-and-cache response capability on the anchored instance.
#The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
proc can_respond {name} {
upvar #0 [instancevar $name] obj
set settled [::opunk::Console::get.o_can_respond $obj]
if {$settled != -1} {
return $settled
}
set r [::opunk::Console::can_respond $obj]
::opunk::Console::set.o_can_respond obj $r
return $r
}
proc size {name} {
::opunk::Console::size [instance $name]
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === ===
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS [list {
@id -id "(package)opunk::console"
@package -name "opunk::console" -help\
"voo class ::opunk::Console representing a console as an in/out channel pair with settled
response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
}]
namespace eval argdoc {
proc package_name {} {
return opunk::console
}
proc about_topics {} {
set topic_funs [info commands [namespace current]::get_topic_*]
set about_topics [list]
foreach f $topic_funs {
set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end]
}
return [lsort $about_topics]
}
proc default_topics {} {return [list Description *]}
proc get_topic_Description {} {
punk::args::lib::tstr [string trim {
package opunk::console
voo class for consoles as channel-pair objects with anchored canonical instances.
} \n]
}
proc get_topic_License {} {
return "BSD"
}
proc get_topic_Version {} {
return "$::opunk::console::version"
}
proc get_topic_Contributors {} {
set authors {{"Julian Noble" "julian@precisium.com.au"}}
set contributors ""
foreach a $authors {
append contributors $a \n
}
if {[string index $contributors end] eq "\n"} {
set contributors [string range $contributors 0 end-1]
}
return $contributors
}
proc get_topic_classes {} {
punk::args::lib::tstr -return string {
opunk::Console "virtual class for consoles as in/out channel pairs"
}
}
}
set overrides [dict create]
dict set overrides @id -id "::opunk::console::about"
dict set overrides @cmd -name "opunk::console::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About opunk::console
}] \n]
dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1
dict set overrides topic -default [opunk::console::argdoc::default_topics]
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
lappend PUNKARGS [list $newdef]
proc about {args} {
package require punk::args
set argd [punk::args::parse $args withid ::opunk::console::about]
lassign [dict values $argd] _leaders opts values _received
punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
}
}
# end of sample 'about' function
# == === === === === === === === === === === === === === ===
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
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 ::opunk::Console
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console [tcl::namespace::eval ::opunk::console {
variable pkg opunk::console
variable version
set version 0.1.1
}]
return

4761
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.5.tm

File diff suppressed because it is too large Load Diff

4223
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.5.tm

File diff suppressed because it is too large Load Diff

361
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/opunk/console-0.1.1.tm

@ -0,0 +1,361 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console 0.1.1
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package require Tcl 8.6-
package require voo
package require punk::args
#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
#belong to one responding endpoint, together with settled capability facts about that endpoint.
#
#Motivation (see punk::console get_size / get_ansi_response_payload history):
#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
#replies land in some other process's input. A console object makes the pairing an explicit,
#construction-time property instead of a per-query-site guess.
#
#Design notes:
# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
# are anchored at well-known namespace variables (::opunk::console::instances::<name>) and the value
# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
# the lowercase ::opunk::console wrapper procs.
# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so 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.
# - This first cut is standalone and passive: capability detection is heuristic (channel options,
# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
# query probing arrives with punk::console integration (which will delegate here rather than the
# class depending on punk::console).
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
#well-known cooperative store for bytes consumed by probe reads on pipe-like channels
#dict keyed by channel name -> list of chunks. Readers taking over a channel should consume
#and clear any entry for that channel first. (analogous to punk::console::input_chunks_waiting)
variable waiting_chunks
if {![info exists waiting_chunks]} {
set waiting_chunks [dict create]
}
}
voo::class ::opunk::Console -virtual {
private {
#o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
#and matches tclOO-style implementations elsewhere for easier code movement).
#Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
string_t o_in stdin
string_t o_out stdout
string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
dict_t o_default_size [dict create columns 80 rows 24]
}
public {
#settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
#public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
#::opunk::console can settle it without reaching into private my.* accessors.
int_t o_can_respond -1
method in {} {
my.get.o_in $this
}
method out {} {
my.get.o_out $this
}
method channels {} {
list [my.get.o_in $this] [my.get.o_out $this]
}
method terminal_class {} {
my.get.o_terminal_class $this
}
method default_size {} {
my.get.o_default_size $this
}
#Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
#A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
#get a non-blocking 1-byte probe. A probed byte is preserved in ::opunk::console::waiting_chunks
#for cooperating readers. Console/tty channels are never probed (must not consume a keystroke).
method at_eof {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan eof $in} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
if {$is_eof} {
return 1
}
if {[catch {chan configure $in} conf]} {
return 1
}
if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
return 0
}
if {[catch {
set prior_blocking [dict get $conf -blocking]
chan configure $in -blocking 0
set probe [read $in 1]
chan configure $in -blocking $prior_blocking
}]} {
return 1
}
if {$probe ne ""} {
dict lappend ::opunk::console::waiting_chunks $in $probe
}
return [chan eof $in]
}
#Heuristic: could the input channel be connected to a terminal able to answer queries?
#Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
#terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
#negatives there at the cost of false positives for genuinely piped-but-open input.
method is_console_or_tty {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan configure $in} conf]} {
return 0
}
if {[dict exists $conf -inputmode]} {
return 1
}
if {[dict exists $conf -mode]} {
return 1
}
if {[at_eof $this]} {
return 0
}
if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
return 1
}
return 0
}
#Response capability: settled value if known, else computed heuristically (without settling -
#this method is pure; use ::opunk::console::can_respond <name> to settle an anchored instance
#heuristically, or punk::console::settle_can_respond <spec> for active-probe settling - the
#probe machinery lives in punk::console so this class stays free of that dependency).
method can_respond {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return [is_console_or_tty $this]
}
#Size of the console: chan -winsize on the output channel when available (fast, no query),
#else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
#this standalone class - they arrive via punk::console integration and subclasses.
method size {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set out [my.get.o_out $this]
if {![catch {chan configure $out} oconf]} {
if {[dict exists $oconf -winsize]} {
lassign [dict get $oconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
}
}
}
return [my.get.o_default_size $this]
}
}
constructor {in out} {
#new() supplies the class defaults including the virtual class tag at slot 0
set obj [new()]
my.set.o_in obj $in
my.set.o_out obj $out
return $obj
}
}
#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
namespace eval ::opunk::Console {
namespace ensemble create
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Anchored instances - the 'consoles are entities' layer
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval ::opunk::console {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
namespace eval instances {
#one variable per anchored console instance - the canonical location of each console object.
#Access via the sibling procs; methods needing to mutate an instance go through these anchors.
}
proc create {name in out} {
if {![string is wordchar -strict $name]} {
error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
}
set [namespace current]::instances::$name [::opunk::Console::new $in $out]
return [namespace current]::instances::$name
}
proc instancevar {name} {
set v [namespace current]::instances::$name
if {![info exists $v]} {
error "opunk::console: no anchored console instance named '$name' (known: [names])"
}
return $v
}
proc instance {name} {
set [instancevar $name]
}
proc names {} {
set result [list]
foreach v [info vars [namespace current]::instances::*] {
lappend result [namespace tail $v]
}
return $result
}
proc forget {name} {
unset [instancevar $name]
return
}
#settle-and-cache response capability on the anchored instance.
#The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
proc can_respond {name} {
upvar #0 [instancevar $name] obj
set settled [::opunk::Console::get.o_can_respond $obj]
if {$settled != -1} {
return $settled
}
set r [::opunk::Console::can_respond $obj]
::opunk::Console::set.o_can_respond obj $r
return $r
}
proc size {name} {
::opunk::Console::size [instance $name]
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === ===
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS [list {
@id -id "(package)opunk::console"
@package -name "opunk::console" -help\
"voo class ::opunk::Console representing a console as an in/out channel pair with settled
response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
}]
namespace eval argdoc {
proc package_name {} {
return opunk::console
}
proc about_topics {} {
set topic_funs [info commands [namespace current]::get_topic_*]
set about_topics [list]
foreach f $topic_funs {
set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end]
}
return [lsort $about_topics]
}
proc default_topics {} {return [list Description *]}
proc get_topic_Description {} {
punk::args::lib::tstr [string trim {
package opunk::console
voo class for consoles as channel-pair objects with anchored canonical instances.
} \n]
}
proc get_topic_License {} {
return "BSD"
}
proc get_topic_Version {} {
return "$::opunk::console::version"
}
proc get_topic_Contributors {} {
set authors {{"Julian Noble" "julian@precisium.com.au"}}
set contributors ""
foreach a $authors {
append contributors $a \n
}
if {[string index $contributors end] eq "\n"} {
set contributors [string range $contributors 0 end-1]
}
return $contributors
}
proc get_topic_classes {} {
punk::args::lib::tstr -return string {
opunk::Console "virtual class for consoles as in/out channel pairs"
}
}
}
set overrides [dict create]
dict set overrides @id -id "::opunk::console::about"
dict set overrides @cmd -name "opunk::console::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About opunk::console
}] \n]
dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1
dict set overrides topic -default [opunk::console::argdoc::default_topics]
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
lappend PUNKARGS [list $newdef]
proc about {args} {
package require punk::args
set argd [punk::args::parse $args withid ::opunk::console::about]
lassign [dict values $argd] _leaders opts values _received
punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
}
}
# end of sample 'about' function
# == === === === === === === === === === === === === === ===
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
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 ::opunk::Console
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console [tcl::namespace::eval ::opunk::console {
variable pkg opunk::console
variable version
set version 0.1.1
}]
return

4761
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.5.tm

File diff suppressed because it is too large Load Diff

4223
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.5.tm

File diff suppressed because it is too large Load Diff

146
src/tests/modules/punk/console/testsuites/console/consolespec.test

@ -18,6 +18,22 @@ namespace eval ::testspace {
variable common {
set result ""
}
#is_input_console_or_tty / settle_can_respond consult env(TERM_PROGRAM)/env(MSYSTEM) as
#mintty-without-winpty hints - save/clear them around tests where they affect outcomes.
variable hints_save {
set saved_hints [dict create]
foreach ev {TERM_PROGRAM MSYSTEM} {
if {[info exists ::env($ev)]} {
dict set saved_hints $ev [set ::env($ev)]
unset ::env($ev)
}
}
}
variable hints_restore {
dict for {ev val} $saved_hints {
set ::env($ev) $val
}
}
test consolespec_resolve_forms {channel pair, anchored name and object value resolve; bad specs error}\
-setup $common -body {
@ -188,5 +204,135 @@ namespace eval ::testspace {
1\
]
# -- active settling probe (settle_can_respond) --------------------------------------------------
#timeout lowered per-test via punk::console::can_respond_probe_timeout_ms save/restore
variable probe_setup {
set result ""
set saved_probe_timeout $::punk::console::can_respond_probe_timeout_ms
set ::punk::console::can_respond_probe_timeout_ms 150
lassign [chan pipe] rdi wri
lassign [chan pipe] rdo wro
chan configure $rdo -blocking 0
}
variable probe_cleanup {
set ::punk::console::can_respond_probe_timeout_ms $saved_probe_timeout
foreach c [list $wri $rdi $wro $rdo] {
catch {chan close $c}
}
array unset ::punk::console::input_chunks_waiting $rdi
}
test settle_certainty_eof {eof input settles 0 with no query emission}\
-setup [string cat $common $hints_save $probe_setup]\
-body {
chan close $wri
opunk::console::create testprobe1 $rdi $wro
lappend result [punk::console::settle_can_respond testprobe1]
lappend result [::opunk::Console::get.o_can_respond [opunk::console::instance testprobe1]]
lappend result [read $rdo] ;#nothing may have been written to the console out channel
}\
-cleanup [string cat {
catch {opunk::console::forget testprobe1}
} $hints_restore $probe_cleanup]\
-result [list\
0\
0\
{}\
]
test settle_clean_pipe_no_emission {open pipe without terminal hints settles 0 heuristically - no query emission}\
-setup [string cat $common $hints_save $probe_setup]\
-body {
opunk::console::create testprobe2 $rdi $wro
lappend result [punk::console::settle_can_respond testprobe2]
lappend result [read $rdo]
}\
-cleanup [string cat {
catch {opunk::console::forget testprobe2}
} $hints_restore $probe_cleanup]\
-result [list\
0\
{}\
]
test settle_ambiguous_probe_timeout {hinted open pipe probes (CSI 6n emitted), times out to 0, pending input preserved}\
-setup [string cat $common $hints_save $probe_setup]\
-body {
set ::env(TERM_PROGRAM) mintty
puts -nonewline $wri "pending123"
flush $wri
opunk::console::create testprobe3 $rdi $wro
lappend result [punk::console::settle_can_respond testprobe3]
#query must have been emitted on the out channel
lappend result [expr {[string first "\x1b\[6n" [read $rdo]] >= 0}]
#pending input consumed while waiting must be reconstructable from input_chunks_waiting
set waiting ""
if {[info exists ::punk::console::input_chunks_waiting($rdi)]} {
set waiting [join $::punk::console::input_chunks_waiting($rdi) ""]
}
lappend result $waiting
}\
-cleanup [string cat {
unset -nocomplain ::env(TERM_PROGRAM)
catch {opunk::console::forget testprobe3}
} $hints_restore $probe_cleanup]\
-result [list\
0\
1\
pending123\
]
test settle_ambiguous_response_then_force {hinted pipe with arriving response settles 1; -force with no response transitions to 0}\
-setup [string cat $common $hints_save $probe_setup]\
-body {
set ::env(TERM_PROGRAM) mintty
opunk::console::create testprobe4 $rdi $wro
#deliver a cursor position report after the query has been emitted (fires inside the
#probe's event loop - simulating a real terminal's response timing)
after 40 [list apply [list {wchan} {
puts -nonewline $wchan "\x1b\[24;80R"
flush $wchan
}] $wri]
lappend result [punk::console::settle_can_respond testprobe4]
lappend result [::opunk::Console::get.o_can_respond [opunk::console::instance testprobe4]]
#settled short-circuit: no new probe without -force
lappend result [punk::console::settle_can_respond testprobe4]
#force re-probe with no response this time - capability transitions to 0
lappend result [punk::console::settle_can_respond -force testprobe4]
lappend result [::opunk::Console::get.o_can_respond [opunk::console::instance testprobe4]]
}\
-cleanup [string cat {
unset -nocomplain ::env(TERM_PROGRAM)
catch {opunk::console::forget testprobe4}
} $hints_restore $probe_cleanup]\
-result [list\
1\
1\
1\
0\
0\
]
test settle_get_size_first_use {get_size triggers first-use settling for the anchored default console}\
-setup [string cat $common $hints_save $probe_setup]\
-body {
catch {opunk::console::forget default}
set obj [::opunk::Console::new.args -o_in $rdi -o_out $wro -o_default_size {columns 99 rows 33}]
set ::opunk::console::instances::default $obj
#no hints, open pipe: layer 2 settles 0 without emission - get_size returns the default size
lappend result [punk::console::get_size [list $rdi $wro]]
lappend result [::opunk::Console::get.o_can_respond $::opunk::console::instances::default]
lappend result [read $rdo]
}\
-cleanup [string cat {
catch {opunk::console::forget default}
} $hints_restore $probe_cleanup]\
-result [list\
{columns 99 rows 33}\
0\
{}\
]
}
tcltest::cleanupTests ;#needed to produce test summary.

361
src/vfs/_vfscommon.vfs/modules/opunk/console-0.1.1.tm

@ -0,0 +1,361 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application opunk::console 0.1.1
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package require Tcl 8.6-
package require voo
package require punk::args
#opunk::Console - a voo (value-based) representation of a console: an in/out channel pair claimed to
#belong to one responding endpoint, together with settled capability facts about that endpoint.
#
#Motivation (see punk::console get_size / get_ansi_response_payload history):
#ANSI query mechanisms assume the input channel receives what the output channel's device emits in
#response. When stdio is redirected that pairing silently breaks - queries go to a terminal whose
#replies land in some other process's input. A console object makes the pairing an explicit,
#construction-time property instead of a per-query-site guess.
#
#Design notes:
# - Objects are voo values (plain lists). Consoles are entities, not values - so canonical instances
# are anchored at well-known namespace variables (::opunk::console::instances::<name>) and the value
# semantics serve as cheap snapshots. Mutation (settling capabilities) goes through the anchor via
# the lowercase ::opunk::console wrapper procs.
# - The class is -virtual: slot 0 of each value carries the concrete class namespace, so 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.
# - This first cut is standalone and passive: capability detection is heuristic (channel options,
# probed eof, terminal env hints) and size uses chan -winsize else the default size. Active ANSI
# query probing arrives with punk::console integration (which will delegate here rather than the
# class depending on punk::console).
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
#well-known cooperative store for bytes consumed by probe reads on pipe-like channels
#dict keyed by channel name -> list of chunks. Readers taking over a channel should consume
#and clear any entry for that channel first. (analogous to punk::console::input_chunks_waiting)
variable waiting_chunks
if {![info exists waiting_chunks]} {
set waiting_chunks [dict create]
}
}
voo::class ::opunk::Console -virtual {
private {
#o_ prefix per opunk voo convention (distinguishes fields from locals in -update methods,
#and matches tclOO-style implementations elsewhere for easier code movement).
#Note the class is -virtual: slot 0 of the object value is the concrete class namespace tag.
string_t o_in stdin
string_t o_out stdout
string_t o_terminal_class "" ;#e.g windows-console, tty, pipe - "" until classified
list_t o_size_mechanism [list] ;#preferred size mechanism try-order - empty until timed/settled
dict_t o_default_size [dict create columns 80 rows 24]
}
public {
#settled response capability: -1 unknown, 0 cannot respond, 1 can respond.
#public accessors (get.o_can_respond / set.o_can_respond) so the anchored-mutation wrappers in
#::opunk::console can settle it without reaching into private my.* accessors.
int_t o_can_respond -1
method in {} {
my.get.o_in $this
}
method out {} {
my.get.o_out $this
}
method channels {} {
list [my.get.o_in $this] [my.get.o_out $this]
}
method terminal_class {} {
my.get.o_terminal_class $this
}
method default_size {} {
my.get.o_default_size $this
}
#Certainty test: is the input channel closed or at eof (and so can never deliver a response)?
#A consumed/half-closed pipe may not flag eof until a read is attempted, so pipe-like channels
#get a non-blocking 1-byte probe. A probed byte is preserved in ::opunk::console::waiting_chunks
#for cooperating readers. Console/tty channels are never probed (must not consume a keystroke).
method at_eof {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan eof $in} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
if {$is_eof} {
return 1
}
if {[catch {chan configure $in} conf]} {
return 1
}
if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
return 0
}
if {[catch {
set prior_blocking [dict get $conf -blocking]
chan configure $in -blocking 0
set probe [read $in 1]
chan configure $in -blocking $prior_blocking
}]} {
return 1
}
if {$probe ne ""} {
dict lappend ::opunk::console::waiting_chunks $in $probe
}
return [chan eof $in]
}
#Heuristic: could the input channel be connected to a terminal able to answer queries?
#Returns 0 only when reasonably confident it cannot. mintty without winpty presents a
#terminal's stdin as a plain pipe, so env hints (MSYSTEM, TERM_PROGRAM) prevent false
#negatives there at the cost of false positives for genuinely piped-but-open input.
method is_console_or_tty {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set in [my.get.o_in $this]
if {[catch {chan configure $in} conf]} {
return 0
}
if {[dict exists $conf -inputmode]} {
return 1
}
if {[dict exists $conf -mode]} {
return 1
}
if {[at_eof $this]} {
return 0
}
if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
return 1
}
return 0
}
#Response capability: settled value if known, else computed heuristically (without settling -
#this method is pure; use ::opunk::console::can_respond <name> to settle an anchored instance
#heuristically, or punk::console::settle_can_respond <spec> for active-probe settling - the
#probe machinery lives in punk::console so this class stays free of that dependency).
method can_respond {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set settled [get.o_can_respond $this]
if {$settled != -1} {
return $settled
}
return [is_console_or_tty $this]
}
#Size of the console: chan -winsize on the output channel when available (fast, no query),
#else the configured default size. ANSI cursor-report mechanisms are deliberately absent from
#this standalone class - they arrive via punk::console integration and subclasses.
method size {} -virtual {
if {[llength $this] < 7} {
error "invalid object $this"
}
set out [my.get.o_out $this]
if {![catch {chan configure $out} oconf]} {
if {[dict exists $oconf -winsize]} {
lassign [dict get $oconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
}
}
}
return [my.get.o_default_size $this]
}
}
constructor {in out} {
#new() supplies the class defaults including the virtual class tag at slot 0
set obj [new()]
my.set.o_in obj $in
my.set.o_out obj $out
return $obj
}
}
#make introspection (with i and punk::ns::cmdinfo) easier by making the class namespace an ensemble - we can still access it as ::opunk::Console
namespace eval ::opunk::Console {
namespace ensemble create
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Anchored instances - the 'consoles are entities' layer
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval ::opunk::console {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
namespace eval instances {
#one variable per anchored console instance - the canonical location of each console object.
#Access via the sibling procs; methods needing to mutate an instance go through these anchors.
}
proc create {name in out} {
if {![string is wordchar -strict $name]} {
error "opunk::console::create name must be alphanumeric/underscore, got '$name'"
}
set [namespace current]::instances::$name [::opunk::Console::new $in $out]
return [namespace current]::instances::$name
}
proc instancevar {name} {
set v [namespace current]::instances::$name
if {![info exists $v]} {
error "opunk::console: no anchored console instance named '$name' (known: [names])"
}
return $v
}
proc instance {name} {
set [instancevar $name]
}
proc names {} {
set result [list]
foreach v [info vars [namespace current]::instances::*] {
lappend result [namespace tail $v]
}
return $result
}
proc forget {name} {
unset [instancevar $name]
return
}
#settle-and-cache response capability on the anchored instance.
#The class method is pure (value-in) - this wrapper owns the mutation via the anchor.
proc can_respond {name} {
upvar #0 [instancevar $name] obj
set settled [::opunk::Console::get.o_can_respond $obj]
if {$settled != -1} {
return $settled
}
set r [::opunk::Console::can_respond $obj]
::opunk::Console::set.o_can_respond obj $r
return $r
}
proc size {name} {
::opunk::Console::size [instance $name]
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === ===
tcl::namespace::eval ::opunk::console {
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS [list {
@id -id "(package)opunk::console"
@package -name "opunk::console" -help\
"voo class ::opunk::Console representing a console as an in/out channel pair with settled
response-capability facts, plus anchored canonical instances under ::opunk::console::instances."
}]
namespace eval argdoc {
proc package_name {} {
return opunk::console
}
proc about_topics {} {
set topic_funs [info commands [namespace current]::get_topic_*]
set about_topics [list]
foreach f $topic_funs {
set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end]
}
return [lsort $about_topics]
}
proc default_topics {} {return [list Description *]}
proc get_topic_Description {} {
punk::args::lib::tstr [string trim {
package opunk::console
voo class for consoles as channel-pair objects with anchored canonical instances.
} \n]
}
proc get_topic_License {} {
return "BSD"
}
proc get_topic_Version {} {
return "$::opunk::console::version"
}
proc get_topic_Contributors {} {
set authors {{"Julian Noble" "julian@precisium.com.au"}}
set contributors ""
foreach a $authors {
append contributors $a \n
}
if {[string index $contributors end] eq "\n"} {
set contributors [string range $contributors 0 end-1]
}
return $contributors
}
proc get_topic_classes {} {
punk::args::lib::tstr -return string {
opunk::Console "virtual class for consoles as in/out channel pairs"
}
}
}
set overrides [dict create]
dict set overrides @id -id "::opunk::console::about"
dict set overrides @cmd -name "opunk::console::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About opunk::console
}] \n]
dict set overrides topic -choices [list {*}[opunk::console::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1
dict set overrides topic -default [opunk::console::argdoc::default_topics]
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
lappend PUNKARGS [list $newdef]
proc about {args} {
package require punk::args
set argd [punk::args::parse $args withid ::opunk::console::about]
lassign [dict values $argd] _leaders opts values _received
punk::args::package::standard_about -package_about_namespace ::opunk::console::argdoc {*}$opts {*}[dict get $values topic]
}
}
# end of sample 'about' function
# == === === === === === === === === === === === === === ===
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
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 ::opunk::Console
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide opunk::console [tcl::namespace::eval ::opunk::console {
variable pkg opunk::console
variable version
set version 0.1.1
}]
return

4761
src/vfs/_vfscommon.vfs/modules/punk/console-0.1.5.tm

File diff suppressed because it is too large Load Diff

4223
src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.5.tm

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save