Browse Source

repl shutdown/eof fixes, PUNK_PIPE_EOF policy, console query guards, opunk::console voo class, test framework fixes

punk::repl (0.1.4):
- cancel the deferred (after idle) stdin reader registration when the repl finishes - a stale
  registration could re-attach a reader to a completed repl (piped stdin racing shutdown produced
  'error2 in repl_process_data: invalid thread handle ""')
- repl_process_data drops input arriving after codethread teardown instead of erroring
- repl::init constructs the process-default console object (anchored opunk::console instance
  'default') and settles can_respond 1 for interactive stdin

app-punkshell:
- eof on the input channel now follows env(PUNK_PIPE_EOF): exit | interactive | unset=heuristic
  (reopen console if available, else terminate) - replaces the unconditional CONIN$/dev-tty repl
  restart that hung automated callers forever; documented in punk::config punk_env_vars_config
  ('help env')

punk::console (0.1.4):
- input_at_eof (certainty, with non-blocking probe read for deferred pipe eof) and
  is_input_console_or_tty (heuristic incl. mintty-without-winpty env hints)
- get_size skips ANSI cursor-report mechanisms for non-terminal input (tput then 80x24 default)
- removed erroneous repl-restart attempts from get_ansi_response_payload eof handling (re-entrancy,
  bypassed eof policy; one call was a typo repl::reopen)
- console_spec_resolve: -console (and legacy positional inoutchannels) accepts a channel pair, an
  anchored opunk::console instance name, or an ::opunk::Console object value; settled can_respond 0
  refuses query emission and supplies the object's default size; default_console proc + auto-attach
  of the anchored 'default' instance for matching channel pairs

opunk::console (new, 0.1.0):
- voo-based -virtual class ::opunk::Console: console as an in/out channel pair with settled
  capability facts; canonical instances anchored at ::opunk::console::instances::<name> with
  value semantics as snapshots; subclass dispatch via slot-0 tag for future channel-environment
  consoles (ssh, tk-widget terminals); added to bootsupport (with voo) for boot-level use

tests:
- src/tests/shell/testsuites/punkexe: hang-proof punk_run/punk_run_expect_alive harness; termination
  guards, exitcode propagation, help-env render with piped stdin, default-console smoke test,
  PUNK_TEST_GOALS-gated interactive drop-in goal tests
- src/tests/modules/punk/console: probe helper and console-spec/integration suites
- src/tests/modules/opunk/console: Console class suite incl. virtual dispatch
- runtests.tcl testinterp: 'package prefer latest' (dev 999999 alpha modules no longer shadowed by
  stable bootsupport copies) and src/vendormodules on the module path (voo etc resolvable)

AGENTS: punk::args usage policy, -console migration contract, bootsupport update workflow
correction, opunk voo conventions, testinterp contracts

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 weeks ago
parent
commit
f0c712543a
  1. 2
      AGENTS.md
  2. 3
      src/bootsupport/AGENTS.md
  3. 2
      src/bootsupport/modules/include_modules.config
  4. 361
      src/bootsupport/modules/opunk/console-0.1.0.tm
  5. 2
      src/bootsupport/modules/punk/config-0.1.tm
  6. 4618
      src/bootsupport/modules/punk/console-0.1.3.tm
  7. 4645
      src/bootsupport/modules/punk/console-0.1.4.tm
  8. 4201
      src/bootsupport/modules/punk/repl-0.1.3.tm
  9. 4222
      src/bootsupport/modules/punk/repl-0.1.4.tm
  10. 768
      src/bootsupport/modules/voo-1.0.0.tm
  11. 2
      src/lib/AGENTS.md
  12. 77
      src/lib/app-punkshell/punkshell.tcl
  13. 3
      src/modules/AGENTS.md
  14. 36
      src/modules/opunk/AGENTS.md
  15. 361
      src/modules/opunk/console-999999.0a1.0.tm
  16. 3
      src/modules/opunk/console-buildversion.txt
  17. 1
      src/modules/punk/AGENTS.md
  18. 2
      src/modules/punk/config-0.1.tm
  19. 239
      src/modules/punk/console-999999.0a1.0.tm
  20. 9
      src/modules/punk/console-buildversion.txt
  21. 48
      src/modules/punk/repl-999999.0a1.0.tm
  22. 5
      src/modules/punk/repl-buildversion.txt
  23. 2
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config
  24. 361
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/opunk/console-0.1.0.tm
  25. 2
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm
  26. 4618
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.3.tm
  27. 4645
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.4.tm
  28. 4201
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.3.tm
  29. 4222
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.4.tm
  30. 768
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/voo-1.0.0.tm
  31. 2
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config
  32. 361
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/opunk/console-0.1.0.tm
  33. 2
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm
  34. 4618
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.3.tm
  35. 4645
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.4.tm
  36. 4201
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.3.tm
  37. 4222
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.4.tm
  38. 768
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/voo-1.0.0.tm
  39. 4
      src/tests/AGENTS.md
  40. 256
      src/tests/modules/opunk/console/testsuites/console/consoleclass.test
  41. 192
      src/tests/modules/punk/console/testsuites/console/consolespec.test
  42. 210
      src/tests/modules/punk/console/testsuites/console/probes.test
  43. 9
      src/tests/runtests.tcl
  44. 6
      src/tests/shell/AGENTS.md
  45. 383
      src/tests/shell/testsuites/punkexe/shellexit.test
  46. 77
      src/vfs/_vfscommon.vfs/lib/app-punkshell/punkshell.tcl
  47. 361
      src/vfs/_vfscommon.vfs/modules/opunk/console-0.1.0.tm
  48. 2
      src/vfs/_vfscommon.vfs/modules/punk/config-0.1.tm
  49. 4520
      src/vfs/_vfscommon.vfs/modules/punk/console-0.1.2.tm
  50. 4618
      src/vfs/_vfscommon.vfs/modules/punk/console-0.1.3.tm
  51. 4645
      src/vfs/_vfscommon.vfs/modules/punk/console-0.1.4.tm
  52. 4201
      src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.3.tm
  53. 4222
      src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.4.tm

2
AGENTS.md

@ -87,7 +87,7 @@ When the user requests a durable behavior change, record it here or in the relev
- `src/modules/` — Main editable module source (see src/modules/AGENTS.md)
- `src/modules/punk/` — Core punk namespace modules (see src/modules/punk/AGENTS.md)
- `src/modules/test/` — Installed-module test packages (see src/modules/test/AGENTS.md)
- `src/modules/opunk/` — Alternative punk namespace
- `src/modules/opunk/` — Alternative punk namespace, voo-based classes (see src/modules/opunk/AGENTS.md)
- `src/modules/punkcheck/` — Build/check system
- `src/modules_tcl8/` — Tcl 8 specific modules (see src/modules_tcl8/AGENTS.md)
- `src/modules_tcl9/` — Tcl 9 specific modules (see src/modules_tcl9/AGENTS.md)

3
src/bootsupport/AGENTS.md

@ -8,7 +8,8 @@ Modules and libraries required during the build/bootstrap/make process before th
- Agents should not directly modify files in this tree unless the task specifically targets boot behaviour.
- Bootsupport modules are snapshots that may lag behind or diverge from the corresponding `src/modules/` versions intentionally.
- The `dev lib.copyasmodule` command is the prescribed way to update bootsupport modules.
- The systematic update workflow is `modules/include_modules.config` (entries are `<base> <module>` pairs; base `src/vendormodules` for vendored modules, base `modules` for the project's *built* root modules - build with `make.tcl modules` first) followed by `tclsh src/make.tcl bootsupport`, which also propagates snapshots into the project-layout bootsupport trees.
- `dev lib.copyasmodule` is a manual convenience for copying a module (or a single-file pkgIndex.tcl library, converting it to .tm form) from a running punk shell - see `modules/README.md`; it is not the primary update mechanism.
## Local Contracts

2
src/bootsupport/modules/include_modules.config

@ -39,6 +39,7 @@ set bootsupport_modules [list\
src/vendormodules textutil::tabify\
src/vendormodules textutil::trim\
src/vendormodules textutil::wcswidth\
src/vendormodules voo\
src/vendormodules uuid\
modules argp\
modules flagfilter\
@ -107,6 +108,7 @@ set bootsupport_modules [list\
modules textblock\
modules natsort\
modules oolib\
modules opunk::console\
modules zipper\
modules zzzload\
]

361
src/bootsupport/modules/opunk/console-0.1.0.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.0
# 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).
#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.
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.0
}]
return

2
src/bootsupport/modules/punk/config-0.1.tm

@ -244,6 +244,7 @@ tcl::namespace::eval punk::config {
syslog_active 0
auto_exec_mechanism exec
auto_noexec 0
pipe_eof {}
} xdg_config_home $default_xdg_config_home {*}{
} xdg_data_home $default_xdg_data_home {*}{
} xdg_cache_home $default_xdg_cache_home {*}{
@ -284,6 +285,7 @@ tcl::namespace::eval punk::config {
PUNK_LOGFILE_STDOUT {type string}
PUNK_LOGFILE_STDERR {type string}
PUNK_LOGFILE_ACTIVE {type string}
PUNK_PIPE_EOF {type string default "" help "punk shell behaviour when its input channel\n(usually piped/redirected stdin) reaches eof.\n exit - always terminate\n interactive - reopen console, interactive repl\n unset/other - console if available, else exit"}
PUNK_SYSLOG_STDOUT {type string}
PUNK_SYSLOG_STDERR {type string}
PUNK_SYSLOG_ACTIVE {type string}

4618
src/bootsupport/modules/punk/console-0.1.3.tm

File diff suppressed because it is too large Load Diff

4645
src/bootsupport/modules/punk/console-0.1.4.tm

File diff suppressed because it is too large Load Diff

4201
src/bootsupport/modules/punk/repl-0.1.3.tm

File diff suppressed because it is too large Load Diff

4222
src/bootsupport/modules/punk/repl-0.1.4.tm

File diff suppressed because it is too large Load Diff

768
src/bootsupport/modules/voo-1.0.0.tm

@ -0,0 +1,768 @@
#https://github.com/AlaoPrado/voo
#License: MIT
namespace eval voo {
# package version
variable version 1.0.0
variable handlerToObjectMap {}
variable handlerCounter 0
##\brief Check if a namespace is a valid voo class
# \param[in] namespaceName the namespace to check
# \return 1 if valid voo class, 0 otherwise
proc isVooClass {namespaceName} {
if {![uplevel [list namespace exists $namespaceName]]} {
return 0
}
return [expr {[uplevel [list namespace eval $namespaceName {
info exists __defaultObj
}]]}]
}
##\brief Declare a new voo class namespace and process its class body
# \param[in] args Arguments for class declaration: <className> <body> and optional -extends parent
# \note Creates the class namespace, imports parent fields/methods when using -extends,
# and registers constructors and exports
proc class {args} {
set optDict {}
set defaultArgs {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-extends"} {
if {$i + 1 >= $numArgs} {
error "Constructor option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} elseif {$arg eq "-virtual" || $arg eq "-v"} {
dict set optDict "-virtual" {}
} else {
lappend defaultArgs $arg
}
}
lassign $defaultArgs className body
set vooNs [namespace current]
# create the namespace for the class
uplevel [list namespace eval $className [subst -nocommands {
namespace path [list $vooNs]
variable __defaultObj {}
variable __fields {}
variable __tmp_isPublicEnabled 1
}]]
uplevel [list namespace eval $className {
##\brief Access default object for this class
# \return Default class instance (list)
# \note Used for inheritance and constructor defaults
proc class.defaultObj {} {
variable __defaultObj
return $__defaultObj
}
##\brief Get list of field names for this class
# \return List of field names in declaration order
# \note Useful for introspection and constructor -name new.args
proc class.fields {} {
variable __fields
return $__fields
}
}]
if {[dict exists $optDict -virtual] && [dict exists $optDict -extends]} {
error "voo::class: cannot use -virtual with -extends; child classes inherit virtual automatically from a -virtual parent"
}
if {[dict exists $optDict -virtual]} {
set normalizedClassName [uplevel [list namespace eval $className {namespace current}]]
uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]]
uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedClassName]]
# Pre-populate __defaultObj with namespace tag at index 0 BEFORE field declarations
# so that _getClassCurrNumFields returns 1 for the first field declared
uplevel [list namespace eval $className [list set __defaultObj [list $normalizedClassName]]]
}
#81
# variable __parentClassNamespace {}
if {[dict exists $optDict -extends]} {
set parentClassName [dict get $optDict -extends]
if {![uplevel [list namespace exists $parentClassName]]} {
error "Parent class ’$parentClassName’ does not exist."
}
# check if parent class exists
if {![uplevel [list namespace eval $parentClassName {info exists __defaultObj}]]} {
error "Parent class ’$parentClassName’ is not a valid voo class."
}
# normalize namespace name of parent class
set parentClassName [uplevel [list namespace eval $parentClassName {
namespace current
}]]
uplevel [list namespace eval $className [subst -nocommands {
variable __parentClassNamespace $parentClassName
}]]
# import parent’s default object values
set parentDefaultObj [${parentClassName}::class.defaultObj]
uplevel [list namespace eval $className [list set __defaultObj $parentDefaultObj]]
# if parent is virtual, update namespace tag at index 0 to child’s namespace
set parentIsVirtual [uplevel [list namespace eval $parentClassName {info exists __voo_is_virtual_class}]]
if {$parentIsVirtual} {
set normalizedChildName [uplevel [list namespace eval $className {namespace current}]]
uplevel [list namespace eval $className \
[list set __defaultObj [lreplace $parentDefaultObj 0 0 $normalizedChildName]]]
uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]]
uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedChildName]]
}
# 121
# import parent’s field index variables by copying actual index values from parent
set parentFields [${parentClassName}::class.fields]
foreach field $parentFields {
set fieldIdx [uplevel [list namespace eval $parentClassName [list set $field]]]
uplevel [list namespace eval $className [list variable $field $fieldIdx]]
uplevel [list namespace eval $className [list lappend __fields $field]]
}
# import parent’s acessors in child class with namespace import
uplevel [list namespace eval $className [subst -nocommands {
namespace import ${parentClassName}::get.*
namespace import ${parentClassName}::set.*
namespace import ${parentClassName}::update.*
}]]
}
# 136
uplevel [list namespace eval $className $body]
uplevel [list namespace eval $className {
if {[info commands new] eq ""} {
constructor
}
if {[info commands new()] eq ""} {
constructor -noargs [_buildConstructorNoArgsBody]
}
if {[info commands new.args] eq ""} {
constructor -name new.args {args} [_buildConstructorArgsBody]
}
}]
# 151
uplevel [list namespace eval $className {
# export class methods
namespace export *
}]
uplevel [list namespace eval $className {
# clean temporary variable
unset __tmp_isPublicEnabled
}]
return
}
# 161
##\brief Return the default value for a given field type
# \param[in] type the field type token (double,int,bool,...)
# \return The default value appropriate for the type
proc _getDefaultValueByType {type} {
switch -- $type {
double { return 0.0 }
int { return 0 }
bool { return 0 }
default { return {} }
}
}
##\brief Get the current number of fields declared in the current class
# \return Number of fields (integer)
proc _getClassCurrNumFields {} {
return [uplevel 2 {llength $__defaultObj}]
}
##\brief Check whether public mode is enabled during class body parsing
# \return 1 if public mode is enabled, 0 otherwise
proc _getClassIsPublicEnabled {} {
return [uplevel 2 {set __tmp_isPublicEnabled}]
}
##\brief Declare getter/setter/updater accessors for a class field
# \param[in] fieldName name of the field
# \param[in] isPublic boolean whether accessors are public
# \param[in] isStatic boolean whether field is static (class-level)
proc _declareFieldAcessors {fieldName isPublic isStatic} {
set prefix {}
if {$isStatic} {
append prefix class.
}
if {!$isPublic} {
append prefix my.
}
set getterName "${prefix}get.$fieldName"
set setterName "${prefix}set.$fieldName"
set updaterName "${prefix}update.$fieldName"
if {$isStatic} {
uplevel 2 [list proc $getterName {} [subst -nocommands {
variable $fieldName
return $$fieldName
}]]
uplevel 2 [list proc $setterName {value} [subst -nocommands {
variable $fieldName
set $fieldName "\$value"
}]]
uplevel 2 [list proc $updaterName {tempVar body} [subst -nocommands {
variable $fieldName
upvar "\$tempVar" temp
set temp $$fieldName
# break link with class variable to avoid copy-on-write
set $fieldName {}
try {
uplevel \$body
} finally {
set $fieldName "\$temp"
}
}]]
} else {
uplevel 2 [list getter $getterName $fieldName]
uplevel 2 [list setter $setterName $fieldName]
uplevel 2 [list updater $updaterName $fieldName]
}
return
}
##\brief Validate a field name for illegal characters
# \param[in] fieldName the field name to validate
# \return Raises an error if invalid
proc _validateFieldName {fieldName} {
if {[string first "." $fieldName] != -1 || [string first "::" $fieldName] != -1} {
error "Field name ’$fieldName’ cannot contain ’.’ or ’::’ substrings."
}
}
##\brief Ensure a field name does not already exist in the class
# \param[in] fieldName the field name to check
# \return Raises an error if the field already exists
# \note Uses __fields for instance fields and fully-qualified namespace lookup for static
# fields to avoid false positives from global variables with the same name
proc _validateFieldDoesNotExist {fieldName} {
# Check instance fields tracked in __fields (class-scoped, no global bleed)
if {$fieldName in [uplevel 2 {set __fields}]} {
error "Field name ’$fieldName’ already exists in the class."
}
# Check static fields via fully-qualified namespace variable; info exists ::Ns::var
# only matches that exact namespace variable, never a same-named global
set classNs [uplevel 2 {namespace current}]
if {[info exists ${classNs}::$fieldName]} {
error "Field name ’$fieldName’ already exists in the class."
}
}
##\brief Validate a variable initial value according to its declared type
# \param[in] type the declared type (double,int,bool,list,dict)
# \param[in] value the value to validate
# \return Raises an error if the value does not match the type
proc _validateVarValueByType {type value} {
switch -- $type {
double {
if {[string is double -strict $value] == 0} {
error "Value for t_double must be a double, got ’$value’"
}
}
int {
if {[string is integer -strict $value] == 0} {
error "Value for t_int must be an integer, got ’$value’"
}
}
bool {
if {[string is boolean -strict $value] == 0} {
error "Value for t_bool must be a boolean, got ’$value’"
}
}
list {
if {[catch {llength $value}]} {
error "Value for t_list must be a list, got ’$value’"
}
}
dict {
if {[catch {dict size $value}]} {
error "Value for t_dict must be a dict, got ’$value’"
}
}
}
}
##\brief Declare a field variable inside the class body
# \param[in] type the field type token (double,int,string,bool,list,dict,obj)
# \param[in] argList arguments: ?-static? <name> ?<initialValue>?
proc _var {type argList} {
set defaultArgs {}
set optDict {}
set numArgs [llength $argList]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $argList $i]
if {$arg eq "-static"} {
dict set optDict $arg {}
} else {
lappend defaultArgs $arg
}
}
if {[llength $defaultArgs] == 0} {
error "Variable definition requires: ?<option>? <name> ?<initialValue>?"
}
if {[llength $defaultArgs] == 2} {
lassign $defaultArgs name initVal
} else {
lassign $defaultArgs name
set initVal [_getDefaultValueByType $type]
}
_validateFieldName $name
_validateFieldDoesNotExist $name
_validateVarValueByType $type $initVal
if {[dict exists $optDict -static]} {
# static field
uplevel [list variable $name $initVal]
} else {
set currNumFields [_getClassCurrNumFields]
uplevel [list variable $name $currNumFields]
uplevel [list lappend __defaultObj $initVal]
uplevel [list lappend __fields $name]
}
set isPublicEnabled [_getClassIsPublicEnabled]
_declareFieldAcessors $name $isPublicEnabled [dict exists $optDict -static]
return
}
# 341
##\brief Declare a double-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc double_t {args} {
uplevel [list _var "double" $args]
}
##\brief Declare an integer-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc int_t {args} {
uplevel [list _var "int" $args]
}
##\brief Declare a string-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc string_t {args} {
uplevel [list _var "string" $args]
}
##\brief Declare a boolean-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc bool_t {args} {
uplevel [list _var "bool" $args]
}
##\brief Declare a list-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc list_t {args} {
uplevel [list _var "list" $args]
}
##\brief Declare a dict-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc dict_t {args} {
uplevel [list _var "dict" $args]
}
##\brief Declare an object-typed field (nested vanilla object)
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc obj_t {args} {
uplevel [list _var "object" $args]
}
# 386
##\brief Enable public mode for declarations inside the provided body
# \param[in] body script to execute with public accessors enabled
# \return Result of executing body
proc public {body} {
uplevel $body
}
##\brief Execute the provided body with private mode enabled (temporarily disables public accessors)
# \param[in] body script to execute with private accessors
# \return Result of executing body
proc private {body} {
uplevel {variable __tmp_isPublicEnabled 0}
try {
uplevel $body
} finally {
uplevel {variable __tmp_isPublicEnabled 1}
}
}
##\brief Build the body for a no-argument constructor
# \return A script chunk used as constructor body that returns the class default object
proc _buildConstructorNoArgsBody {} {
return {
variable __defaultObj
return $__defaultObj;
}
}
##\brief Build the body for a constructor that accepts named args (-field value pairs)
# \return A script chunk used as constructor body that applies named arguments to the default object
proc _buildConstructorArgsBody {} {
return {
variable __defaultObj
set obj $__defaultObj
if {[catch {dict size $args}]} {
error "Constructor argument must be a list of ’-<field> <value>’ pairs"
}
dict for {key value} $args {
if {[string index $key 0] ne "-"} {
error "Constructor argument keys must start with ’-’, got ’$key’"
}
set field [string range $key 1 end]
set setter set.$field
if {[info commands $setter] ne ""} {
$setter obj $value
} else {
set setter my.set.$field
if {[info commands $setter] ne ""} {
$setter obj $value
} else {
error "Unknown field option: $field"
}
}
}
return $obj
}
}
##\brief Build constructor parameter list and body for positional constructors
# \return A list of two elements: argument names list and a body script that returns them as a list
# \note For virtual classes, the concrete class namespace is embedded as a literal string at
# class-definition time (not looked up at runtime), producing:
# return [list ::ClassName $f1 $f2 ...]
# This avoids all runtime proc calls (class.defaultObj, set.*) and variable lookups,
# making virtual object creation as cheap as non-virtual.
proc _buildConstructorParams {} {
set argList [uplevel 2 {set __fields}]
set isVirtual [uplevel 2 {info exists __voo_is_virtual_class}]
set spacedArgVarListStr {}
foreach arg $argList {
append spacedArgVarListStr "\$$arg "
}
if {$isVirtual} {
# Read the normalized class namespace at definition time so subst embeds it
# as a literal in the generated body - no runtime variable lookup required.
set classNs [uplevel 2 {set __voo_class_namespace}]
set spacedArgVarListStr "{$classNs} $spacedArgVarListStr"
set body [subst -nocommands {
return [list $spacedArgVarListStr]
}]
} else {
set body [subst -nocommands {
return [list $spacedArgVarListStr]
}]
}
return [list $argList $body]
}
##\brief Define a constructor for the current class
# \param[in] args Constructor declaration options and body
# \note Supports -name, -noargs and -typed variants
proc constructor {args} {
set defaultArgs {}
set optDict {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-name" || $arg eq "-noargs" || $arg eq "-typed"} {
if {$i + 1 >= $numArgs} {
error "Constructor option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} else {
lappend defaultArgs $arg
}
}
# check valid option combinations
if {[dict exists $optDict -name]} {
if {[dict exists $optDict -noargs] || [dict exists $optDict -typed]} {
error "Constructor cannot have -name option with -noargs or -typed options"
}
}
if {[dict exists $optDict -noargs] && [dict exists $optDict -typed]} {
error "Constructor cannot have both -noargs and -typed options"
}
if {[dict exists $optDict -name]} {
set constructorName [dict get $optDict -name]
} elseif {[dict exists $optDict -noargs]} {
set constructorName "new()"
} elseif {[dict exists $optDict -typed]} {
set constructorName "new([join [dict get $optDict -typed] ,])"
} else {
set constructorName "new"
}
if {[dict exists $optDict -noargs]} {
if {[llength $defaultArgs] != 0} {
error "Invalid constructor definition, expected ’?...? ?<body>?’ for -noargs"
}
set argList {}
set body [dict get $optDict -noargs]
} else {
if {[llength $defaultArgs] == 0} {
lassign [_buildConstructorParams] argList body
} else {
if {[llength $defaultArgs] != 2} {
error "Invalid constructor definition, expected ’?...? ?<argList> <body>?’"
}
lassign $defaultArgs argList body
}
}
uplevel [list proc $constructorName $argList $body]
return
}
# 531
##\brief Generate a getter procedure for a field
# \param[in] methodName name of the generated getter (may include namespace prefix)
# \param[in] fieldName name of the field to read
proc getter {methodName fieldName} {
# implementation of getter definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Getter for $fieldName
# \\param\[in\] this class instance
# \\return $fieldName value
proc $methodName {this} {
return [lindex \$this $fieldIdx]
}
}]
return
}
##\brief Generate a setter procedure for a field
# \param[in] methodName name of the generated setter (may include namespace prefix)
# \param[in] fieldName name of the field to write
proc setter {methodName fieldName} {
# implementation of setter definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Setter for $fieldName
# \\param\[in\] thisVar name of variable containing class instance
# \\param\[in\] value new value for $fieldName
proc $methodName {thisVar value} {
upvar \$thisVar this
lset this $fieldIdx \$value
}
}]
return
}
##\brief Generate an updater procedure for a field (copy-on-write safe)
# \param[in] methodName name of the generated updater (may include namespace prefix)
# \param[in] fieldName name of the field to update by reference
# \note The updater detaches the field to avoid unnecessary copying during updates
proc updater {methodName fieldName} {
# implementation of updater definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Update $fieldName by reference
# \\param\[in\] thisVar name of variable containing class instance
# \\param\[out\] tempVar name of variable to hold $fieldName during update
# \\param\[in\] body script to execute with $fieldName in tempVar
# \\note Avoids copy-on-write by detaching field during update
proc $methodName {thisVar tempVar body} {
upvar \$thisVar this
upvar \$tempVar temp
set temp [lindex \$this $fieldIdx]
# break link with object to avoid copy-on-write
lset this $fieldIdx {}
try {
uplevel \$body
} finally {
lset this $fieldIdx \$temp
}
}
}]
}
##\brief Declare a method in the current class namespace
# \param[in] args Method declaration arguments: name, argList, body and options (-static, -upvar, -update, -override)
proc method {args} {
set isPublicEnabled [_getClassIsPublicEnabled]
set defaultArgs {}
set optDict {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-static" || $arg eq "-upvar"} {
dict set optDict $arg {}
} elseif {$arg eq "-update"} {
if {$i + 1 >= $numArgs} {
error "Method option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} elseif {$arg eq "-override"} {
# Explicit override indicator
dict set optDict $arg {}
} elseif {$arg eq "-virtual"} {
dict set optDict $arg {}
} else {
lappend defaultArgs $arg
}
}
lassign $defaultArgs name argList body
# check valid option combinations
if {[dict exists $optDict -static]} {
if {[dict exists $optDict -upvar] || [dict exists $optDict -update]} {
error "Method cannot have both -static and -upvar or -update options"
}
}
if {[dict exists $optDict -update]} {
if {![dict exists $optDict -upvar]} {
# automatically add -upvar if -update is specified
dict set optDict -upvar {}
}
}
set finalArgList {}
set finalBody {}
if {[dict exists $optDict -upvar]} {
lappend finalArgList "thisVar"
append finalBody {
upvar $thisVar this
}
} elseif {![dict exists $optDict -static]} {
lappend finalArgList "this"
}
lappend finalArgList {*}$argList
set className [uplevel {namespace current}]
if {[dict exists $optDict -update]} {
set updateFields [dict get $optDict -update]
if {[llength $updateFields] == 0} {
error "-update option requires at least one field name"
}
foreach field $updateFields {
try {
set fieldIdx [uplevel [list set $field]]
} trap {} {} {
error "Field ’$field’ specified in -update option does not exist in class ’$className’"
}
append finalBody [subst -nocommands {
set $field [lindex \$this $fieldIdx]
lset this $fieldIdx {}
}]
}
append finalBody "try \{"
}
append finalBody $body
if {[dict exists $optDict -update]} {
append finalBody "\} finally \{"
foreach field $updateFields {
set fieldIdx [uplevel [list set $field]]
append finalBody [subst -nocommands {
lset this $fieldIdx \$$field
}]
}
append finalBody "\}"
}
if {!$isPublicEnabled} {
set name "my.$name"
}
if {[dict exists $optDict -override]} {
set parentNs [uplevel {set __parentClassNamespace}]
if {[info commands "${parentNs}::$name"] eq ""} {
error "Method ’$name’ does not override any method in parent class ’$parentNs’"
}
# If parent’s method is virtual (has base.<name>), auto-promote this override
# to a dispatcher so that deep inheritance dispatch works correctly
if {[uplevel {info exists __voo_is_virtual_class}] && \
[info commands "${parentNs}::base.$name"] ne ""} {
dict set optDict -virtual {}
}
}
if {[dict exists $optDict -virtual]} {
if {![uplevel {info exists __voo_is_virtual_class}]} {
error "Method ’$name’ is declared -virtual but ’[uplevel {namespace current}]’ is not a virtual class"
}
if {[dict exists $optDict -upvar] || [dict exists $optDict -update] || [dict exists $optDict -static]} {
error "Method ’$name’ cannot combine -virtual with -upvar, -update, or -static"
}
# Register base.<name> with the original body for direct parent calls from subclasses
uplevel [list proc "base.$name" $finalArgList $finalBody]
# Build dispatch body: route to concrete class implementation at runtime
set dispatchBody "set __voo_cls \[lindex \$this 0\]\n"
append dispatchBody "if \{\$__voo_cls ne \[namespace current\] && \[info commands \${__voo_cls}::$name\] ne {}\} \{\n"
append dispatchBody " return \[\${__voo_cls}::$name \$this"
foreach arg $argList {
append dispatchBody " \$$arg"
}
append dispatchBody "\]\n\}\n"
append dispatchBody "return \[base.$name \$this"
foreach arg $argList {
append dispatchBody " \$$arg"
}
append dispatchBody "\]"
set finalBody $dispatchBody
}
uplevel [list proc $name $finalArgList $finalBody]
return
}
# 726
##\brief Import one or more methods from parent class into the current (child) class namespace.
# \param[in] methods List of method names (or a single method name) to import from parent.
# \note Must be called inside a class declared with -extends. Methods are copied at class-definition time.
proc importMethods {methods} {
set parentNs [uplevel {set __parentClassNamespace}]
# Validate caller context and get parent namespace stored by -extends handling
if {$parentNs eq ""} {
error "importMethods can only be used inside a class declared with -extends"
}
# Normalize to a list of method names
if {[string length [string trim $methods]] == 0} {
return
}
if {[catch {llength $methods}]} {
set methodList [list $methods]
} else {
set methodList $methods
}
foreach methodName $methodList {
set fullMethodName "${parentNs}::$methodName"
# Validate parent method exists
if {[info commands $fullMethodName] eq ""} {
error "Method ’$methodName’ not found in parent class ’$parentNs’"
}
# Define a copy in the child namespace so unqualified calls resolve to child
set argList [info args $fullMethodName]
set body [info body $fullMethodName]
uplevel [list proc $methodName $argList $body]
}
return
}
namespace export *
}
# provide the package
package provide voo $::voo::version

2
src/lib/AGENTS.md

@ -15,7 +15,7 @@ Source of truth for all editable `pkgIndex.tcl`-based library packages. These ar
- If a request starts from `pkgIndex.tcl`, resolve the mapped implementation file before analyzing or editing a proc.
- Libraries here include:
- `app-punk/` — Punk REPL app entry (`app-punk::repl`)
- `app-punkshell/` — Punkshell app entry (`app-punkshell`)
- `app-punkshell/` — Punkshell app entry (`app-punkshell`); on eof of its input channel the `env(PUNK_PIPE_EOF)` policy (`exit`|`interactive`|unset=console heuristic) decides between terminating and reopening the console for an interactive repl — automated callers should set `exit` or end piped input with an explicit `exit`
- `app-shellspy/` — ShellSpy app entry (`app-shellspy`)
- `app_shell/` — Shell app helpers (`app_shell`)
- `app_shellrun/` — Shell run helpers (`app_shellrun`)

77
src/lib/app-punkshell/punkshell.tcl

@ -465,29 +465,59 @@ dict with prevglobal {}
}
eof {
puts "app-punkshell eof on input channel [lindex $result 1]"
#eof on the input channel - usually piped/redirected stdin reaching its end (also e.g ctrl-z/ctrl-d at a console)
#Historical behaviour is to reopen the console and drop into an interactive repl - desirable when a user is
#present, but a permanent hang for automated callers (CI/agents/exec) since nobody will ever type anything.
#Policy selected via env(PUNK_PIPE_EOF) (documented in punk::config punk_env_vars_config - see 'help env'):
# exit - always terminate
# interactive - always attempt the console reopen (terminates exitcode 1 if no console can be opened)
# unset/other - heuristic: attempt the console reopen, terminate cleanly if no console is available
#Note the heuristic cannot detect a console that exists but has no user watching (some CI/agent harnesses) -
#automated callers should set PUNK_PIPE_EOF=exit (or ensure their piped input ends with an explicit exit).
#review - propagate a nonzero exitcode if errors occurred in the piped input?
#review - should this consult the punk::config running configuration (pipe_eof) rather than raw ::env?
set eof_policy heuristic
if {[info exists ::env(PUNK_PIPE_EOF)]} {
switch -exact -- [string tolower $::env(PUNK_PIPE_EOF)] {
exit - quit - terminate {
set eof_policy exit
}
interactive - repl - shell {
set eof_policy interactive
}
}
}
if {$eof_policy eq "exit"} {
exit 0
}
if {$::tcl_platform(platform) eq "windows"} {
#set console_input_source "CON"
set console_input_source {CONIN$}
} else {
#/dev/tty - reference to the controlling terminal for a process
#review/test
set console_input_source "/dev/tty"
}
if {[catch {open $console_input_source r} s]} {
puts stderr "app-punkshell: no console available for interactive shell after eof on input channel ($console_input_source: $s)"
if {$eof_policy eq "interactive"} {
exit 1
}
exit 0
}
set errdeviceinfo [shellfilter::stack::new punkshellerr -settings [list -tag "punkshellerr" -buffering none -raw 1 -syslog {} -file {}]]
set outdeviceinfo [shellfilter::stack::new punkshellout -settings [list -tag "punkshellout" -buffering none -raw 1 -syslog {} -file {}]]
#reopen stdin as console???
if {$::tcl_platform(platform) eq "windows"} {
#set s [open "CON" r]
set s [open {CONIN$} r]
if {[package provide twapi] ne ""} {
set h [twapi::get_tcl_channel_handle $s in]
twapi::SetStdHandle -10 $h
}
puts stderr "restarting repl on inputchannel:$s"
#return [repl::start $s -title "reopen_stdin a"]
} else {
#/dev/tty - reference to the controlling terminal for a process
#review/test
set s [open "/dev/tty" r]
}
puts stderr "input chan '$s': [chan configure $s]"
puts stderr "punkshell.tcl restarting repl on inputchannel:$s"
#puts stderr "input chan '$s': [chan configure $s]"
set ::tcl_interactive [punk::repl::is_interactive $s]
puts "app-punkshell: tcl_interactive: $::tcl_interactive"
#puts "app-punkshell: tcl_interactive: $::tcl_interactive"
set exitinfo [punkshell::do_shell $s]
@ -513,8 +543,25 @@ dict with prevglobal {}
}
set result [dict get $exitinfo result]
set firstword [lindex $result 0]
puts stdout "result: $result"
#review
switch -- $firstword {
exit {
exit [lindex $result 1]
}
quit {
puts stdout "result: [lrange $result 1 end]"
exit 0
}
interrupt {
#exit with 128 + signal number - unix-like compatibility.
exit [expr {[lindex $result 1] + 128}]
}
default {
#review - we should get a recognised firstword.
puts stderr "unexpected result from repl after reopened input channel: $result"
flush stdout
exit 1
}
}
}
default {
#review - we should get a recognised firstword.

3
src/modules/AGENTS.md

@ -31,6 +31,7 @@ Source of truth for all editable Punk project modules. This is where agents shou
- Shell modules include `punk::repl`, `punk::shellrun` for `run`/`runout`/`runerr`/`runx`, and `punk::nav::ns` for namespace browsing.
- Functional and pattern modules include `pattern` and `patterncmd`; treat them as experimental and use them only with user instruction or confirmation.
- Argument handling uses `punk::args` for processing, validation, and documentation generation; prefer it for public APIs and user-facing utilities unless the code path is performance critical.
- Performance-critical procs may parse arguments manually, but must still carry an accurate `punk::args` (PUNKARGS) definition for documentation, with a comment in the proc referencing the definition id; use `punk::args::parse` whenever speed isn't critical or manual parsing would be complex.
- Networking modules include `punk::imap4`, `punk::netbox`, `punk::basictelnet`, and `punk::net::vxlan`.
- Plugin and capability provider/handler support uses `punk::cap`.
@ -329,6 +330,6 @@ return
## Child DOX Index
- `punk/` — Core punk namespace modules (see punk/AGENTS.md)
- `opunk/` — Alternative punk namespace (opunk::str)
- `opunk/` — Alternative punk namespace, voo-based classes (opunk::str, opunk::console) (see opunk/AGENTS.md)
- `punkcheck/` — Build/check system (punkcheck + punkcheck::cli)
- `test/` — Installed-module test packages (see test/AGENTS.md)

36
src/modules/opunk/AGENTS.md

@ -0,0 +1,36 @@
# src/modules/opunk — Alternative Punk Namespace (voo-based classes)
## Purpose
Modules under the `opunk::*` namespace explore value-based class implementations of punk facilities using the vendored `voo` OO system (`src/vendormodules/voo-1.0.0.tm`). Objects are plain Tcl list values; class namespaces hold the methods.
## Ownership
- Agents may edit modules here when working on voo-based reimplementations; the corresponding `punk::*` modules remain the production implementations until integration is explicitly done.
- `voo` itself is a vendored dependency — change it via the vendormodules workflow, not here.
## Local Contracts
- Class namespaces are capitalized (`::opunk::Str`, `::opunk::Console`); companion lowercase namespaces (`::opunk::str`, `::opunk::console`) hold package-level procs, PUNKARGS docs and shared state.
- Fields use the `o_` prefix (even though voo private accessors already get a `my.` prefix) to distinguish fields from locals in `-update` method bodies and ease code movement to/from tclOO implementations.
- voo makes each field name a class-namespace variable holding the field's list index — avoid `variable o_<field>` in method bodies.
- Classes intended for behavioral subclassing are declared `-virtual` (slot 0 of each value is the concrete class namespace tag); behaviour-differentiating methods are declared `-virtual` so calls through the base class dispatch on the tag.
- Entity-like objects (e.g. consoles) anchor canonical instances at well-known namespace variables (`::opunk::console::instances::<name>`) with lowercase wrapper procs owning mutation; the value semantics serve as snapshots/copies where cheap.
- Method-entry validity guards use `[llength $this] < N` (not `!= N`) so field-adding subclasses keep working.
- 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`.
## Work Guidance
- Use `::opunk::Str` (str module) as the reference for voo idioms: unshare/restore scripts, `-update` copy-on-write avoidance, explicit list-literal construction in constructors.
- Keep `opunk::console` free of `punk::console` dependencies — integration is planned as punk::console delegating to the class, not the reverse.
- Future capability to preserve: a subshell repl attached to a foreign console (e.g ssh channels, or a tk text widget acting as a terminal). Design rules that protect it: behaviour goes through `-virtual` methods so non-channel subclasses can override everything channel-related; `o_in`/`o_out` are uninterpreted strings (a widget subclass may store widget paths); instances are named-multi-anchor (never a singleton); and punk::console integration must keep migrating operations to object-method dispatch rather than entrenching raw channel operations after spec resolution.
## Verification
- `tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/opunk/<module>/**` passes for the touched module.
- `tclsh src/make.tcl modules` completes without errors after module additions.
## Child DOX Index
(None — flat module files)

361
src/modules/opunk/console-999999.0a1.0.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 999999.0a1.0
# 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).
#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.
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 999999.0a1.0
}]
return

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

@ -0,0 +1,3 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.

1
src/modules/punk/AGENTS.md

@ -28,6 +28,7 @@ Source of truth for all modules under the `punk::*` namespace. This is the prima
## Work Guidance
- New modules under `punk::*` should be created as `<subpath>/<modulename>-999999.0a1.0.tm` following the namespace-to-path convention.
- punk::console is incrementally migrating positional `inoutchannels` arguments to the documented `-console` convention. A `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Converted procs accept the legacy leading positional channel-pair during the transition; convert remaining positional procs opportunistically when touched, with tests.
- Use `punk::args::parse` with `@id` references in `argdoc` namespaces for public API procs.
- Private helpers go in `namespace eval private { ... }` blocks.
- Keep `namespace export` lists alphabetized.

2
src/modules/punk/config-0.1.tm

@ -244,6 +244,7 @@ tcl::namespace::eval punk::config {
syslog_active 0
auto_exec_mechanism exec
auto_noexec 0
pipe_eof {}
} xdg_config_home $default_xdg_config_home {*}{
} xdg_data_home $default_xdg_data_home {*}{
} xdg_cache_home $default_xdg_cache_home {*}{
@ -284,6 +285,7 @@ tcl::namespace::eval punk::config {
PUNK_LOGFILE_STDOUT {type string}
PUNK_LOGFILE_STDERR {type string}
PUNK_LOGFILE_ACTIVE {type string}
PUNK_PIPE_EOF {type string default "" help "punk shell behaviour when its input channel\n(usually piped/redirected stdin) reaches eof.\n exit - always terminate\n interactive - reopen console, interactive repl\n unset/other - console if available, else exit"}
PUNK_SYSLOG_STDOUT {type string}
PUNK_SYSLOG_STDERR {type string}
PUNK_SYSLOG_ACTIVE {type string}

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

@ -290,7 +290,10 @@ namespace eval punk::console {
"Return format"
-console -default {stdin stdout} -type list -help\
"console/terminal (currently list of in/out channels) (todo - object?)"
"Console specification: a 2-element {in out} channel list, an anchored
opunk::console instance name, or an ::opunk::Console object value.
An object with settled can_respond 0 causes an immediate error
rather than emitting a query that cannot be answered."
-passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels {
none
{ ANSI sent without any passthrough wrapping.
@ -347,7 +350,23 @@ namespace eval punk::console {
set query [dict get $values query]
set capturingendregex [dict get $values capturingendregex]
#-console accepts a channel pair, an anchored opunk::console instance name, or an
#::opunk::Console object value - resolve to the canonical channel pair (and object if any)
set cinfo [punk::console::console_spec_resolve $inoutchannels]
set inoutchannels [list [dict get $cinfo in] [dict get $cinfo out]]
lassign $inoutchannels input output
if {[dict get $cinfo object] ne "" && [::opunk::Console::get.o_can_respond [dict get $cinfo object]] == 0} {
error "punk::console::get_ansi_response_payload console object for '$input' '$output' is settled as unable to respond (can_respond=0) - refusing to emit query"
}
#A closed or eof input channel can never deliver a terminal response - fail fast rather than
#emitting the query and waiting for a reply that cannot arrive (e.g piped/redirected stdin that
#has already been consumed). Deliberately only tests certainty of failure (with a probe read for
#deferred pipe eof) - a pipe that is still open could be mintty without winpty presenting a
#terminal's stdin as a pipe (see is_input_console_or_tty)
if {[::punk::console::input_at_eof $input]} {
error "punk::console::get_ansi_response_payload input channel '$input' is closed or at eof - cannot receive terminal response"
}
#chunks from input that need to be handled by readers
upvar ::punk::console::input_chunks_waiting input_chunks_waiting
@ -600,12 +619,12 @@ namespace eval punk::console {
} else {
#! todo? for now, emit a clue as to what's happening.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload cannot trigger existing handler $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} {
if {[eof $input]} {
puts stdout "restarting repl"
repl::reopen_stdin
}
}
#If $input is at eof here, we deliberately take no action - a failed query returns
#empty/timeout to the caller and the active reader will discover the eof through its
#normal read path (repl::start ends with done='eof <chan>' and the application layer
#e.g app-punkshell PUNK_PIPE_EOF policy decides between exit and console reopen).
#Restarting the repl from inside this low-level query primitive caused re-entrancy
#and bypassed that policy.
}
}
#Note - we still may be in_repl_handler here (which disables its own reader while executing commandlines)
@ -618,11 +637,10 @@ namespace eval punk::console {
#The cooperating reader must be designed to consume waiting chunks and only reschedule it's channel read handler once all waiting chunks have been consumed.
#puts stderr "[punk::ansi::a+ green bold]--> repl_handler has chunks to consume [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
#}
if {[eof $input]} {
#test
puts stdout "get_ansi_response_payload experimental - restarting repl"
repl::reopen stdin
}
#If $input is at eof here, we deliberately take no action (previously an erroneous repl
#restart was attempted from this branch). A failed query returns empty/timeout to the caller;
#the repl reader discovers the eof through its normal read path and the application layer
#(e.g app-punkshell PUNK_PIPE_EOF policy) owns the exit-vs-console-reopen decision.
}
# -------------------------------------------------------------------------------------
@ -1531,8 +1549,178 @@ namespace eval punk::console {
variable get_size_mechanism
set get_size_mechanism [dict create]
proc get_size {{inoutchannels {stdin stdout}}} {
lassign $inoutchannels in out
#Determine with certainty whether an input channel is closed or at eof (and so can never deliver
#a terminal response). A pipe that has been fully consumed/half-closed may not have its eof flag set
#until a read is attempted - so for pipe-like channels a non-blocking 1-byte probe read is performed.
#Any byte consumed by the probe is preserved in ::punk::console::input_chunks_waiting($input) for
#cooperating readers (repl etc).
proc input_at_eof {{input stdin}} {
if {[catch {chan eof $input} is_eof]} {
return 1 ;#closed/invalid channel - unusable
}
if {$is_eof} {
return 1
}
if {[catch {chan configure $input} conf]} {
return 1
}
if {[dict exists $conf -inputmode] || [dict exists $conf -mode]} {
#console/tty - not subject to the deferred-eof pipe situation, and we must not consume a
#pending keystroke with a probe read here.
return 0
}
#pipe-like channel - probe without blocking to force eof detection
if {[catch {
set prior_blocking [dict get $conf -blocking]
chan configure $input -blocking 0
set probe [read $input 1]
chan configure $input -blocking $prior_blocking
}]} {
return 1
}
if {$probe ne ""} {
upvar ::punk::console::input_chunks_waiting input_chunks_waiting
lappend input_chunks_waiting($input) $probe
}
return [chan eof $input]
}
#Best effort determination of whether an input channel could be connected to a terminal able to
#answer ANSI queries (cursor position reports etc).
#Returns 0 only when reasonably confident it cannot: channel closed/at eof (probed), or no console/tty
#indicators and no terminal-suggesting environment hints.
#Note: mintty without winpty (e.g git bash) presents a terminal's stdin as a pipe with no -inputmode,
#so absence of console/tty indicators alone must not be treated as 'not a terminal'. Environment hints
#(MSYSTEM, TERM_PROGRAM) are used to avoid false negatives there, at the cost of false positives in
#environments that inherit such vars with genuinely piped (but still open) input - those paths still
#rely on the ANSI query timeout mechanisms rather than hanging indefinitely.
proc is_input_console_or_tty {{input stdin}} {
if {[catch {chan configure $input} conf]} {
return 0
}
if {[dict exists $conf -inputmode]} {
#windows console (and some other terminal environments)
return 1
}
if {[dict exists $conf -mode]} {
#tty on unix-like platforms
return 1
}
if {[input_at_eof $input]} {
return 0
}
if {[info exists ::env(TERM_PROGRAM)] || [info exists ::env(MSYSTEM)]} {
#possibly mintty (or similar) without winpty - terminal presenting channels as pipes
return 1
}
return 0
}
#Resolve a console specification to its channel pair and (optionally) governing object.
#Accepted forms:
# 2-element list - {in out} channel pair (legacy/primitive form)
# 1-element value - name of an anchored opunk::console instance
# >=7 element list - an ::opunk::Console (or subclass) object value (voo)
#Returns dict: in <chan> out <chan> object <objectvalue-or-empty-string>
#The opunk::console package is only required for the name/object forms (lazy - punk::console
#itself must remain loadable without opunk::console; voo is now in bootsupport via
#include_modules.config but opunk::console is not, and channel-pair-only callers should not
#pay any object-layer load cost).
proc console_spec_resolve {spec} {
switch -exact -- [llength $spec] {
2 {
set in [lindex $spec 0]
set out [lindex $spec 1]
#auto-attach the process-default console object (anchored opunk::console instance
#'default' - normally constructed during repl::init) when it governs this channel pair,
#so all query sites benefit from its settled facts without explicitly passing it.
#info exists on the anchor variable is cheap and requires no package load.
if {[info exists ::opunk::console::instances::default]} {
set dobj [set ::opunk::console::instances::default]
if {[::opunk::Console::in $dobj] eq $in && [::opunk::Console::out $dobj] eq $out} {
return [dict create in $in out $out object $dobj]
}
}
return [dict create in $in out $out object ""]
}
1 {
if {[catch {package require opunk::console} errM]} {
error "console_spec_resolve: spec '$spec' looks like an anchored console instance name but package opunk::console is unavailable ($errM)"
}
set obj [opunk::console::instance $spec]
return [dict create in [::opunk::Console::in $obj] out [::opunk::Console::out $obj] object $obj]
}
default {
if {[llength $spec] >= 7} {
set tag [lindex $spec 0]
if {![catch {package require opunk::console}] && ![catch {voo::isVooClass $tag} isclass] && $isclass} {
return [dict create in [::opunk::Console::in $spec] out [::opunk::Console::out $spec] object $spec]
}
}
error "console_spec_resolve: unrecognised console specification '$spec' - expected {in out} channel pair, anchored opunk::console instance name, or ::opunk::Console object value"
}
}
}
#Return the name of the anchored process-default console instance (creating it for {stdin stdout}
#if it doesn't yet exist), or empty string if opunk::console is unavailable.
#The default console is normally constructed - and its can_respond settled where safely possible -
#during repl::init. console_spec_resolve auto-attaches it for matching channel pairs.
proc default_console {} {
if {[info exists ::opunk::console::instances::default]} {
return default
}
if {[catch {package require opunk::console}]} {
return ""
}
opunk::console::create default stdin stdout
return default
}
punk::args::define {
@id -id ::punk::console::get_size
@cmd -name punk::console::get_size -summary\
"Console size in character cells."\
-help\
"Size of the console as a dict with keys columns and rows.
Mechanisms are tried fastest-first: chan configure -winsize on the output channel, then ANSI
cursor-report mechanisms (timed per console on first use), then tput, defaulting to 80x24.
ANSI mechanisms are skipped when the input side cannot answer queries: a supplied console
object with settled can_respond 0 is authoritative (its default_size is returned), otherwise
heuristic channel/eof detection applies. A settled can_respond 1 bypasses the heuristics.
NOTE: argument parsing in this proc is manual for performance - this definition is documentation."
@opts
-console -default {stdin stdout} -type list -help\
"Console specification: a 2-element {in out} channel list, an anchored opunk::console
instance name, or an ::opunk::Console object value."
@values -min 0 -max 1
inoutchannels -type list -optional 1 -help\
"Legacy positional console specification (same forms accepted as -console)."
}
proc get_size {args} {
#manual argument parsing - performance sensitive path (see PUNKARGS id ::punk::console::get_size for documentation)
switch -exact -- [llength $args] {
0 {
set consolespec {stdin stdout}
}
1 {
set consolespec [lindex $args 0] ;#legacy positional inoutchannels - accepts any console spec form
}
2 {
if {[lindex $args 0] ne "-console"} {
error "get_size: expected ?consolespec? or -console <consolespec> - got '$args'"
}
set consolespec [lindex $args 1]
}
default {
error "get_size: expected ?consolespec? or -console <consolespec> - got '$args'"
}
}
set cinfo [console_spec_resolve $consolespec]
set in [dict get $cinfo in]
set out [dict get $cinfo out]
set cobj [dict get $cinfo object]
set inoutchannels [list $in $out] ;#canonical channel pair - also the get_size_mechanism cache key
set tried_mechlist [list]
#fastest mechanism if available - use Tcl's inbuilt -winsize key from chan configure if available - this is much faster than any ANSI mechanism
@ -1542,6 +1730,29 @@ namespace eval punk::console {
}
lappend tried_mechlist "chanconfigure"
#settled facts on a supplied console object override the heuristics in both directions
set can_respond -1
if {$cobj ne ""} {
set can_respond [::opunk::Console::get.o_can_respond $cobj]
}
if {$can_respond == 0} {
#console object is settled as unable to answer queries - its default_size is authoritative.
#(tput deliberately not consulted here: it reflects the controlling terminal environment,
# not the supplied channel pair)
return [::opunk::Console::default_size $cobj]
}
if {$can_respond != 1 && ![is_input_console_or_tty $in]} {
#input channel can't answer cursor position queries - skip the ANSI cursor-report mechanisms
#entirely (they would hang or timeout waiting for a response that cannot arrive)
if {![catch {get_size_using_tput $inoutchannels} sizedict]} {
return $sizedict
}
#fall back to a conventional default size rather than erroring, so callers laying out text
#(help tables etc) still function with piped/redirected input.
return [dict create columns 80 rows 24]
}
variable is_vt52
if {$is_vt52} {

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

@ -1,3 +1,10 @@
0.1.1
0.1.4
#First line must be a semantic version number
#all other lines are ignored.
#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
#0.1.3 - get_ansi_response_payload resolves console specs and errors immediately for objects settled as unable to respond
#0.1.2 - get_ansi_response_payload fails fast when the input channel is closed/at eof instead of emitting a query that can never be answered
#0.1.2 - get_size skips ANSI cursor-report mechanisms and falls back to tput then a default 80x24 when input is not a console/tty (new helpers input_at_eof with probe read, is_input_console_or_tty)
#0.1.2 - removed erroneous repl restart attempts from get_ansi_response_payload eof handling (re-entrancy, bypassed app-layer PUNK_PIPE_EOF policy; one call was a typo repl::reopen). eof now flows to the active reader.

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

@ -582,7 +582,7 @@ proc repl::start {inchan args} {
#REVIEW - timing (we need to ensure that the first chunk is read before we start the event loop - so we don't miss it if piped input is already available (cmdline data piped to process with EOF))
set readmore 1
after idle [list chan event $inchan readable [list [namespace current]::repl_handler $inchan $readmore $prompt_config]]
set reader_registration_id [after idle [list chan event $inchan readable [list [namespace current]::repl_handler $inchan $readmore $prompt_config]]]
#chan event $inchan readable [list [namespace current]::repl_handler $inchan $readmore $prompt_config]
set reading 1
#puts stderr "-->repl::start active on $inchan $args replthread:[thread::id] codethread:$codethread"
@ -592,6 +592,10 @@ proc repl::start {inchan args} {
# set punk::console::tabwidth [punk::console::get_tabstop_apparent_width]
#}
vwait [namespace current]::done
#done can be set before the deferred reader registration above has run (e.g exit/quit message from codethread arriving first)
#in which case the pending idle callback would re-register a readable handler on a repl that has finished
# - it must be cancelled as well as clearing any active registration.
after cancel $reader_registration_id
chan event $inchan readable {}
@ -2618,6 +2622,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#pass unevaluated command to runraw
set status [catch {uplevel #0 [list runraw $commandstr]} raw_result]
} else {
if {$codethread eq "" || ![thread::exists $codethread]} {
#input arrived after codethread teardown - e.g piped stdin data racing repl shutdown after an exit/quit was processed.
#There is nothing left that can evaluate it - drop it rather than raising 'invalid thread handle' from the thread::send below.
set commandstr ""
set input_chunks_waiting($inputchan) [list]
return
}
#puts stderr "repl uplevel 0 '$run_command_string'"
#JMN
#puts stderr "sending to codethread::runscript $run_command_string"
@ -3094,9 +3105,42 @@ namespace eval repl {
#error out before we create a thread - punk requires this - review
error "::argv0 not set"
}
#Construct the process-default console object (anchored opunk::console instance 'default').
#punk::console::console_spec_resolve auto-attaches it for matching channel pairs, so its settled
#facts inform subsequent get_size/ANSI-query decisions throughout the session.
#Only the positive capability is settled here (interactive stdin -> can_respond 1).
#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.
if {![catch {package require opunk::console}]} {
if {![info exists ::opunk::console::instances::default]} {
opunk::console::create default stdin stdout
}
upvar #0 [opunk::console::instancevar default] dconsole
if {[::opunk::Console::get.o_can_respond $dconsole] == -1 && [punk::repl::is_interactive]} {
::opunk::Console::set.o_can_respond dconsole 1
}
#review - after a console reopen (app-punkshell eof restart via CONIN$/SetStdHandle) the
#anchored default console retains its original channels/settled state - revisit when console
#objects participate more deeply at boot/restart.
}
#TODO - modify system to avoid this "first subshell" asymmetry.
#We want the same 'code running in a sub interp' behaviour for the first subshell as for subsequent subshells.
#This will be important later for us to control aspects of the code interp such as cpu/memory resource limits and sandboxing.
#It is also in terms of consistency for how thread calls are routed to the parent interp vs a child interp.
#Further todo: investigate whether Tcl code or thread extension code is responsible for routing thread::send -async calls.
#We want to be able to control which interp receives the call.
#If we can't do this in pure tcl, then first investigate if we can do it purely in an alternative thread extension.
#Such an extension should be based closely on the existing thread extension, with minimal changes to allow us to control which interp receives the call,
#the intention being to see if the changes can be made unobtrusive and backwards compatible so that it has a chance of being accepted into the mainline thread extension.
#in case -callback_interp wasn't explicitly defined - we make a guess based on how init was called as to whether this is being launched from a 'code' or root ("") interp.
if {[catch {info level -1} caller]} {
#todo logger
#puts "repl::init from: global"
set default_callback_interp ""
} else {

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

@ -1,3 +1,6 @@
0.1.2
0.1.4
#First line must be a semantic version number
#all other lines are ignored.
#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

2
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config

@ -39,6 +39,7 @@ set bootsupport_modules [list\
src/vendormodules textutil::tabify\
src/vendormodules textutil::trim\
src/vendormodules textutil::wcswidth\
src/vendormodules voo\
src/vendormodules uuid\
modules argp\
modules flagfilter\
@ -107,6 +108,7 @@ set bootsupport_modules [list\
modules textblock\
modules natsort\
modules oolib\
modules opunk::console\
modules zipper\
modules zzzload\
]

361
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/opunk/console-0.1.0.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.0
# 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).
#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.
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.0
}]
return

2
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm

@ -244,6 +244,7 @@ tcl::namespace::eval punk::config {
syslog_active 0
auto_exec_mechanism exec
auto_noexec 0
pipe_eof {}
} xdg_config_home $default_xdg_config_home {*}{
} xdg_data_home $default_xdg_data_home {*}{
} xdg_cache_home $default_xdg_cache_home {*}{
@ -284,6 +285,7 @@ tcl::namespace::eval punk::config {
PUNK_LOGFILE_STDOUT {type string}
PUNK_LOGFILE_STDERR {type string}
PUNK_LOGFILE_ACTIVE {type string}
PUNK_PIPE_EOF {type string default "" help "punk shell behaviour when its input channel\n(usually piped/redirected stdin) reaches eof.\n exit - always terminate\n interactive - reopen console, interactive repl\n unset/other - console if available, else exit"}
PUNK_SYSLOG_STDOUT {type string}
PUNK_SYSLOG_STDERR {type string}
PUNK_SYSLOG_ACTIVE {type string}

4618
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.3.tm

File diff suppressed because it is too large Load Diff

4645
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.4.tm

File diff suppressed because it is too large Load Diff

4201
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.3.tm

File diff suppressed because it is too large Load Diff

4222
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.4.tm

File diff suppressed because it is too large Load Diff

768
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/voo-1.0.0.tm

@ -0,0 +1,768 @@
#https://github.com/AlaoPrado/voo
#License: MIT
namespace eval voo {
# package version
variable version 1.0.0
variable handlerToObjectMap {}
variable handlerCounter 0
##\brief Check if a namespace is a valid voo class
# \param[in] namespaceName the namespace to check
# \return 1 if valid voo class, 0 otherwise
proc isVooClass {namespaceName} {
if {![uplevel [list namespace exists $namespaceName]]} {
return 0
}
return [expr {[uplevel [list namespace eval $namespaceName {
info exists __defaultObj
}]]}]
}
##\brief Declare a new voo class namespace and process its class body
# \param[in] args Arguments for class declaration: <className> <body> and optional -extends parent
# \note Creates the class namespace, imports parent fields/methods when using -extends,
# and registers constructors and exports
proc class {args} {
set optDict {}
set defaultArgs {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-extends"} {
if {$i + 1 >= $numArgs} {
error "Constructor option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} elseif {$arg eq "-virtual" || $arg eq "-v"} {
dict set optDict "-virtual" {}
} else {
lappend defaultArgs $arg
}
}
lassign $defaultArgs className body
set vooNs [namespace current]
# create the namespace for the class
uplevel [list namespace eval $className [subst -nocommands {
namespace path [list $vooNs]
variable __defaultObj {}
variable __fields {}
variable __tmp_isPublicEnabled 1
}]]
uplevel [list namespace eval $className {
##\brief Access default object for this class
# \return Default class instance (list)
# \note Used for inheritance and constructor defaults
proc class.defaultObj {} {
variable __defaultObj
return $__defaultObj
}
##\brief Get list of field names for this class
# \return List of field names in declaration order
# \note Useful for introspection and constructor -name new.args
proc class.fields {} {
variable __fields
return $__fields
}
}]
if {[dict exists $optDict -virtual] && [dict exists $optDict -extends]} {
error "voo::class: cannot use -virtual with -extends; child classes inherit virtual automatically from a -virtual parent"
}
if {[dict exists $optDict -virtual]} {
set normalizedClassName [uplevel [list namespace eval $className {namespace current}]]
uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]]
uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedClassName]]
# Pre-populate __defaultObj with namespace tag at index 0 BEFORE field declarations
# so that _getClassCurrNumFields returns 1 for the first field declared
uplevel [list namespace eval $className [list set __defaultObj [list $normalizedClassName]]]
}
#81
# variable __parentClassNamespace {}
if {[dict exists $optDict -extends]} {
set parentClassName [dict get $optDict -extends]
if {![uplevel [list namespace exists $parentClassName]]} {
error "Parent class ’$parentClassName’ does not exist."
}
# check if parent class exists
if {![uplevel [list namespace eval $parentClassName {info exists __defaultObj}]]} {
error "Parent class ’$parentClassName’ is not a valid voo class."
}
# normalize namespace name of parent class
set parentClassName [uplevel [list namespace eval $parentClassName {
namespace current
}]]
uplevel [list namespace eval $className [subst -nocommands {
variable __parentClassNamespace $parentClassName
}]]
# import parent’s default object values
set parentDefaultObj [${parentClassName}::class.defaultObj]
uplevel [list namespace eval $className [list set __defaultObj $parentDefaultObj]]
# if parent is virtual, update namespace tag at index 0 to child’s namespace
set parentIsVirtual [uplevel [list namespace eval $parentClassName {info exists __voo_is_virtual_class}]]
if {$parentIsVirtual} {
set normalizedChildName [uplevel [list namespace eval $className {namespace current}]]
uplevel [list namespace eval $className \
[list set __defaultObj [lreplace $parentDefaultObj 0 0 $normalizedChildName]]]
uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]]
uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedChildName]]
}
# 121
# import parent’s field index variables by copying actual index values from parent
set parentFields [${parentClassName}::class.fields]
foreach field $parentFields {
set fieldIdx [uplevel [list namespace eval $parentClassName [list set $field]]]
uplevel [list namespace eval $className [list variable $field $fieldIdx]]
uplevel [list namespace eval $className [list lappend __fields $field]]
}
# import parent’s acessors in child class with namespace import
uplevel [list namespace eval $className [subst -nocommands {
namespace import ${parentClassName}::get.*
namespace import ${parentClassName}::set.*
namespace import ${parentClassName}::update.*
}]]
}
# 136
uplevel [list namespace eval $className $body]
uplevel [list namespace eval $className {
if {[info commands new] eq ""} {
constructor
}
if {[info commands new()] eq ""} {
constructor -noargs [_buildConstructorNoArgsBody]
}
if {[info commands new.args] eq ""} {
constructor -name new.args {args} [_buildConstructorArgsBody]
}
}]
# 151
uplevel [list namespace eval $className {
# export class methods
namespace export *
}]
uplevel [list namespace eval $className {
# clean temporary variable
unset __tmp_isPublicEnabled
}]
return
}
# 161
##\brief Return the default value for a given field type
# \param[in] type the field type token (double,int,bool,...)
# \return The default value appropriate for the type
proc _getDefaultValueByType {type} {
switch -- $type {
double { return 0.0 }
int { return 0 }
bool { return 0 }
default { return {} }
}
}
##\brief Get the current number of fields declared in the current class
# \return Number of fields (integer)
proc _getClassCurrNumFields {} {
return [uplevel 2 {llength $__defaultObj}]
}
##\brief Check whether public mode is enabled during class body parsing
# \return 1 if public mode is enabled, 0 otherwise
proc _getClassIsPublicEnabled {} {
return [uplevel 2 {set __tmp_isPublicEnabled}]
}
##\brief Declare getter/setter/updater accessors for a class field
# \param[in] fieldName name of the field
# \param[in] isPublic boolean whether accessors are public
# \param[in] isStatic boolean whether field is static (class-level)
proc _declareFieldAcessors {fieldName isPublic isStatic} {
set prefix {}
if {$isStatic} {
append prefix class.
}
if {!$isPublic} {
append prefix my.
}
set getterName "${prefix}get.$fieldName"
set setterName "${prefix}set.$fieldName"
set updaterName "${prefix}update.$fieldName"
if {$isStatic} {
uplevel 2 [list proc $getterName {} [subst -nocommands {
variable $fieldName
return $$fieldName
}]]
uplevel 2 [list proc $setterName {value} [subst -nocommands {
variable $fieldName
set $fieldName "\$value"
}]]
uplevel 2 [list proc $updaterName {tempVar body} [subst -nocommands {
variable $fieldName
upvar "\$tempVar" temp
set temp $$fieldName
# break link with class variable to avoid copy-on-write
set $fieldName {}
try {
uplevel \$body
} finally {
set $fieldName "\$temp"
}
}]]
} else {
uplevel 2 [list getter $getterName $fieldName]
uplevel 2 [list setter $setterName $fieldName]
uplevel 2 [list updater $updaterName $fieldName]
}
return
}
##\brief Validate a field name for illegal characters
# \param[in] fieldName the field name to validate
# \return Raises an error if invalid
proc _validateFieldName {fieldName} {
if {[string first "." $fieldName] != -1 || [string first "::" $fieldName] != -1} {
error "Field name ’$fieldName’ cannot contain ’.’ or ’::’ substrings."
}
}
##\brief Ensure a field name does not already exist in the class
# \param[in] fieldName the field name to check
# \return Raises an error if the field already exists
# \note Uses __fields for instance fields and fully-qualified namespace lookup for static
# fields to avoid false positives from global variables with the same name
proc _validateFieldDoesNotExist {fieldName} {
# Check instance fields tracked in __fields (class-scoped, no global bleed)
if {$fieldName in [uplevel 2 {set __fields}]} {
error "Field name ’$fieldName’ already exists in the class."
}
# Check static fields via fully-qualified namespace variable; info exists ::Ns::var
# only matches that exact namespace variable, never a same-named global
set classNs [uplevel 2 {namespace current}]
if {[info exists ${classNs}::$fieldName]} {
error "Field name ’$fieldName’ already exists in the class."
}
}
##\brief Validate a variable initial value according to its declared type
# \param[in] type the declared type (double,int,bool,list,dict)
# \param[in] value the value to validate
# \return Raises an error if the value does not match the type
proc _validateVarValueByType {type value} {
switch -- $type {
double {
if {[string is double -strict $value] == 0} {
error "Value for t_double must be a double, got ’$value’"
}
}
int {
if {[string is integer -strict $value] == 0} {
error "Value for t_int must be an integer, got ’$value’"
}
}
bool {
if {[string is boolean -strict $value] == 0} {
error "Value for t_bool must be a boolean, got ’$value’"
}
}
list {
if {[catch {llength $value}]} {
error "Value for t_list must be a list, got ’$value’"
}
}
dict {
if {[catch {dict size $value}]} {
error "Value for t_dict must be a dict, got ’$value’"
}
}
}
}
##\brief Declare a field variable inside the class body
# \param[in] type the field type token (double,int,string,bool,list,dict,obj)
# \param[in] argList arguments: ?-static? <name> ?<initialValue>?
proc _var {type argList} {
set defaultArgs {}
set optDict {}
set numArgs [llength $argList]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $argList $i]
if {$arg eq "-static"} {
dict set optDict $arg {}
} else {
lappend defaultArgs $arg
}
}
if {[llength $defaultArgs] == 0} {
error "Variable definition requires: ?<option>? <name> ?<initialValue>?"
}
if {[llength $defaultArgs] == 2} {
lassign $defaultArgs name initVal
} else {
lassign $defaultArgs name
set initVal [_getDefaultValueByType $type]
}
_validateFieldName $name
_validateFieldDoesNotExist $name
_validateVarValueByType $type $initVal
if {[dict exists $optDict -static]} {
# static field
uplevel [list variable $name $initVal]
} else {
set currNumFields [_getClassCurrNumFields]
uplevel [list variable $name $currNumFields]
uplevel [list lappend __defaultObj $initVal]
uplevel [list lappend __fields $name]
}
set isPublicEnabled [_getClassIsPublicEnabled]
_declareFieldAcessors $name $isPublicEnabled [dict exists $optDict -static]
return
}
# 341
##\brief Declare a double-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc double_t {args} {
uplevel [list _var "double" $args]
}
##\brief Declare an integer-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc int_t {args} {
uplevel [list _var "int" $args]
}
##\brief Declare a string-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc string_t {args} {
uplevel [list _var "string" $args]
}
##\brief Declare a boolean-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc bool_t {args} {
uplevel [list _var "bool" $args]
}
##\brief Declare a list-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc list_t {args} {
uplevel [list _var "list" $args]
}
##\brief Declare a dict-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc dict_t {args} {
uplevel [list _var "dict" $args]
}
##\brief Declare an object-typed field (nested vanilla object)
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc obj_t {args} {
uplevel [list _var "object" $args]
}
# 386
##\brief Enable public mode for declarations inside the provided body
# \param[in] body script to execute with public accessors enabled
# \return Result of executing body
proc public {body} {
uplevel $body
}
##\brief Execute the provided body with private mode enabled (temporarily disables public accessors)
# \param[in] body script to execute with private accessors
# \return Result of executing body
proc private {body} {
uplevel {variable __tmp_isPublicEnabled 0}
try {
uplevel $body
} finally {
uplevel {variable __tmp_isPublicEnabled 1}
}
}
##\brief Build the body for a no-argument constructor
# \return A script chunk used as constructor body that returns the class default object
proc _buildConstructorNoArgsBody {} {
return {
variable __defaultObj
return $__defaultObj;
}
}
##\brief Build the body for a constructor that accepts named args (-field value pairs)
# \return A script chunk used as constructor body that applies named arguments to the default object
proc _buildConstructorArgsBody {} {
return {
variable __defaultObj
set obj $__defaultObj
if {[catch {dict size $args}]} {
error "Constructor argument must be a list of ’-<field> <value>’ pairs"
}
dict for {key value} $args {
if {[string index $key 0] ne "-"} {
error "Constructor argument keys must start with ’-’, got ’$key’"
}
set field [string range $key 1 end]
set setter set.$field
if {[info commands $setter] ne ""} {
$setter obj $value
} else {
set setter my.set.$field
if {[info commands $setter] ne ""} {
$setter obj $value
} else {
error "Unknown field option: $field"
}
}
}
return $obj
}
}
##\brief Build constructor parameter list and body for positional constructors
# \return A list of two elements: argument names list and a body script that returns them as a list
# \note For virtual classes, the concrete class namespace is embedded as a literal string at
# class-definition time (not looked up at runtime), producing:
# return [list ::ClassName $f1 $f2 ...]
# This avoids all runtime proc calls (class.defaultObj, set.*) and variable lookups,
# making virtual object creation as cheap as non-virtual.
proc _buildConstructorParams {} {
set argList [uplevel 2 {set __fields}]
set isVirtual [uplevel 2 {info exists __voo_is_virtual_class}]
set spacedArgVarListStr {}
foreach arg $argList {
append spacedArgVarListStr "\$$arg "
}
if {$isVirtual} {
# Read the normalized class namespace at definition time so subst embeds it
# as a literal in the generated body - no runtime variable lookup required.
set classNs [uplevel 2 {set __voo_class_namespace}]
set spacedArgVarListStr "{$classNs} $spacedArgVarListStr"
set body [subst -nocommands {
return [list $spacedArgVarListStr]
}]
} else {
set body [subst -nocommands {
return [list $spacedArgVarListStr]
}]
}
return [list $argList $body]
}
##\brief Define a constructor for the current class
# \param[in] args Constructor declaration options and body
# \note Supports -name, -noargs and -typed variants
proc constructor {args} {
set defaultArgs {}
set optDict {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-name" || $arg eq "-noargs" || $arg eq "-typed"} {
if {$i + 1 >= $numArgs} {
error "Constructor option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} else {
lappend defaultArgs $arg
}
}
# check valid option combinations
if {[dict exists $optDict -name]} {
if {[dict exists $optDict -noargs] || [dict exists $optDict -typed]} {
error "Constructor cannot have -name option with -noargs or -typed options"
}
}
if {[dict exists $optDict -noargs] && [dict exists $optDict -typed]} {
error "Constructor cannot have both -noargs and -typed options"
}
if {[dict exists $optDict -name]} {
set constructorName [dict get $optDict -name]
} elseif {[dict exists $optDict -noargs]} {
set constructorName "new()"
} elseif {[dict exists $optDict -typed]} {
set constructorName "new([join [dict get $optDict -typed] ,])"
} else {
set constructorName "new"
}
if {[dict exists $optDict -noargs]} {
if {[llength $defaultArgs] != 0} {
error "Invalid constructor definition, expected ’?...? ?<body>?’ for -noargs"
}
set argList {}
set body [dict get $optDict -noargs]
} else {
if {[llength $defaultArgs] == 0} {
lassign [_buildConstructorParams] argList body
} else {
if {[llength $defaultArgs] != 2} {
error "Invalid constructor definition, expected ’?...? ?<argList> <body>?’"
}
lassign $defaultArgs argList body
}
}
uplevel [list proc $constructorName $argList $body]
return
}
# 531
##\brief Generate a getter procedure for a field
# \param[in] methodName name of the generated getter (may include namespace prefix)
# \param[in] fieldName name of the field to read
proc getter {methodName fieldName} {
# implementation of getter definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Getter for $fieldName
# \\param\[in\] this class instance
# \\return $fieldName value
proc $methodName {this} {
return [lindex \$this $fieldIdx]
}
}]
return
}
##\brief Generate a setter procedure for a field
# \param[in] methodName name of the generated setter (may include namespace prefix)
# \param[in] fieldName name of the field to write
proc setter {methodName fieldName} {
# implementation of setter definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Setter for $fieldName
# \\param\[in\] thisVar name of variable containing class instance
# \\param\[in\] value new value for $fieldName
proc $methodName {thisVar value} {
upvar \$thisVar this
lset this $fieldIdx \$value
}
}]
return
}
##\brief Generate an updater procedure for a field (copy-on-write safe)
# \param[in] methodName name of the generated updater (may include namespace prefix)
# \param[in] fieldName name of the field to update by reference
# \note The updater detaches the field to avoid unnecessary copying during updates
proc updater {methodName fieldName} {
# implementation of updater definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Update $fieldName by reference
# \\param\[in\] thisVar name of variable containing class instance
# \\param\[out\] tempVar name of variable to hold $fieldName during update
# \\param\[in\] body script to execute with $fieldName in tempVar
# \\note Avoids copy-on-write by detaching field during update
proc $methodName {thisVar tempVar body} {
upvar \$thisVar this
upvar \$tempVar temp
set temp [lindex \$this $fieldIdx]
# break link with object to avoid copy-on-write
lset this $fieldIdx {}
try {
uplevel \$body
} finally {
lset this $fieldIdx \$temp
}
}
}]
}
##\brief Declare a method in the current class namespace
# \param[in] args Method declaration arguments: name, argList, body and options (-static, -upvar, -update, -override)
proc method {args} {
set isPublicEnabled [_getClassIsPublicEnabled]
set defaultArgs {}
set optDict {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-static" || $arg eq "-upvar"} {
dict set optDict $arg {}
} elseif {$arg eq "-update"} {
if {$i + 1 >= $numArgs} {
error "Method option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} elseif {$arg eq "-override"} {
# Explicit override indicator
dict set optDict $arg {}
} elseif {$arg eq "-virtual"} {
dict set optDict $arg {}
} else {
lappend defaultArgs $arg
}
}
lassign $defaultArgs name argList body
# check valid option combinations
if {[dict exists $optDict -static]} {
if {[dict exists $optDict -upvar] || [dict exists $optDict -update]} {
error "Method cannot have both -static and -upvar or -update options"
}
}
if {[dict exists $optDict -update]} {
if {![dict exists $optDict -upvar]} {
# automatically add -upvar if -update is specified
dict set optDict -upvar {}
}
}
set finalArgList {}
set finalBody {}
if {[dict exists $optDict -upvar]} {
lappend finalArgList "thisVar"
append finalBody {
upvar $thisVar this
}
} elseif {![dict exists $optDict -static]} {
lappend finalArgList "this"
}
lappend finalArgList {*}$argList
set className [uplevel {namespace current}]
if {[dict exists $optDict -update]} {
set updateFields [dict get $optDict -update]
if {[llength $updateFields] == 0} {
error "-update option requires at least one field name"
}
foreach field $updateFields {
try {
set fieldIdx [uplevel [list set $field]]
} trap {} {} {
error "Field ’$field’ specified in -update option does not exist in class ’$className’"
}
append finalBody [subst -nocommands {
set $field [lindex \$this $fieldIdx]
lset this $fieldIdx {}
}]
}
append finalBody "try \{"
}
append finalBody $body
if {[dict exists $optDict -update]} {
append finalBody "\} finally \{"
foreach field $updateFields {
set fieldIdx [uplevel [list set $field]]
append finalBody [subst -nocommands {
lset this $fieldIdx \$$field
}]
}
append finalBody "\}"
}
if {!$isPublicEnabled} {
set name "my.$name"
}
if {[dict exists $optDict -override]} {
set parentNs [uplevel {set __parentClassNamespace}]
if {[info commands "${parentNs}::$name"] eq ""} {
error "Method ’$name’ does not override any method in parent class ’$parentNs’"
}
# If parent’s method is virtual (has base.<name>), auto-promote this override
# to a dispatcher so that deep inheritance dispatch works correctly
if {[uplevel {info exists __voo_is_virtual_class}] && \
[info commands "${parentNs}::base.$name"] ne ""} {
dict set optDict -virtual {}
}
}
if {[dict exists $optDict -virtual]} {
if {![uplevel {info exists __voo_is_virtual_class}]} {
error "Method ’$name’ is declared -virtual but ’[uplevel {namespace current}]’ is not a virtual class"
}
if {[dict exists $optDict -upvar] || [dict exists $optDict -update] || [dict exists $optDict -static]} {
error "Method ’$name’ cannot combine -virtual with -upvar, -update, or -static"
}
# Register base.<name> with the original body for direct parent calls from subclasses
uplevel [list proc "base.$name" $finalArgList $finalBody]
# Build dispatch body: route to concrete class implementation at runtime
set dispatchBody "set __voo_cls \[lindex \$this 0\]\n"
append dispatchBody "if \{\$__voo_cls ne \[namespace current\] && \[info commands \${__voo_cls}::$name\] ne {}\} \{\n"
append dispatchBody " return \[\${__voo_cls}::$name \$this"
foreach arg $argList {
append dispatchBody " \$$arg"
}
append dispatchBody "\]\n\}\n"
append dispatchBody "return \[base.$name \$this"
foreach arg $argList {
append dispatchBody " \$$arg"
}
append dispatchBody "\]"
set finalBody $dispatchBody
}
uplevel [list proc $name $finalArgList $finalBody]
return
}
# 726
##\brief Import one or more methods from parent class into the current (child) class namespace.
# \param[in] methods List of method names (or a single method name) to import from parent.
# \note Must be called inside a class declared with -extends. Methods are copied at class-definition time.
proc importMethods {methods} {
set parentNs [uplevel {set __parentClassNamespace}]
# Validate caller context and get parent namespace stored by -extends handling
if {$parentNs eq ""} {
error "importMethods can only be used inside a class declared with -extends"
}
# Normalize to a list of method names
if {[string length [string trim $methods]] == 0} {
return
}
if {[catch {llength $methods}]} {
set methodList [list $methods]
} else {
set methodList $methods
}
foreach methodName $methodList {
set fullMethodName "${parentNs}::$methodName"
# Validate parent method exists
if {[info commands $fullMethodName] eq ""} {
error "Method ’$methodName’ not found in parent class ’$parentNs’"
}
# Define a copy in the child namespace so unqualified calls resolve to child
set argList [info args $fullMethodName]
set body [info body $fullMethodName]
uplevel [list proc $methodName $argList $body]
}
return
}
namespace export *
}
# provide the package
package provide voo $::voo::version

2
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config

@ -39,6 +39,7 @@ set bootsupport_modules [list\
src/vendormodules textutil::tabify\
src/vendormodules textutil::trim\
src/vendormodules textutil::wcswidth\
src/vendormodules voo\
src/vendormodules uuid\
modules argp\
modules flagfilter\
@ -107,6 +108,7 @@ set bootsupport_modules [list\
modules textblock\
modules natsort\
modules oolib\
modules opunk::console\
modules zipper\
modules zzzload\
]

361
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/opunk/console-0.1.0.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.0
# 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).
#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.
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.0
}]
return

2
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm

@ -244,6 +244,7 @@ tcl::namespace::eval punk::config {
syslog_active 0
auto_exec_mechanism exec
auto_noexec 0
pipe_eof {}
} xdg_config_home $default_xdg_config_home {*}{
} xdg_data_home $default_xdg_data_home {*}{
} xdg_cache_home $default_xdg_cache_home {*}{
@ -284,6 +285,7 @@ tcl::namespace::eval punk::config {
PUNK_LOGFILE_STDOUT {type string}
PUNK_LOGFILE_STDERR {type string}
PUNK_LOGFILE_ACTIVE {type string}
PUNK_PIPE_EOF {type string default "" help "punk shell behaviour when its input channel\n(usually piped/redirected stdin) reaches eof.\n exit - always terminate\n interactive - reopen console, interactive repl\n unset/other - console if available, else exit"}
PUNK_SYSLOG_STDOUT {type string}
PUNK_SYSLOG_STDERR {type string}
PUNK_SYSLOG_ACTIVE {type string}

4618
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.3.tm

File diff suppressed because it is too large Load Diff

4645
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.4.tm

File diff suppressed because it is too large Load Diff

4201
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.3.tm

File diff suppressed because it is too large Load Diff

4222
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.4.tm

File diff suppressed because it is too large Load Diff

768
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/voo-1.0.0.tm

@ -0,0 +1,768 @@
#https://github.com/AlaoPrado/voo
#License: MIT
namespace eval voo {
# package version
variable version 1.0.0
variable handlerToObjectMap {}
variable handlerCounter 0
##\brief Check if a namespace is a valid voo class
# \param[in] namespaceName the namespace to check
# \return 1 if valid voo class, 0 otherwise
proc isVooClass {namespaceName} {
if {![uplevel [list namespace exists $namespaceName]]} {
return 0
}
return [expr {[uplevel [list namespace eval $namespaceName {
info exists __defaultObj
}]]}]
}
##\brief Declare a new voo class namespace and process its class body
# \param[in] args Arguments for class declaration: <className> <body> and optional -extends parent
# \note Creates the class namespace, imports parent fields/methods when using -extends,
# and registers constructors and exports
proc class {args} {
set optDict {}
set defaultArgs {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-extends"} {
if {$i + 1 >= $numArgs} {
error "Constructor option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} elseif {$arg eq "-virtual" || $arg eq "-v"} {
dict set optDict "-virtual" {}
} else {
lappend defaultArgs $arg
}
}
lassign $defaultArgs className body
set vooNs [namespace current]
# create the namespace for the class
uplevel [list namespace eval $className [subst -nocommands {
namespace path [list $vooNs]
variable __defaultObj {}
variable __fields {}
variable __tmp_isPublicEnabled 1
}]]
uplevel [list namespace eval $className {
##\brief Access default object for this class
# \return Default class instance (list)
# \note Used for inheritance and constructor defaults
proc class.defaultObj {} {
variable __defaultObj
return $__defaultObj
}
##\brief Get list of field names for this class
# \return List of field names in declaration order
# \note Useful for introspection and constructor -name new.args
proc class.fields {} {
variable __fields
return $__fields
}
}]
if {[dict exists $optDict -virtual] && [dict exists $optDict -extends]} {
error "voo::class: cannot use -virtual with -extends; child classes inherit virtual automatically from a -virtual parent"
}
if {[dict exists $optDict -virtual]} {
set normalizedClassName [uplevel [list namespace eval $className {namespace current}]]
uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]]
uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedClassName]]
# Pre-populate __defaultObj with namespace tag at index 0 BEFORE field declarations
# so that _getClassCurrNumFields returns 1 for the first field declared
uplevel [list namespace eval $className [list set __defaultObj [list $normalizedClassName]]]
}
#81
# variable __parentClassNamespace {}
if {[dict exists $optDict -extends]} {
set parentClassName [dict get $optDict -extends]
if {![uplevel [list namespace exists $parentClassName]]} {
error "Parent class ’$parentClassName’ does not exist."
}
# check if parent class exists
if {![uplevel [list namespace eval $parentClassName {info exists __defaultObj}]]} {
error "Parent class ’$parentClassName’ is not a valid voo class."
}
# normalize namespace name of parent class
set parentClassName [uplevel [list namespace eval $parentClassName {
namespace current
}]]
uplevel [list namespace eval $className [subst -nocommands {
variable __parentClassNamespace $parentClassName
}]]
# import parent’s default object values
set parentDefaultObj [${parentClassName}::class.defaultObj]
uplevel [list namespace eval $className [list set __defaultObj $parentDefaultObj]]
# if parent is virtual, update namespace tag at index 0 to child’s namespace
set parentIsVirtual [uplevel [list namespace eval $parentClassName {info exists __voo_is_virtual_class}]]
if {$parentIsVirtual} {
set normalizedChildName [uplevel [list namespace eval $className {namespace current}]]
uplevel [list namespace eval $className \
[list set __defaultObj [lreplace $parentDefaultObj 0 0 $normalizedChildName]]]
uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]]
uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedChildName]]
}
# 121
# import parent’s field index variables by copying actual index values from parent
set parentFields [${parentClassName}::class.fields]
foreach field $parentFields {
set fieldIdx [uplevel [list namespace eval $parentClassName [list set $field]]]
uplevel [list namespace eval $className [list variable $field $fieldIdx]]
uplevel [list namespace eval $className [list lappend __fields $field]]
}
# import parent’s acessors in child class with namespace import
uplevel [list namespace eval $className [subst -nocommands {
namespace import ${parentClassName}::get.*
namespace import ${parentClassName}::set.*
namespace import ${parentClassName}::update.*
}]]
}
# 136
uplevel [list namespace eval $className $body]
uplevel [list namespace eval $className {
if {[info commands new] eq ""} {
constructor
}
if {[info commands new()] eq ""} {
constructor -noargs [_buildConstructorNoArgsBody]
}
if {[info commands new.args] eq ""} {
constructor -name new.args {args} [_buildConstructorArgsBody]
}
}]
# 151
uplevel [list namespace eval $className {
# export class methods
namespace export *
}]
uplevel [list namespace eval $className {
# clean temporary variable
unset __tmp_isPublicEnabled
}]
return
}
# 161
##\brief Return the default value for a given field type
# \param[in] type the field type token (double,int,bool,...)
# \return The default value appropriate for the type
proc _getDefaultValueByType {type} {
switch -- $type {
double { return 0.0 }
int { return 0 }
bool { return 0 }
default { return {} }
}
}
##\brief Get the current number of fields declared in the current class
# \return Number of fields (integer)
proc _getClassCurrNumFields {} {
return [uplevel 2 {llength $__defaultObj}]
}
##\brief Check whether public mode is enabled during class body parsing
# \return 1 if public mode is enabled, 0 otherwise
proc _getClassIsPublicEnabled {} {
return [uplevel 2 {set __tmp_isPublicEnabled}]
}
##\brief Declare getter/setter/updater accessors for a class field
# \param[in] fieldName name of the field
# \param[in] isPublic boolean whether accessors are public
# \param[in] isStatic boolean whether field is static (class-level)
proc _declareFieldAcessors {fieldName isPublic isStatic} {
set prefix {}
if {$isStatic} {
append prefix class.
}
if {!$isPublic} {
append prefix my.
}
set getterName "${prefix}get.$fieldName"
set setterName "${prefix}set.$fieldName"
set updaterName "${prefix}update.$fieldName"
if {$isStatic} {
uplevel 2 [list proc $getterName {} [subst -nocommands {
variable $fieldName
return $$fieldName
}]]
uplevel 2 [list proc $setterName {value} [subst -nocommands {
variable $fieldName
set $fieldName "\$value"
}]]
uplevel 2 [list proc $updaterName {tempVar body} [subst -nocommands {
variable $fieldName
upvar "\$tempVar" temp
set temp $$fieldName
# break link with class variable to avoid copy-on-write
set $fieldName {}
try {
uplevel \$body
} finally {
set $fieldName "\$temp"
}
}]]
} else {
uplevel 2 [list getter $getterName $fieldName]
uplevel 2 [list setter $setterName $fieldName]
uplevel 2 [list updater $updaterName $fieldName]
}
return
}
##\brief Validate a field name for illegal characters
# \param[in] fieldName the field name to validate
# \return Raises an error if invalid
proc _validateFieldName {fieldName} {
if {[string first "." $fieldName] != -1 || [string first "::" $fieldName] != -1} {
error "Field name ’$fieldName’ cannot contain ’.’ or ’::’ substrings."
}
}
##\brief Ensure a field name does not already exist in the class
# \param[in] fieldName the field name to check
# \return Raises an error if the field already exists
# \note Uses __fields for instance fields and fully-qualified namespace lookup for static
# fields to avoid false positives from global variables with the same name
proc _validateFieldDoesNotExist {fieldName} {
# Check instance fields tracked in __fields (class-scoped, no global bleed)
if {$fieldName in [uplevel 2 {set __fields}]} {
error "Field name ’$fieldName’ already exists in the class."
}
# Check static fields via fully-qualified namespace variable; info exists ::Ns::var
# only matches that exact namespace variable, never a same-named global
set classNs [uplevel 2 {namespace current}]
if {[info exists ${classNs}::$fieldName]} {
error "Field name ’$fieldName’ already exists in the class."
}
}
##\brief Validate a variable initial value according to its declared type
# \param[in] type the declared type (double,int,bool,list,dict)
# \param[in] value the value to validate
# \return Raises an error if the value does not match the type
proc _validateVarValueByType {type value} {
switch -- $type {
double {
if {[string is double -strict $value] == 0} {
error "Value for t_double must be a double, got ’$value’"
}
}
int {
if {[string is integer -strict $value] == 0} {
error "Value for t_int must be an integer, got ’$value’"
}
}
bool {
if {[string is boolean -strict $value] == 0} {
error "Value for t_bool must be a boolean, got ’$value’"
}
}
list {
if {[catch {llength $value}]} {
error "Value for t_list must be a list, got ’$value’"
}
}
dict {
if {[catch {dict size $value}]} {
error "Value for t_dict must be a dict, got ’$value’"
}
}
}
}
##\brief Declare a field variable inside the class body
# \param[in] type the field type token (double,int,string,bool,list,dict,obj)
# \param[in] argList arguments: ?-static? <name> ?<initialValue>?
proc _var {type argList} {
set defaultArgs {}
set optDict {}
set numArgs [llength $argList]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $argList $i]
if {$arg eq "-static"} {
dict set optDict $arg {}
} else {
lappend defaultArgs $arg
}
}
if {[llength $defaultArgs] == 0} {
error "Variable definition requires: ?<option>? <name> ?<initialValue>?"
}
if {[llength $defaultArgs] == 2} {
lassign $defaultArgs name initVal
} else {
lassign $defaultArgs name
set initVal [_getDefaultValueByType $type]
}
_validateFieldName $name
_validateFieldDoesNotExist $name
_validateVarValueByType $type $initVal
if {[dict exists $optDict -static]} {
# static field
uplevel [list variable $name $initVal]
} else {
set currNumFields [_getClassCurrNumFields]
uplevel [list variable $name $currNumFields]
uplevel [list lappend __defaultObj $initVal]
uplevel [list lappend __fields $name]
}
set isPublicEnabled [_getClassIsPublicEnabled]
_declareFieldAcessors $name $isPublicEnabled [dict exists $optDict -static]
return
}
# 341
##\brief Declare a double-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc double_t {args} {
uplevel [list _var "double" $args]
}
##\brief Declare an integer-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc int_t {args} {
uplevel [list _var "int" $args]
}
##\brief Declare a string-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc string_t {args} {
uplevel [list _var "string" $args]
}
##\brief Declare a boolean-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc bool_t {args} {
uplevel [list _var "bool" $args]
}
##\brief Declare a list-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc list_t {args} {
uplevel [list _var "list" $args]
}
##\brief Declare a dict-typed field
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc dict_t {args} {
uplevel [list _var "dict" $args]
}
##\brief Declare an object-typed field (nested vanilla object)
# \param[in] args same arguments accepted by _var (name and optional initial value)
proc obj_t {args} {
uplevel [list _var "object" $args]
}
# 386
##\brief Enable public mode for declarations inside the provided body
# \param[in] body script to execute with public accessors enabled
# \return Result of executing body
proc public {body} {
uplevel $body
}
##\brief Execute the provided body with private mode enabled (temporarily disables public accessors)
# \param[in] body script to execute with private accessors
# \return Result of executing body
proc private {body} {
uplevel {variable __tmp_isPublicEnabled 0}
try {
uplevel $body
} finally {
uplevel {variable __tmp_isPublicEnabled 1}
}
}
##\brief Build the body for a no-argument constructor
# \return A script chunk used as constructor body that returns the class default object
proc _buildConstructorNoArgsBody {} {
return {
variable __defaultObj
return $__defaultObj;
}
}
##\brief Build the body for a constructor that accepts named args (-field value pairs)
# \return A script chunk used as constructor body that applies named arguments to the default object
proc _buildConstructorArgsBody {} {
return {
variable __defaultObj
set obj $__defaultObj
if {[catch {dict size $args}]} {
error "Constructor argument must be a list of ’-<field> <value>’ pairs"
}
dict for {key value} $args {
if {[string index $key 0] ne "-"} {
error "Constructor argument keys must start with ’-’, got ’$key’"
}
set field [string range $key 1 end]
set setter set.$field
if {[info commands $setter] ne ""} {
$setter obj $value
} else {
set setter my.set.$field
if {[info commands $setter] ne ""} {
$setter obj $value
} else {
error "Unknown field option: $field"
}
}
}
return $obj
}
}
##\brief Build constructor parameter list and body for positional constructors
# \return A list of two elements: argument names list and a body script that returns them as a list
# \note For virtual classes, the concrete class namespace is embedded as a literal string at
# class-definition time (not looked up at runtime), producing:
# return [list ::ClassName $f1 $f2 ...]
# This avoids all runtime proc calls (class.defaultObj, set.*) and variable lookups,
# making virtual object creation as cheap as non-virtual.
proc _buildConstructorParams {} {
set argList [uplevel 2 {set __fields}]
set isVirtual [uplevel 2 {info exists __voo_is_virtual_class}]
set spacedArgVarListStr {}
foreach arg $argList {
append spacedArgVarListStr "\$$arg "
}
if {$isVirtual} {
# Read the normalized class namespace at definition time so subst embeds it
# as a literal in the generated body - no runtime variable lookup required.
set classNs [uplevel 2 {set __voo_class_namespace}]
set spacedArgVarListStr "{$classNs} $spacedArgVarListStr"
set body [subst -nocommands {
return [list $spacedArgVarListStr]
}]
} else {
set body [subst -nocommands {
return [list $spacedArgVarListStr]
}]
}
return [list $argList $body]
}
##\brief Define a constructor for the current class
# \param[in] args Constructor declaration options and body
# \note Supports -name, -noargs and -typed variants
proc constructor {args} {
set defaultArgs {}
set optDict {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-name" || $arg eq "-noargs" || $arg eq "-typed"} {
if {$i + 1 >= $numArgs} {
error "Constructor option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} else {
lappend defaultArgs $arg
}
}
# check valid option combinations
if {[dict exists $optDict -name]} {
if {[dict exists $optDict -noargs] || [dict exists $optDict -typed]} {
error "Constructor cannot have -name option with -noargs or -typed options"
}
}
if {[dict exists $optDict -noargs] && [dict exists $optDict -typed]} {
error "Constructor cannot have both -noargs and -typed options"
}
if {[dict exists $optDict -name]} {
set constructorName [dict get $optDict -name]
} elseif {[dict exists $optDict -noargs]} {
set constructorName "new()"
} elseif {[dict exists $optDict -typed]} {
set constructorName "new([join [dict get $optDict -typed] ,])"
} else {
set constructorName "new"
}
if {[dict exists $optDict -noargs]} {
if {[llength $defaultArgs] != 0} {
error "Invalid constructor definition, expected ’?...? ?<body>?’ for -noargs"
}
set argList {}
set body [dict get $optDict -noargs]
} else {
if {[llength $defaultArgs] == 0} {
lassign [_buildConstructorParams] argList body
} else {
if {[llength $defaultArgs] != 2} {
error "Invalid constructor definition, expected ’?...? ?<argList> <body>?’"
}
lassign $defaultArgs argList body
}
}
uplevel [list proc $constructorName $argList $body]
return
}
# 531
##\brief Generate a getter procedure for a field
# \param[in] methodName name of the generated getter (may include namespace prefix)
# \param[in] fieldName name of the field to read
proc getter {methodName fieldName} {
# implementation of getter definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Getter for $fieldName
# \\param\[in\] this class instance
# \\return $fieldName value
proc $methodName {this} {
return [lindex \$this $fieldIdx]
}
}]
return
}
##\brief Generate a setter procedure for a field
# \param[in] methodName name of the generated setter (may include namespace prefix)
# \param[in] fieldName name of the field to write
proc setter {methodName fieldName} {
# implementation of setter definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Setter for $fieldName
# \\param\[in\] thisVar name of variable containing class instance
# \\param\[in\] value new value for $fieldName
proc $methodName {thisVar value} {
upvar \$thisVar this
lset this $fieldIdx \$value
}
}]
return
}
##\brief Generate an updater procedure for a field (copy-on-write safe)
# \param[in] methodName name of the generated updater (may include namespace prefix)
# \param[in] fieldName name of the field to update by reference
# \note The updater detaches the field to avoid unnecessary copying during updates
proc updater {methodName fieldName} {
# implementation of updater definition
set fieldIdx [uplevel [list set $fieldName]]
uplevel [subst -nocommands {
##\\brief Update $fieldName by reference
# \\param\[in\] thisVar name of variable containing class instance
# \\param\[out\] tempVar name of variable to hold $fieldName during update
# \\param\[in\] body script to execute with $fieldName in tempVar
# \\note Avoids copy-on-write by detaching field during update
proc $methodName {thisVar tempVar body} {
upvar \$thisVar this
upvar \$tempVar temp
set temp [lindex \$this $fieldIdx]
# break link with object to avoid copy-on-write
lset this $fieldIdx {}
try {
uplevel \$body
} finally {
lset this $fieldIdx \$temp
}
}
}]
}
##\brief Declare a method in the current class namespace
# \param[in] args Method declaration arguments: name, argList, body and options (-static, -upvar, -update, -override)
proc method {args} {
set isPublicEnabled [_getClassIsPublicEnabled]
set defaultArgs {}
set optDict {}
set numArgs [llength $args]
for {set i 0} {$i < $numArgs} {incr i} {
set arg [lindex $args $i]
if {$arg eq "-static" || $arg eq "-upvar"} {
dict set optDict $arg {}
} elseif {$arg eq "-update"} {
if {$i + 1 >= $numArgs} {
error "Method option ’$arg’ requires an argument"
}
dict set optDict $arg [lindex $args [incr i]]
} elseif {$arg eq "-override"} {
# Explicit override indicator
dict set optDict $arg {}
} elseif {$arg eq "-virtual"} {
dict set optDict $arg {}
} else {
lappend defaultArgs $arg
}
}
lassign $defaultArgs name argList body
# check valid option combinations
if {[dict exists $optDict -static]} {
if {[dict exists $optDict -upvar] || [dict exists $optDict -update]} {
error "Method cannot have both -static and -upvar or -update options"
}
}
if {[dict exists $optDict -update]} {
if {![dict exists $optDict -upvar]} {
# automatically add -upvar if -update is specified
dict set optDict -upvar {}
}
}
set finalArgList {}
set finalBody {}
if {[dict exists $optDict -upvar]} {
lappend finalArgList "thisVar"
append finalBody {
upvar $thisVar this
}
} elseif {![dict exists $optDict -static]} {
lappend finalArgList "this"
}
lappend finalArgList {*}$argList
set className [uplevel {namespace current}]
if {[dict exists $optDict -update]} {
set updateFields [dict get $optDict -update]
if {[llength $updateFields] == 0} {
error "-update option requires at least one field name"
}
foreach field $updateFields {
try {
set fieldIdx [uplevel [list set $field]]
} trap {} {} {
error "Field ’$field’ specified in -update option does not exist in class ’$className’"
}
append finalBody [subst -nocommands {
set $field [lindex \$this $fieldIdx]
lset this $fieldIdx {}
}]
}
append finalBody "try \{"
}
append finalBody $body
if {[dict exists $optDict -update]} {
append finalBody "\} finally \{"
foreach field $updateFields {
set fieldIdx [uplevel [list set $field]]
append finalBody [subst -nocommands {
lset this $fieldIdx \$$field
}]
}
append finalBody "\}"
}
if {!$isPublicEnabled} {
set name "my.$name"
}
if {[dict exists $optDict -override]} {
set parentNs [uplevel {set __parentClassNamespace}]
if {[info commands "${parentNs}::$name"] eq ""} {
error "Method ’$name’ does not override any method in parent class ’$parentNs’"
}
# If parent’s method is virtual (has base.<name>), auto-promote this override
# to a dispatcher so that deep inheritance dispatch works correctly
if {[uplevel {info exists __voo_is_virtual_class}] && \
[info commands "${parentNs}::base.$name"] ne ""} {
dict set optDict -virtual {}
}
}
if {[dict exists $optDict -virtual]} {
if {![uplevel {info exists __voo_is_virtual_class}]} {
error "Method ’$name’ is declared -virtual but ’[uplevel {namespace current}]’ is not a virtual class"
}
if {[dict exists $optDict -upvar] || [dict exists $optDict -update] || [dict exists $optDict -static]} {
error "Method ’$name’ cannot combine -virtual with -upvar, -update, or -static"
}
# Register base.<name> with the original body for direct parent calls from subclasses
uplevel [list proc "base.$name" $finalArgList $finalBody]
# Build dispatch body: route to concrete class implementation at runtime
set dispatchBody "set __voo_cls \[lindex \$this 0\]\n"
append dispatchBody "if \{\$__voo_cls ne \[namespace current\] && \[info commands \${__voo_cls}::$name\] ne {}\} \{\n"
append dispatchBody " return \[\${__voo_cls}::$name \$this"
foreach arg $argList {
append dispatchBody " \$$arg"
}
append dispatchBody "\]\n\}\n"
append dispatchBody "return \[base.$name \$this"
foreach arg $argList {
append dispatchBody " \$$arg"
}
append dispatchBody "\]"
set finalBody $dispatchBody
}
uplevel [list proc $name $finalArgList $finalBody]
return
}
# 726
##\brief Import one or more methods from parent class into the current (child) class namespace.
# \param[in] methods List of method names (or a single method name) to import from parent.
# \note Must be called inside a class declared with -extends. Methods are copied at class-definition time.
proc importMethods {methods} {
set parentNs [uplevel {set __parentClassNamespace}]
# Validate caller context and get parent namespace stored by -extends handling
if {$parentNs eq ""} {
error "importMethods can only be used inside a class declared with -extends"
}
# Normalize to a list of method names
if {[string length [string trim $methods]] == 0} {
return
}
if {[catch {llength $methods}]} {
set methodList [list $methods]
} else {
set methodList $methods
}
foreach methodName $methodList {
set fullMethodName "${parentNs}::$methodName"
# Validate parent method exists
if {[info commands $fullMethodName] eq ""} {
error "Method ’$methodName’ not found in parent class ’$parentNs’"
}
# Define a copy in the child namespace so unqualified calls resolve to child
set argList [info args $fullMethodName]
set body [info body $fullMethodName]
uplevel [list proc $methodName $argList $body]
}
return
}
namespace export *
}
# provide the package
package provide voo $::voo::version

4
src/tests/AGENTS.md

@ -12,9 +12,11 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc
## Local Contracts
- `runtests.tcl` is the primary source-tree test entry point.
- `runtests.tcl` is the primary source-tree test entry point; sublevel `all.tcl` files are a legacy pattern and are not required.
- Tests use `tcltest` unless a child AGENTS.md documents a different local harness.
- `runtests.tcl` excludes `AGENTS.md` and `*.tcl` helper files when discovering `.test` files.
- The single-process testinterp runs `package prefer latest` so alpha-versioned dev modules (`999999.0a1.0`) are preferred over stable bootsupport/vendored copies on unversioned `package require`.
- The testinterp module path includes `src/vendormodules` (and `src/vendormodules_tcl<major>` when present) so vendored dependencies such as `voo` resolve in tests.
- Tests should run against source modules and libraries from `src/`, not installed packages or root-level build outputs.
- Test files must `package require` any extra packages explicitly.
- Tcltest files must finish with `tcltest::cleanupTests`; missing cleanup produces a `missing-cleanupTests` runner warning and only untrusted observed testcase events.

256
src/tests/modules/opunk/console/testsuites/console/consoleclass.test

@ -0,0 +1,256 @@
package require tcltest
tcltest::configure {*}$::argv
package require voo
package require opunk::console
#Tests for the voo class ::opunk::Console (console as in/out channel pair value) and the anchored
#instance layer in ::opunk::console. All channel fixtures use chan pipe so results don't depend on
#how the test runner's stdio is connected. Env hints (TERM_PROGRAM/MSYSTEM) are cleared per-test
#where they affect heuristics.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
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 console_construct {constructor produces virtual-tagged 7 element value with channels set}\
-setup $common -body {
set c [::opunk::Console::new in_chan_x out_chan_y]
lappend result [llength $c]
lappend result [lindex $c 0]
lappend result [::opunk::Console::channels $c]
lappend result [::opunk::Console::get.o_can_respond $c]
}\
-cleanup {
}\
-result [list\
7\
::opunk::Console\
{in_chan_x out_chan_y}\
-1\
]
test console_default_size_construct {default size is 80x24 and overridable via new.args}\
-setup $common -body {
set c [::opunk::Console::new in_x out_y]
lappend result [::opunk::Console::default_size $c]
set c2 [::opunk::Console::new.args -o_in in_x -o_out out_y -o_default_size {columns 132 rows 50}]
lappend result [::opunk::Console::default_size $c2]
}\
-cleanup {
}\
-result [list\
{columns 80 rows 24}\
{columns 132 rows 50}\
]
test console_at_eof_pipe_states {at_eof certainty across pipe states (open-empty, open-data, closed-drained, invalid)}\
-setup $common -body {
lassign [chan pipe] rd wr
set c [::opunk::Console::new $rd $wr]
lappend result [::opunk::Console::at_eof $c]
puts -nonewline $wr "x"
flush $wr
lappend result [::opunk::Console::at_eof $c]
dict unset ::opunk::console::waiting_chunks $rd
chan configure $rd -blocking 0
read $rd
chan close $wr
#Windows pipe eof can take a moment to become visible to the reader after writer close
#- retry briefly rather than asserting on the first call
set r 0
for {set i 0} {$i < 50} {incr i} {
set r [::opunk::Console::at_eof $c]
if {$r} {
break
}
after 20
}
lappend result $r
chan close $rd
lappend result [::opunk::Console::at_eof $c]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
dict unset ::opunk::console::waiting_chunks $rd
}\
-result [list\
0\
0\
1\
1\
]
test console_at_eof_probe_preserves_data {probe-consumed byte is preserved in ::opunk::console::waiting_chunks}\
-setup $common -body {
lassign [chan pipe] rd wr
set c [::opunk::Console::new $rd $wr]
puts -nonewline $wr "abc"
flush $wr
lappend result [::opunk::Console::at_eof $c]
set waiting ""
if {[dict exists $::opunk::console::waiting_chunks $rd]} {
set waiting [join [dict get $::opunk::console::waiting_chunks $rd] ""]
dict unset ::opunk::console::waiting_chunks $rd
}
chan close $wr
chan configure $rd -blocking 0
lappend result [string cat $waiting [read $rd]]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
dict unset ::opunk::console::waiting_chunks $rd
}\
-result [list\
0\
abc\
]
test console_is_console_or_tty_heuristics {pipe: 0 without env hints, 1 with TERM_PROGRAM hint, 0 at eof despite hint}\
-setup [string cat $common $hints_save]\
-body {
lassign [chan pipe] rd wr
set c [::opunk::Console::new $rd $wr]
lappend result [::opunk::Console::is_console_or_tty $c]
set ::env(TERM_PROGRAM) mintty
lappend result [::opunk::Console::is_console_or_tty $c]
chan close $wr
chan configure $rd -blocking 0
read $rd
#Windows pipe eof can take a moment to become visible after writer close - retry briefly
set r 1
for {set i 0} {$i < 50} {incr i} {
set r [::opunk::Console::is_console_or_tty $c]
if {!$r} {
break
}
after 20
}
lappend result $r
}\
-cleanup [string cat {
unset -nocomplain ::env(TERM_PROGRAM)
} $hints_restore {
catch {chan close $wr}
catch {chan close $rd}
dict unset ::opunk::console::waiting_chunks $rd
}]\
-result [list\
0\
1\
0\
]
test console_size_fallback {size on pipe out channel returns the default size (no -winsize, no ANSI query)}\
-setup $common -body {
lassign [chan pipe] rd wr
set c [::opunk::Console::new $rd $wr]
lappend result [::opunk::Console::size $c]
set c2 [::opunk::Console::new.args -o_in $rd -o_out $wr -o_default_size {columns 132 rows 50}]
lappend result [::opunk::Console::size $c2]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
{columns 80 rows 24}\
{columns 132 rows 50}\
]
test console_anchoring {create/instancevar/instance/names/forget manage canonical instances}\
-setup $common -body {
lassign [chan pipe] rd wr
set v [opunk::console::create testanchor1 $rd $wr]
lappend result [expr {$v eq [opunk::console::instancevar testanchor1]}]
lappend result [expr {"testanchor1" in [opunk::console::names]}]
lappend result [expr {[::opunk::Console::channels [opunk::console::instance testanchor1]] eq [list $rd $wr]}]
opunk::console::forget testanchor1
lappend result [expr {"testanchor1" in [opunk::console::names]}]
}\
-cleanup {
catch {opunk::console::forget testanchor1}
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
1\
1\
1\
0\
]
test console_can_respond_settles_on_anchor {wrapper settles capability into the anchored instance}\
-setup [string cat $common $hints_save]\
-body {
lassign [chan pipe] rd wr
chan close $wr
chan configure $rd -blocking 0
read $rd
opunk::console::create testanchor2 $rd $wr
lappend result [::opunk::Console::get.o_can_respond [opunk::console::instance testanchor2]]
lappend result [opunk::console::can_respond testanchor2]
lappend result [::opunk::Console::get.o_can_respond [opunk::console::instance testanchor2]]
#settled value reused (still 0 even if hints now suggest otherwise)
set ::env(TERM_PROGRAM) mintty
lappend result [opunk::console::can_respond testanchor2]
}\
-cleanup [string cat {
unset -nocomplain ::env(TERM_PROGRAM)
} $hints_restore {
catch {opunk::console::forget testanchor2}
catch {chan close $rd}
dict unset ::opunk::console::waiting_chunks $rd
}]\
-result [list\
-1\
0\
0\
0\
]
test console_subclass_virtual_dispatch {subclass overriding size dispatches via slot 0 tag from parent-class call sites}\
-setup $common -body {
voo::class ::testspace::FixedSizeConsole -extends ::opunk::Console {
method size {} -override {
return [dict create columns 999 rows 111]
}
}
#child auto 'new' is positional over all fields (in out terminal_class size_mechanism default_size can_respond order per declaration)
set c [::testspace::FixedSizeConsole::new() ]
lappend result [lindex $c 0]
#call through the PARENT class namespace - dispatch must route to the subclass implementation
lappend result [::opunk::Console::size $c]
#non-overridden methods still work on the subclass value
lappend result [::opunk::Console::default_size $c]
}\
-cleanup {
namespace delete ::testspace::FixedSizeConsole
}\
-result [list\
::testspace::FixedSizeConsole\
{columns 999 rows 111}\
{columns 80 rows 24}\
]
}
tcltest::cleanupTests ;#needed to produce test summary.

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

@ -0,0 +1,192 @@
package require tcltest
tcltest::configure {*}$::argv
#min-version bounds document that these tests target the dev modules' API and protect against
#stable copies shadowing them if this file is sourced outside runtests.tcl (whose testinterp
#runs 'package prefer latest').
package require punk::console 999999.0a1.0-
package require opunk::console 999999.0a1.0-
#Tests for punk::console console-spec resolution (console_spec_resolve) and its integration into
#get_size and get_ansi_response_payload: -console/legacy-positional accepting a channel pair, an
#anchored opunk::console instance name, or an ::opunk::Console object value, with settled
#can_respond facts short-circuiting query emission.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
test consolespec_resolve_forms {channel pair, anchored name and object value resolve; bad specs error}\
-setup $common -body {
lassign [chan pipe] rd wr
#channel pair
set d [punk::console::console_spec_resolve [list $rd $wr]]
lappend result [expr {[dict get $d in] eq $rd && [dict get $d out] eq $wr && [dict get $d object] eq ""}]
#object value
set obj [::opunk::Console::new $rd $wr]
set d [punk::console::console_spec_resolve $obj]
lappend result [expr {[dict get $d in] eq $rd && [dict get $d out] eq $wr && [dict get $d object] eq $obj}]
#anchored instance name
opunk::console::create testspec1 $rd $wr
set d [punk::console::console_spec_resolve testspec1]
lappend result [expr {[dict get $d in] eq $rd && [dict get $d out] eq $wr}]
#unknown name errors
lappend result [catch {punk::console::console_spec_resolve no_such_instance_zzz}]
#malformed spec errors
lappend result [catch {punk::console::console_spec_resolve [list a b c]}]
}\
-cleanup {
catch {opunk::console::forget testspec1}
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
1\
1\
1\
1\
1\
]
test consolespec_get_size_legacy_compat {legacy positional and -console channel-pair forms both work}\
-setup $common -body {
lassign [chan pipe] rd wr
chan close $wr
set s1 [punk::console::get_size [list $rd stdout]]
lappend result [string is integer -strict [dict get $s1 columns]]
set s2 [punk::console::get_size -console [list $rd stdout]]
lappend result [string is integer -strict [dict get $s2 rows]]
#bad option form errors
lappend result [catch {punk::console::get_size -notanoption x}]
}\
-cleanup {
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}\
-result [list\
1\
1\
1\
]
test consolespec_get_size_settled_object {object with settled can_respond 0 gets its default_size back (no tput, no query)}\
-setup $common -body {
lassign [chan pipe] rd wr
set obj [::opunk::Console::new.args -o_in $rd -o_out $wr -o_default_size {columns 132 rows 50}]
::opunk::Console::set.o_can_respond obj 0
lappend result [punk::console::get_size -console $obj]
#same via legacy positional slot
lappend result [punk::console::get_size $obj]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
{columns 132 rows 50}\
{columns 132 rows 50}\
]
test consolespec_get_size_anchored_name {anchored instance name resolves and settled capability is honoured}\
-setup $common -body {
lassign [chan pipe] rd wr
opunk::console::create testspec2 $rd $wr
upvar #0 [opunk::console::instancevar testspec2] cobj
::opunk::Console::set.o_can_respond cobj 0
lappend result [punk::console::get_size -console testspec2]
}\
-cleanup {
catch {opunk::console::forget testspec2}
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
{columns 80 rows 24}\
]
test consolespec_default_console_proc {default_console creates and returns the anchored 'default' instance idempotently}\
-setup $common -body {
catch {opunk::console::forget default}
lappend result [punk::console::default_console]
lappend result [expr {"default" in [opunk::console::names]}]
lappend result [::opunk::Console::channels [opunk::console::instance default]]
lappend result [punk::console::default_console]
lappend result [llength [opunk::console::names]]
}\
-cleanup {
catch {opunk::console::forget default}
}\
-result [list\
default\
1\
{stdin stdout}\
default\
1\
]
test consolespec_default_console_autoattach {console_spec_resolve attaches the default console object only for its own channel pair}\
-setup $common -body {
catch {opunk::console::forget default}
lassign [chan pipe] rd wr
opunk::console::create default $rd $wr
set d [punk::console::console_spec_resolve [list $rd $wr]]
lappend result [expr {[dict get $d object] eq [opunk::console::instance default]}]
set d2 [punk::console::console_spec_resolve {stdin stdout}]
lappend result [expr {[dict get $d2 object] eq ""}]
}\
-cleanup {
catch {opunk::console::forget default}
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
1\
1\
]
test consolespec_default_console_governs_get_size {settled default console supplies its default_size to plain get_size calls on its channel pair}\
-setup $common -body {
catch {opunk::console::forget default}
lassign [chan pipe] rd wr
set obj [::opunk::Console::new.args -o_in $rd -o_out $wr -o_default_size {columns 61 rows 19}]
::opunk::Console::set.o_can_respond obj 0
#anchoring directly at the documented canonical location
set ::opunk::console::instances::default $obj
#plain channel-pair call - no object passed - default console consulted via auto-attach
lappend result [punk::console::get_size [list $rd $wr]]
}\
-cleanup {
catch {opunk::console::forget default}
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
{columns 61 rows 19}\
]
test consolespec_query_refused_for_settled_object {get_cursor_pos with a settled-cannot-respond object errors immediately instead of querying}\
-setup $common -body {
lassign [chan pipe] rd wr
set obj [::opunk::Console::new $rd $wr]
::opunk::Console::set.o_can_respond obj 0
set ms_start [clock milliseconds]
lappend result [catch {punk::console::get_cursor_pos $obj} emsg]
set elapsed [expr {[clock milliseconds] - $ms_start}]
lappend result [expr {$elapsed < 2000}]
lappend result [string match "*unable to respond*" $emsg]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
}\
-result [list\
1\
1\
1\
]
}
tcltest::cleanupTests ;#needed to produce test summary.

210
src/tests/modules/punk/console/testsuites/console/probes.test

@ -0,0 +1,210 @@
package require tcltest
tcltest::configure {*}$::argv
#min-version bound documents that these tests target the dev module's API (input_at_eof etc.) and protects
#against stable bootsupport copies shadowing the alpha dev version if this file is sourced outside
#runtests.tcl (whose testinterp runs 'package prefer latest' making the bound redundant there).
package require punk::console 999999.0a1.0-
#Tests for punk::console channel probe helpers used to decide whether ANSI terminal queries can be
#emitted/answered: input_at_eof (certainty, with non-blocking probe read for deferred pipe eof),
#is_input_console_or_tty (heuristic incl. env hints), and the get_size non-terminal fallback.
#All fixtures use chan pipe so results do not depend on how the test runner's stdin/stdout are connected.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
#is_input_console_or_tty consults env(TERM_PROGRAM)/env(MSYSTEM) as mintty-without-winpty hints.
#Save/clear them around each such test so results don't depend on the launching environment.
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 input_at_eof_pipe_open_empty {open pipe with no data is not at eof (could still receive)}\
-setup $common -body {
lassign [chan pipe] rd wr
lappend result [punk::console::input_at_eof $rd]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}\
-result [list\
0\
]
test input_at_eof_pipe_open_data {open pipe with unread data is not at eof}\
-setup $common -body {
lassign [chan pipe] rd wr
puts -nonewline $wr "x"
flush $wr
lappend result [punk::console::input_at_eof $rd]
}\
-cleanup {
#probe may have consumed and parked a byte - clear it: channel names are reused by Tcl
#so stale waiting chunks would leak into later tests' fixtures
catch {chan close $wr}
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}\
-result [list\
0\
]
test input_at_eof_probe_preserves_data {byte consumed by the probe read is preserved in input_chunks_waiting}\
-setup $common -body {
lassign [chan pipe] rd wr
puts -nonewline $wr "abc"
flush $wr
lappend result [punk::console::input_at_eof $rd]
#probe may have consumed a byte - full data must be reconstructable from waiting chunks + channel
set waiting ""
if {[info exists ::punk::console::input_chunks_waiting($rd)]} {
set waiting [join $::punk::console::input_chunks_waiting($rd) ""]
set ::punk::console::input_chunks_waiting($rd) [list]
}
chan close $wr
chan configure $rd -blocking 0
lappend result [string cat $waiting [read $rd]]
}\
-cleanup {
catch {chan close $wr}
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}\
-result [list\
0\
abc\
]
test input_at_eof_pipe_closed_pending {closed-writer pipe with unread data is not at eof yet}\
-setup $common -body {
lassign [chan pipe] rd wr
puts -nonewline $wr "x"
flush $wr
chan close $wr
lappend result [punk::console::input_at_eof $rd]
}\
-cleanup {
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}\
-result [list\
0\
]
test input_at_eof_pipe_closed_drained {closed-writer drained pipe reports eof (probe forces deferred eof detection)}\
-setup $common -body {
lassign [chan pipe] rd wr
chan close $wr
#no read has occurred - eof flag not yet set without the probe
lappend result [punk::console::input_at_eof $rd]
}\
-cleanup {
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}\
-result [list\
1\
]
test input_at_eof_closed_channel {closed/invalid channel reports eof (unusable)}\
-setup $common -body {
lassign [chan pipe] rd wr
chan close $wr
chan close $rd
lappend result [punk::console::input_at_eof $rd]
}\
-cleanup {
}\
-result [list\
1\
]
test is_input_console_or_tty_pipe_no_hints {open pipe with terminal env hints cleared is not considered a console/tty}\
-setup [string cat $common $hints_save]\
-body {
lassign [chan pipe] rd wr
lappend result [punk::console::is_input_console_or_tty $rd]
}\
-cleanup [string cat $hints_restore {
catch {chan close $wr}
catch {chan close $rd}
}]\
-result [list\
0\
]
test is_input_console_or_tty_pipe_hinted {open pipe with TERM_PROGRAM hint set is treated as possible terminal (mintty-without-winpty)}\
-setup [string cat $common $hints_save]\
-body {
set ::env(TERM_PROGRAM) mintty
lassign [chan pipe] rd wr
lappend result [punk::console::is_input_console_or_tty $rd]
}\
-cleanup [string cat {
unset -nocomplain ::env(TERM_PROGRAM)
} $hints_restore {
catch {chan close $wr}
catch {chan close $rd}
}]\
-result [list\
1\
]
test is_input_console_or_tty_eof_beats_hints {eof pipe is not a terminal even with env hints set}\
-setup [string cat $common $hints_save]\
-body {
set ::env(TERM_PROGRAM) mintty
lassign [chan pipe] rd wr
chan close $wr
lappend result [punk::console::is_input_console_or_tty $rd]
}\
-cleanup [string cat {
unset -nocomplain ::env(TERM_PROGRAM)
} $hints_restore {
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}]\
-result [list\
0\
]
test get_size_nonterminal_fallback {get_size on pipe channels returns integer size promptly (skips ANSI query mechanisms)}\
-setup [string cat $common $hints_save]\
-body {
lassign [chan pipe] rd wr
chan close $wr
#shape assertion only: tput may or may not be available/functional, so exact values vary
#(with neither tput nor terminal the documented default is columns 80 rows 24)
set sized [punk::console::get_size [list $rd stdout]]
lappend result [string is integer -strict [dict get $sized columns]]
lappend result [string is integer -strict [dict get $sized rows]]
}\
-cleanup [string cat $hints_restore {
catch {chan close $rd}
array unset ::punk::console::input_chunks_waiting $rd
}]\
-result [list\
1\
1\
]
}
tcltest::cleanupTests ;#needed to produce test summary.

9
src/tests/runtests.tcl

@ -250,6 +250,11 @@ append ifneeded_script [punk::tcltestrun::tm_path_additional_ifneeded [file norm
lappend test_tmlist [file normalize $test_base/../modules_tcl$tcl_major]
append ifneeded_script [punk::tcltestrun::tm_path_additional_ifneeded [file normalize $test_base/../modules]]
lappend test_tmlist [file normalize $test_base/../bootsupport/modules]
#vendored module dependencies (e.g voo) must be resolvable by modules/tests using them
lappend test_tmlist [file normalize $test_base/../vendormodules]
if {[file isdirectory $test_base/../vendormodules_tcl$tcl_major]} {
lappend test_tmlist [file normalize $test_base/../vendormodules_tcl$tcl_major]
}
set test_auto_path [list]
lappend test_auto_path [file normalize $test_base/../lib]
lappend test_auto_path [file normalize $test_base/../lib/tcl$tcl_major]
@ -422,6 +427,10 @@ foreach testfile_relative $testfiles {
if {$singleproc} {
#in single process mode we can just source the test file in an interp - but we need to set up the environment for the test file - such as the module search path to ensure it picks up the unbuilt modules under development rather than any installed versions.
interp create testinterp
#latest mode so the alpha-versioned dev modules (999999.0a1.0) are preferred over stable
#bootsupport/vendored copies on unversioned 'package require' (default 'stable' mode would
#silently select e.g bootsupport punk::console 0.1.x over the dev module under test)
testinterp eval {package prefer latest}
testinterp eval {tcl::tm::remove [tcl::tm::list]}
testinterp eval [list tcl::tm::add {*}$test_tmlist]
testinterp eval [list set ::auto_path $test_auto_path]

6
src/tests/shell/AGENTS.md

@ -14,6 +14,12 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- Tests here may exercise process execution, stdin, stdout, stderr, and shell filtering behavior.
- Keep command inputs and expected outputs explicit so failures can be reproduced outside the aggregate runner.
- Avoid relying on installed packages or root-level build outputs unless the test explicitly targets a built executable.
- `testsuites/punkexe/` targets a built punk executable: resolved from `env(PUNK_SHELL_TEST_EXE)`, else `<projectroot>/bin/punk902z.exe`, else `<projectroot>/bin/punkshell902`; tests auto-skip via the `punkexeavailable` constraint when none is found.
- Tests that spawn the built executable must be hang-proof: use the event-loop `punk_run` pattern (pipe stdin, half-close for EOF, timeout then force-kill) rather than plain `exec`, because known failure modes leave the child waiting on a reopened console.
- `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling.
- punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate` (requires interactive `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`.
- GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build.
- The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset.
## Work Guidance

383
src/tests/shell/testsuites/punkexe/shellexit.test

@ -0,0 +1,383 @@
package require tcltest
#Tests for termination behaviour of a built punk executable's 'shell' subcommand when stdin is a pipe.
#Diagnosed issues these tests guard (see punk::repl repl::start shutdown ordering and app-punkshell eof handling):
# 1) 'shell <script>' where the script calls exit, with data still pending on piped stdin,
# can service that pending input after the codethread is torn down, producing:
# error2 in repl_process_data: invalid thread handle ""
# (stale 'after idle' reader re-registration racing repl shutdown)
# 2) An error in piped input aborts processing of subsequent piped commands (so a trailing 'exit'
# never runs) and the eof path reopens the console (CONIN$ / /dev/tty) and restarts the repl,
# leaving a process that never terminates when no interactive user is present.
#
#These tests assert the fixed behaviour: clean termination, correct exitcode propagation and no
#thread-handle error spew. Fixes: repl::start cancels its deferred stdin reader registration on completion,
#repl_process_data drops input arriving after codethread teardown, and app-punkshell's eof handling follows
#the env(PUNK_PIPE_EOF) policy: 'exit' always terminates, 'interactive' always attempts the console
#reopen, unset uses the heuristic (reopen console if one can be opened, else terminate cleanly).
#
#The target executable is resolved from env(PUNK_SHELL_TEST_EXE) if set, else <projectroot>/bin/punk902z.exe
#then <projectroot>/bin/punkshell902. Tests are skipped (constraint punkexeavailable) if none found.
#
#NOTE: the tests exercise the BUILT executable - after changing punk::repl or app-punkshell source, rebuild
#with 'make.tcl packages', then 'make.tcl vfscommonupdate' (interactive y confirmation), then 'make.tcl project'
#or these tests will run against a stale binary.
#
#WARNING for interactive runs: a regression in the eof/error termination path causes the spawned executable
#to reopen the launching console and wait for input - the harness kills it after punk_run_timeout_ms, but
#keystrokes typed into the console during that window may be consumed.
namespace eval ::testspace {
namespace import ::tcltest::*
variable testdir [file dirname [file normalize [info script]]]
#<projectroot>/src/tests/shell/testsuites/punkexe -> 5 levels up to <projectroot>
variable projectroot [file normalize [file join $testdir .. .. .. .. ..]]
variable punkexe ""
if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} {
set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)]
} else {
foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] {
if {[file exists $candidate]} {
set punkexe $candidate
break
}
}
}
testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}]
#GOAL tests exercise console-dependent behaviour (interactive drop-in after piped input) and briefly
#attach a live shell to the launching console. They are excluded from normal runs; enable with
#env PUNK_TEST_GOALS=1 in a console-attached environment.
testConstraint punkgoals [expr {[info exists ::env(PUNK_TEST_GOALS)] && [string is true -strict $::env(PUNK_TEST_GOALS)]}]
variable punk_run_timeout_ms 15000
variable runstate
array set runstate {}
proc punk_run_read {chan} {
variable runstate
append runstate(output) [read $chan]
if {[chan eof $chan]} {
chan event $chan readable {}
set runstate(done) eof
}
}
#Run the target executable with args, feed stdin_data via pipe then half-close to signal EOF.
#Returns dict: timedout 0|1, exitcode <int|kill-info|"">, output <combined stdout+stderr>
#On timeout the process (and any pipeline members) are forcibly killed so the suite cannot hang.
#The child is launched with PUNK_PIPE_EOF=exit so eof-on-piped-stdin terminates deterministically
#regardless of whether the test environment has a console (see app-punkshell eof policy).
proc punk_run {exe cmdargs stdin_data} {
variable runstate
variable punk_run_timeout_ms
array unset runstate
set runstate(output) ""
set runstate(done) ""
set env_had [info exists ::env(PUNK_PIPE_EOF)]
if {$env_had} {
set env_save $::env(PUNK_PIPE_EOF)
}
set ::env(PUNK_PIPE_EOF) exit
set chan [open |[list $exe {*}$cmdargs 2>@1] r+]
if {$env_had} {
set ::env(PUNK_PIPE_EOF) $env_save
} else {
unset ::env(PUNK_PIPE_EOF)
}
set pids [pid $chan]
chan configure $chan -blocking 0 -translation binary
catch {
puts -nonewline $chan $stdin_data
flush $chan
chan close $chan write ;#half-close - child sees EOF on stdin
}
set timerid [after $punk_run_timeout_ms [list set [namespace current]::runstate(done) timeout]]
chan event $chan readable [list [namespace current]::punk_run_read $chan]
while {$runstate(done) eq ""} {
vwait [namespace current]::runstate(done)
}
after cancel $timerid
chan event $chan readable {}
set timedout [expr {$runstate(done) eq "timeout"}]
set exitcode ""
if {$timedout} {
foreach p $pids {
if {$::tcl_platform(platform) eq "windows"} {
catch {exec {*}[auto_execok taskkill] /F /PID $p}
} else {
catch {exec kill -9 $p}
}
}
catch {close $chan}
} else {
catch {chan configure $chan -blocking 1}
if {[catch {close $chan} errmsg erropts]} {
set ecode [dict get $erropts -errorcode]
switch -- [lindex $ecode 0] {
CHILDSTATUS {
set exitcode [lindex $ecode 2]
}
default {
set exitcode $ecode
}
}
} else {
set exitcode 0
}
}
return [dict create timedout $timedout exitcode $exitcode output $runstate(output)]
}
proc punk_run_read_pulse {chan} {
variable runstate
append runstate(output) [read $chan]
if {[chan eof $chan]} {
chan event $chan readable {}
set runstate(done) eof
}
set runstate(pulse) 1
}
#Run the target executable, feed stdin_data then half-close (EOF). Wait until marker appears in output
#(or eof/timeout), then wait a further alive_ms grace period and report whether the process is still running.
#The process is always killed before returning - callers use this to assert the shell does NOT exit
#immediately after consuming piped input.
#Returns dict: marker_seen 0|1, alive 0|1, output <combined stdout+stderr>
proc punk_run_expect_alive {exe cmdargs stdin_data marker alive_ms} {
variable runstate
variable punk_run_timeout_ms
array unset runstate
set runstate(output) ""
set runstate(done) ""
set runstate(pulse) 0
set runstate(grace_elapsed) 0
#child launched with PUNK_PIPE_EOF unset - eof policy is the heuristic (attempt console reopen)
#mirroring a user's plain invocation. Requires the test environment to have an openable console for
#the alive assertion to hold.
set env_had [info exists ::env(PUNK_PIPE_EOF)]
if {$env_had} {
set env_save $::env(PUNK_PIPE_EOF)
unset ::env(PUNK_PIPE_EOF)
}
set chan [open |[list $exe {*}$cmdargs 2>@1] r+]
if {$env_had} {
set ::env(PUNK_PIPE_EOF) $env_save
}
set pids [pid $chan]
chan configure $chan -blocking 0 -translation binary
catch {
puts -nonewline $chan $stdin_data
flush $chan
chan close $chan write ;#half-close - child sees EOF on stdin
}
set ns [namespace current]
set timerid [after $punk_run_timeout_ms [list apply [list ns {
set ${ns}::runstate(done) timeout
set ${ns}::runstate(pulse) 1
}] $ns]]
chan event $chan readable [list ${ns}::punk_run_read_pulse $chan]
while {$runstate(done) eq "" && [string first $marker $runstate(output)] < 0} {
vwait ${ns}::runstate(pulse)
}
set marker_seen [expr {[string first $marker $runstate(output)] >= 0}]
if {$runstate(done) eq ""} {
#marker seen and process still connected - grace wait to check it doesn't exit shortly after
set graceid [after $alive_ms [list apply [list ns {
set ${ns}::runstate(grace_elapsed) 1
set ${ns}::runstate(pulse) 1
}] $ns]]
while {!$runstate(grace_elapsed) && $runstate(done) eq ""} {
vwait ${ns}::runstate(pulse)
}
after cancel $graceid
}
after cancel $timerid
chan event $chan readable {}
#if we never saw eof on the pipe, the process is still running
set alive [expr {$runstate(done) eq ""}]
foreach p $pids {
if {$::tcl_platform(platform) eq "windows"} {
catch {exec {*}[auto_execok taskkill] /F /PID $p}
} else {
catch {exec kill -9 $p}
}
}
catch {close $chan}
return [dict create marker_seen $marker_seen alive $alive output $runstate(output)]
}
variable common {
set result ""
}
# -- baseline canaries: pass today - guard that fixes don't break working paths ------------------
test shellpipe_baseline_exit {piped 'puts;exit' to 'shell' runs command, terminates, exitcode 0}\
-constraints punkexeavailable\
-setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list shell] "puts hello-punkexe-test\nexit\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "hello-punkexe-test" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
}\
-cleanup {
}\
-result [list\
0\
1\
0\
]
test shellpipe_exitcode_propagation {piped 'exit 42' to 'shell' propagates exitcode 42}\
-constraints punkexeavailable\
-setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list shell] "exit 42\n"]
lappend result [dict get $rd timedout]
lappend result [dict get $rd exitcode]
}\
-cleanup {
}\
-result [list\
0\
42\
]
# -- desired-behaviour tests: expected to FAIL until repl shutdown/eof handling fixed ------------
test shellpipe_script_exit_pending_stdin {'shell <script-calling-exit>' with pending piped stdin: no thread-handle error, clean exit}\
-constraints punkexeavailable\
-setup {
set result ""
set exitscript [tcltest::makeFile {exit {*}$::argv} punkexe_scriptexit.tcl]
}\
-body {
variable punkexe
set rd [punk_run $punkexe [list shell $exitscript] "puts test\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "invalid thread handle" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
}\
-cleanup {
tcltest::removeFile punkexe_scriptexit.tcl
}\
-result [list\
0\
0\
0\
]
test shellpipe_error_then_exit_terminates {piped error followed by 'exit 7': process must still terminate (no console-reopen hang)}\
-constraints punkexeavailable\
-setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list shell] "nosuchcommand_zzz_punktest\nexit 7\n"]
lappend result [dict get $rd timedout]
}\
-cleanup {
}\
-result [list\
0\
]
test shellpipe_help_env_renders {'help env' renders with piped (non-terminal) stdin: ANSI size query skipped, default width used}\
-constraints punkexeavailable\
-setup $common -body {
#relies on scriptlib/help_exit.tcl resolving via the lib: mechanism (vfs scriptlib or bin-sibling scriptlib)
variable punkexe
set rd [punk_run $punkexe [list shell lib:help_exit env] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PUNK_PIPE_EOF" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
}\
-cleanup {
}\
-result [list\
0\
1\
0\
]
test shellpipe_default_console_anchored {repl startup anchors the process-default console object (opunk::console instance 'default') in the repl thread's root interp}\
-constraints punkexeavailable\
-setup $common -body {
variable punkexe
#The anchor lives in the repl thread's root interp (where repl::init runs) - not in the
#code interp where piped commands evaluate. Two-hop probe:
# code interp --(thread::send -async to own thread: lands in codethread ROOT interp,
# which holds the replthread id)--> sync send to replthread root interp.
set probe {thread::send -async [thread::id] {puts stdout REPLPROBE=[thread::send $::punk::repl::codethread::replthread {interp eval {} {info exists ::opunk::console::instances::default}}]}}
set rd [punk_run $punkexe [list shell] "$probe\nexit\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "REPLPROBE=1" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
}\
-cleanup {
}\
-result [list\
0\
1\
0\
]
# -- GOAL tests (constraint punkgoals - set env PUNK_TEST_GOALS=1 to run) ------------------------
#Workflow under test: echo 'puts "some tcl"' | punk902z shell (with or without a trailing script)
#runs the piped commands then drops the user into the interactive punk shell on the console.
#Timing-based approximation: after piped input EOF (without an explicit exit) the process must still
#be running after a grace period, rather than exiting immediately.
#The eof policy discriminator is env(PUNK_PIPE_EOF): these tests spawn with it UNSET (heuristic -
#console reopen attempted) while the termination guards above spawn with it set to 'exit' - so both
#behaviours are covered deterministically in one suite. These tests additionally require the test
#environment to have an openable console (CONIN$ / /dev/tty) - headless runners will fail the alive check.
#The spawned shell may consume keystrokes from the launching console for up to the grace period
#before being killed.
test shellpipe_eof_then_interactive_shell {GOAL: piped commands without exit; shell remains running after stdin EOF (interactive drop-in)}\
-constraints {punkexeavailable punkgoals}\
-setup $common -body {
variable punkexe
set rd [punk_run_expect_alive $punkexe [list shell] "puts marker-interactive-goal\n" "marker-interactive-goal" 3000]
lappend result [dict get $rd marker_seen]
lappend result [dict get $rd alive]
}\
-cleanup {
}\
-result [list\
1\
1\
]
test shellpipe_script_and_eof_then_interactive_shell {GOAL: 'shell <script>' plus piped commands without exit; shell remains running after stdin EOF}\
-constraints {punkexeavailable punkgoals}\
-setup {
set result ""
set goalscript [tcltest::makeFile {puts "script-ran-goal"} punkexe_goalscript.tcl]
}\
-body {
variable punkexe
set rd [punk_run_expect_alive $punkexe [list shell $goalscript] "puts marker-goal2\n" "marker-goal2" 3000]
lappend result [expr {[string first "script-ran-goal" [dict get $rd output]] >= 0}]
lappend result [dict get $rd marker_seen]
lappend result [dict get $rd alive]
}\
-cleanup {
tcltest::removeFile punkexe_goalscript.tcl
}\
-result [list\
1\
1\
1\
]
}
tcltest::cleanupTests ;#needed to produce test summary.

77
src/vfs/_vfscommon.vfs/lib/app-punkshell/punkshell.tcl

@ -465,29 +465,59 @@ dict with prevglobal {}
}
eof {
puts "app-punkshell eof on input channel [lindex $result 1]"
#eof on the input channel - usually piped/redirected stdin reaching its end (also e.g ctrl-z/ctrl-d at a console)
#Historical behaviour is to reopen the console and drop into an interactive repl - desirable when a user is
#present, but a permanent hang for automated callers (CI/agents/exec) since nobody will ever type anything.
#Policy selected via env(PUNK_PIPE_EOF) (documented in punk::config punk_env_vars_config - see 'help env'):
# exit - always terminate
# interactive - always attempt the console reopen (terminates exitcode 1 if no console can be opened)
# unset/other - heuristic: attempt the console reopen, terminate cleanly if no console is available
#Note the heuristic cannot detect a console that exists but has no user watching (some CI/agent harnesses) -
#automated callers should set PUNK_PIPE_EOF=exit (or ensure their piped input ends with an explicit exit).
#review - propagate a nonzero exitcode if errors occurred in the piped input?
#review - should this consult the punk::config running configuration (pipe_eof) rather than raw ::env?
set eof_policy heuristic
if {[info exists ::env(PUNK_PIPE_EOF)]} {
switch -exact -- [string tolower $::env(PUNK_PIPE_EOF)] {
exit - quit - terminate {
set eof_policy exit
}
interactive - repl - shell {
set eof_policy interactive
}
}
}
if {$eof_policy eq "exit"} {
exit 0
}
if {$::tcl_platform(platform) eq "windows"} {
#set console_input_source "CON"
set console_input_source {CONIN$}
} else {
#/dev/tty - reference to the controlling terminal for a process
#review/test
set console_input_source "/dev/tty"
}
if {[catch {open $console_input_source r} s]} {
puts stderr "app-punkshell: no console available for interactive shell after eof on input channel ($console_input_source: $s)"
if {$eof_policy eq "interactive"} {
exit 1
}
exit 0
}
set errdeviceinfo [shellfilter::stack::new punkshellerr -settings [list -tag "punkshellerr" -buffering none -raw 1 -syslog {} -file {}]]
set outdeviceinfo [shellfilter::stack::new punkshellout -settings [list -tag "punkshellout" -buffering none -raw 1 -syslog {} -file {}]]
#reopen stdin as console???
if {$::tcl_platform(platform) eq "windows"} {
#set s [open "CON" r]
set s [open {CONIN$} r]
if {[package provide twapi] ne ""} {
set h [twapi::get_tcl_channel_handle $s in]
twapi::SetStdHandle -10 $h
}
puts stderr "restarting repl on inputchannel:$s"
#return [repl::start $s -title "reopen_stdin a"]
} else {
#/dev/tty - reference to the controlling terminal for a process
#review/test
set s [open "/dev/tty" r]
}
puts stderr "input chan '$s': [chan configure $s]"
puts stderr "punkshell.tcl restarting repl on inputchannel:$s"
#puts stderr "input chan '$s': [chan configure $s]"
set ::tcl_interactive [punk::repl::is_interactive $s]
puts "app-punkshell: tcl_interactive: $::tcl_interactive"
#puts "app-punkshell: tcl_interactive: $::tcl_interactive"
set exitinfo [punkshell::do_shell $s]
@ -513,8 +543,25 @@ dict with prevglobal {}
}
set result [dict get $exitinfo result]
set firstword [lindex $result 0]
puts stdout "result: $result"
#review
switch -- $firstword {
exit {
exit [lindex $result 1]
}
quit {
puts stdout "result: [lrange $result 1 end]"
exit 0
}
interrupt {
#exit with 128 + signal number - unix-like compatibility.
exit [expr {[lindex $result 1] + 128}]
}
default {
#review - we should get a recognised firstword.
puts stderr "unexpected result from repl after reopened input channel: $result"
flush stdout
exit 1
}
}
}
default {
#review - we should get a recognised firstword.

361
src/vfs/_vfscommon.vfs/modules/opunk/console-0.1.0.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.0
# 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).
#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.
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.0
}]
return

2
src/vfs/_vfscommon.vfs/modules/punk/config-0.1.tm

@ -244,6 +244,7 @@ tcl::namespace::eval punk::config {
syslog_active 0
auto_exec_mechanism exec
auto_noexec 0
pipe_eof {}
} xdg_config_home $default_xdg_config_home {*}{
} xdg_data_home $default_xdg_data_home {*}{
} xdg_cache_home $default_xdg_cache_home {*}{
@ -284,6 +285,7 @@ tcl::namespace::eval punk::config {
PUNK_LOGFILE_STDOUT {type string}
PUNK_LOGFILE_STDERR {type string}
PUNK_LOGFILE_ACTIVE {type string}
PUNK_PIPE_EOF {type string default "" help "punk shell behaviour when its input channel\n(usually piped/redirected stdin) reaches eof.\n exit - always terminate\n interactive - reopen console, interactive repl\n unset/other - console if available, else exit"}
PUNK_SYSLOG_STDOUT {type string}
PUNK_SYSLOG_STDERR {type string}
PUNK_SYSLOG_ACTIVE {type string}

4520
src/vfs/_vfscommon.vfs/modules/punk/console-0.1.2.tm

File diff suppressed because it is too large Load Diff

4618
src/vfs/_vfscommon.vfs/modules/punk/console-0.1.3.tm

File diff suppressed because it is too large Load Diff

4645
src/vfs/_vfscommon.vfs/modules/punk/console-0.1.4.tm

File diff suppressed because it is too large Load Diff

4201
src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.3.tm

File diff suppressed because it is too large Load Diff

4222
src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.4.tm

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