Browse Source
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
53 changed files with 80901 additions and 53 deletions
@ -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 |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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 |
||||
|
||||
@ -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) |
||||
@ -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 |
||||
@ -0,0 +1,3 @@
|
||||
0.1.0 |
||||
#First line must be a semantic version number |
||||
#all other lines are ignored. |
||||
@ -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. |
||||
|
||||
@ -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 |
||||
|
||||
@ -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 |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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 |
||||
|
||||
@ -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 |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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 |
||||
|
||||
@ -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. |
||||
@ -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. |
||||
@ -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. |
||||
@ -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. |
||||
@ -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 |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue