From 3a85fa85f5c705ad77dce6963b4414f3834a7cce Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sat, 4 Jul 2026 21:44:17 +1000 Subject: [PATCH] punk::console 0.4.0: mode setters resolve console specs, -minsize 2 removed, mode caches canonicalised Migration plan phase 1: - dec_set_mode/dec_unset_mode/ansi_set_mode/ansi_unset_mode and cursor_style resolve -console via console_spec_resolve instead of '[lindex $terminal 1]' - anchored opunk::console instance names and ::opunk::Console object values now work (previously silently wrote to the wrong target) - the fourteen '-console -type list -minsize 2' declarations replaced with new shared fragment ::punk::console::argdoc::console_opts (the -minsize 2 constraint rejected 1-element instance-name specs at parse time); show_input_response and cell_size -console docs unified on the fragment - dec_has_mode/ansi_has_mode cache on the canonical {in out} pair - all spec forms addressing the same console share one cache entry - test_is_vt52 option -inoutchannels renamed to -console per the documented convention (experimental/broken proc, no callers) - set/unset mode argdocs corrected: are semicolon delimited AGENTS.md records the fragment usage and the -minsize 2 prohibition. 3 new tests in consolespec.test (setter spec forms, cache canonicalisation via mixed specs, test_is_vt52 -console); full console suite 52/52 on Tcl 9.0.3. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- src/modules/punk/AGENTS.md | 2 +- src/modules/punk/console-999999.0a1.0.tm | 81 +++++++----- src/modules/punk/console-buildversion.txt | 7 +- .../testsuites/console/consolespec.test | 121 ++++++++++++++++++ 4 files changed, 177 insertions(+), 34 deletions(-) diff --git a/src/modules/punk/AGENTS.md b/src/modules/punk/AGENTS.md index 954b260d..01c10884 100644 --- a/src/modules/punk/AGENTS.md +++ b/src/modules/punk/AGENTS.md @@ -28,7 +28,7 @@ Source of truth for all modules under the `punk::*` namespace. This is the prima ## Work Guidance - New modules under `punk::*` should be created as `/-999999.0a1.0.tm` following the namespace-to-path convention. -- punk::console is incrementally migrating positional `inoutchannels` arguments to the documented `-console` convention. A `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Converted procs accept the legacy leading positional channel-pair during the transition; convert remaining positional procs opportunistically when touched, with tests. +- punk::console is incrementally migrating positional `inoutchannels` arguments to the documented `-console` convention. A `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Converted procs accept the legacy leading positional channel-pair during the transition; convert remaining positional procs opportunistically when touched, with tests. PUNKARGS definitions include the `-console` option via the shared fragments `::punk::console::argdoc::console_opts` (query/set functions) or `::punk::console::argdoc::console_emit_opts` (emit functions) rather than duplicating the option text; never re-add a `-minsize 2` constraint to `-console` (it rejects instance-name specs). - punk::console emit-side functions (the `punk::console::ansi::*` emit wrappers, mouse/paste toggles, `vt52`, `set_tabstop_width`, `titleset`, top-level `move`, and the width-test probes) accept an optional trailing `-console ` pair, parsed manually for performance by `punk::console::internal::opt_console_out`/`opt_console_channels` (`_var` variants for procs whose args-tail also carries row/col/data triples). Each carries a documentation-only PUNKARGS definition that includes the shared `::punk::console::argdoc::console_emit_opts` fragment via `punk::args::resolved_def`; keep manual parsing and PUNKARGS synchronized. New emit procs must follow this pattern. Tests live in `src/tests/modules/punk/console/testsuites/console/emitconsole.test`. - punk::console terminal-property facts (is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, check::has_bug_*) are per-console: read/write them via `punk::console::console_fact_get`/`console_fact_set`, keyed by canonical {in out} channel pair. The process-default console `{stdin stdout}` stores its facts in the legacy namespace variables (`::punk::console::is_vt52`, `tabwidth`, ...) so existing external readers keep working - do not bypass the helpers for non-default consoles. `ansi_wanted`/`colour_disabled` (string-generation gates), `ansi_available` and raw-mode state are deliberately process-global (rationale documented at the `console_facts` store in the module). Tests live in `src/tests/modules/punk/console/testsuites/console/consolefacts.test`. - Use `punk::args::parse` with `@id` references in `argdoc` namespaces for public API procs. diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index d1e5eeb8..8a6f2c5a 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/src/modules/punk/console-999999.0a1.0.tm @@ -162,6 +162,18 @@ namespace eval punk::console { opunk::console instance name, or an ::opunk::Console object value. Emitted output is written to the resolved console's output channel." } + #As above, with neutral wording for the query/set functions (mode queries, DECRQSS, + #cursor style etc) whose -console governs both the query emission and the response read. + punk::args::define { + @id -id ::punk::console::argdoc::console_opts + @cmd -name punk::console::argdoc::console_opts -help\ + "Definition fragment (not a command). + Provides the -console option documentation for the console query/set functions." + @opts + -console -type list -default {stdin stdout} -help\ + "Console specification: a 2-element {in out} channel list, an anchored + opunk::console instance name, or an ::opunk::Console object value." + } @@ -1182,8 +1194,7 @@ namespace eval punk::console { @cmd -name punk::console::show_input_response -help\ "Debug command for console queries using ANSI" @opts - -console -default {stdin stdout} -type list -help\ - "console/terminal (currently list of in/out channels) (todo - object?)" + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} ${[punk::args::resolved_def -types opts ::punk::console::internal::get_ansi_response_payload -passthrough]} -expected_ms -type integer -default 500 -help\ "Number of ms to wait for response" @@ -1339,7 +1350,7 @@ namespace eval punk::console { - punk::console::dec_modes } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 0 -max 0 }] } @@ -1561,7 +1572,7 @@ namespace eval punk::console { - punk::console::classify_device_attributes_secondary } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 0 -max 0 }] } @@ -1703,11 +1714,10 @@ namespace eval punk::console { set cell_size "" set cell_size_fallback 10x20 - #todo - change -console to -terminalobject with prebuilt default - punk::args::define { @id -id ::punk::console::cell_size - -console -default {stdin stdout} -type list + @opts + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 0 -max 1 newsize -default "" -help\ "character cell pixel dimensions WxH @@ -1757,7 +1767,8 @@ namespace eval punk::console { @id -id ::punk::console::test_is_vt52 @cmd -name punk::console::test_is_vt52 -help\ "in development.. broken" - -inoutchannels -default {stdin stdout} -type list + @opts + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 0 -max 0 } @@ -1765,7 +1776,7 @@ namespace eval punk::console { # works in line mode for alacrity and wezterm proc test_is_vt52 {args} { set argd [punk::args::parse $args -cache 1 withid ::punk::console::test_is_vt52] - set inoutchannels [dict get $argd opts -inoutchannels] + set inoutchannels [dict get $argd opts -console] #ESC / K VT52 without printer #ESC / M VT52 with printer #ESC / Z VT52 emulator?? review @@ -2467,7 +2478,7 @@ namespace eval punk::console { (which is cached) to reduce the number of queries to the terminal. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} ${[punk::args::resolved_def -types opts ::punk::console::internal::get_ansi_response_payload -passthrough]} @values -min 1 -max 1 mode -type {int|string} -multiple 0 -help\ @@ -2513,11 +2524,11 @@ namespace eval punk::console { {Set DEC mode(s) by sending to the console the DECSET sequence: ESC [ ? h - Where is a colon delimited set of integers + Where is a semicolon delimited set of integers formed from the supplied ${$I}mode${$NI} values. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 1 -max -1 mode -type {int|string} -multiple 1 -help\ "integer for DEC mode, or name as in the dict: @@ -2547,7 +2558,7 @@ namespace eval punk::console { lappend modelist $m } set modes_string [join $modelist {;}] - set term_out [lindex $terminal 1] + set term_out [dict get [console_spec_resolve $terminal] out] puts -nonewline $term_out "\x1b\[?${modes_string}h" } namespace eval argdoc { @@ -2560,11 +2571,11 @@ namespace eval punk::console { {Unset DEC mode(s) by sending to the console the DECRST sequence: ESC [ ? l - Where is a colon delimited set of integers + Where is a semicolon delimited set of integers formed from the supplied ${$I}mode${$NI} values. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 1 -max -1 mode -type {int|string} -multiple 1 -help\ "integer for DEC mode, or name as in the dict: @@ -2593,7 +2604,7 @@ namespace eval punk::console { lappend modelist $m } set modes_string [join $modelist {;}] - set term_out [lindex $terminal 1] + set term_out [dict get [console_spec_resolve $terminal] out] puts -nonewline $term_out "\x1b\[?${modes_string}l" } @@ -2619,7 +2630,7 @@ namespace eval punk::console { used if up-to-date current state of a supported mode is required. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} ${[punk::args::resolved_def -types opts ::punk::console::internal::get_ansi_response_payload -passthrough]} -refresh -type none -help\ "Force a re-test of the mode." @@ -2655,7 +2666,10 @@ namespace eval punk::console { } } variable dec_has_mode_cache - #make sure we cache on both console and passthrough + set cinfo [console_spec_resolve $console] + set console [list [dict get $cinfo in] [dict get $cinfo out]] + #cache on the canonical channel pair and passthrough - all -console spec forms + #addressing the same console share one cache entry set cachekey "$console $passthrough" if {$do_refresh} { if {[dict exists $dec_has_mode_cache $cachekey $m]} { @@ -2696,7 +2710,7 @@ namespace eval punk::console { -help\ {Show table of DEC modes with basic information.} @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} ${[punk::args::resolved_def -types opts ::punk::console::internal::get_ansi_response_payload -passthrough]} -test -type none -help\ "Test current value/support for each mode" @@ -2871,7 +2885,7 @@ namespace eval punk::console { @opts #review - problem with 's ansi_has_mode' #-console -type list -typesynopsis {{${$I}inputchan${$NI} ${$I}outputchan${$NI}}} -minsize 2 -default {stdin stdout} - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} -refresh -type none -return -type string -choices {dict result} -default result -choicelabels { dict\ @@ -2906,7 +2920,10 @@ namespace eval punk::console { } } variable ansi_has_mode_cache - #make sure we cache on both console and passthrough + set cinfo [console_spec_resolve $console] + set console [list [dict get $cinfo in] [dict get $cinfo out]] + #cache on the canonical channel pair and passthrough - all -console spec forms + #addressing the same console share one cache entry set cachekey "$console $passthrough" if {$do_refresh} { if {[dict exists $ansi_has_mode_cache $cachekey $m]} { @@ -2947,11 +2964,11 @@ namespace eval punk::console { {Set ANSI mode(s) by sending to the console the SM sequence: ESC [ h - Where is a colon delimited set of integers + Where is a semicolon delimited set of integers formed from the supplied ${$I}mode${$NI} values. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 1 -max -1 mode -type {int|string} -multiple 1 -help\ "integer for ANSI mode, or name as in the dict: @@ -2980,7 +2997,7 @@ namespace eval punk::console { lappend modelist $m } set modes_string [join $modelist {;}] - set term_out [lindex $terminal 1] + set term_out [dict get [console_spec_resolve $terminal] out] puts -nonewline $term_out "\x1b\[${modes_string}h" } @@ -2994,11 +3011,11 @@ namespace eval punk::console { {Unset ANSI mode(s) by sending to the console the RM sequence: ESC [ l - Where is a colon delimited set of integers + Where is a semicolon delimited set of integers formed from the supplied ${$I}mode${$NI} values. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 1 -max -1 mode -type {int|string} -multiple 1 -help\ "integer for ANSI mode, or name as in the dict: @@ -3027,7 +3044,7 @@ namespace eval punk::console { lappend modelist $m } set modes_string [join $modelist {;}] - set term_out [lindex $terminal 1] + set term_out [dict get [console_spec_resolve $terminal] out] puts -nonewline $term_out "\x1b\[${modes_string}l" } namespace eval argdoc { @@ -3059,7 +3076,7 @@ namespace eval punk::console { The value of is returned by the ansi_get_mode function. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} ${[punk::args::resolved_def -types opts ::punk::console::internal::get_ansi_response_payload -passthrough]} @values -min 1 -max 1 mode -type {int|string} -multiple 0 -help\ @@ -3104,7 +3121,7 @@ namespace eval punk::console { -help\ {Show table of ANSI modes with basic information.} @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} ${[punk::args::resolved_def -types opts ::punk::console::internal::get_ansi_response_payload -passthrough]} -test -type none -help\ "Test current value/support for each mode" @@ -3370,7 +3387,7 @@ namespace eval punk::console { Return payload from console's DECRPSS response. } @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 1 -max 1 ${$DECRQSS_CHOICES} } @@ -3430,7 +3447,7 @@ namespace eval punk::console { Note that support for DECSCUSR may be inconsistent across terminals. Some terminals may only support a subset of the possible styles.} @opts - -console -type list -minsize 2 -default {stdin stdout} + ${[punk::args::resolved_def -types opts ::punk::console::argdoc::console_opts -console]} @values -min 0 -max 1 style -type dict -optional 1 -choicerestricted 0 -choices {0 default 1 block-blink 2 block-steady 3 underline-blink 4 underline-steady 5 beam-blink 6 beam-steady} -choicecolumns 2 -help\ "Cursor style to set @@ -3515,7 +3532,7 @@ namespace eval punk::console { } #set cursor style using DECSCUSR set request "\x1b\[${style_num} q" - set term_out [lindex $console 1] + set term_out [dict get [console_spec_resolve $console] out] puts -nonewline $term_out $request flush $term_out } diff --git a/src/modules/punk/console-buildversion.txt b/src/modules/punk/console-buildversion.txt index cfda16fd..d32969ed 100644 --- a/src/modules/punk/console-buildversion.txt +++ b/src/modules/punk/console-buildversion.txt @@ -1,6 +1,11 @@ -0.3.0 +0.4.0 #First line must be a semantic version number #all other lines are ignored. +#0.4.0 - mode setters (dec_set_mode/dec_unset_mode/ansi_set_mode/ansi_unset_mode) and cursor_style resolve their -console spec via console_spec_resolve instead of '[lindex $terminal 1]' - anchored instance names and ::opunk::Console object values now work (previously silently wrote to the wrong target) +#0.4.0 - the fourteen '-console -type list -minsize 2' declarations replaced with shared fragment ::punk::console::argdoc::console_opts (the -minsize 2 constraint rejected 1-element instance-name specs at parse time); show_input_response and cell_size -console docs unified on the fragment +#0.4.0 - dec_has_mode/ansi_has_mode cache on the canonical {in out} pair - all spec forms addressing the same console share one cache entry (previously keyed on the raw spec string) +#0.4.0 - test_is_vt52 option -inoutchannels renamed to -console per the documented convention (breaking for that experimental/broken proc only) +#0.4.0 - set/unset mode argdocs corrected: are semicolon delimited (docs said colon) #0.3.0 - per-console facts (interim store for multi-console goals G-001/G-002): is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support and check::has_bug_* are keyed by canonical {in out} pair via new console_fact_get/console_fact_set; the default console {stdin stdout} stores facts in the legacy namespace variables so external readers (punk::repl, textblock, overtype, punk::ansi, punk::ns) are unaffected #0.3.0 - vt52 state, size_via_query_mechanisms, get_cursor_pos and all vt52-aware emit wrappers (incl. string-returning ansi::move) consult the spec'd console's is_vt52 fact instead of the process-global flag #0.3.0 - grapheme_cluster_support and check::has_bug_legacysymbolwidth/has_bug_zwsp accept a trailing '-console '; the TERM_PROGRAM shortcut in grapheme_cluster_support only applies to the default console (env hints describe the process's own terminal) diff --git a/src/tests/modules/punk/console/testsuites/console/consolespec.test b/src/tests/modules/punk/console/testsuites/console/consolespec.test index 2b092ff0..dc06aad8 100644 --- a/src/tests/modules/punk/console/testsuites/console/consolespec.test +++ b/src/tests/modules/punk/console/testsuites/console/consolespec.test @@ -424,5 +424,126 @@ namespace eval ::testspace { {columns 640 rows 480}\ ] + #see emitconsole.test - windows [chan pipe] reads race the background reader thread + proc read_emitted {chan {timeout_ms 2000}} { + set data "" + set deadline [expr {[clock milliseconds] + $timeout_ms}] + while {[clock milliseconds] < $deadline} { + append data [read $chan] + if {$data ne ""} { + after 20 + append data [read $chan] + return $data + } + after 5 + } + return $data + } + + test setters_spec_forms {mode setters and cursor_style resolve all -console spec forms to the console's output channel}\ + -setup $common -body { + lassign [chan pipe] rd wr + chan configure $wr -buffering none -translation lf + chan configure $rd -blocking 0 -translation lf + opunk::console::create settertest1 $rd $wr + #anchored instance name + punk::console::dec_set_mode -console settertest1 7 + lappend result [expr {[read_emitted $rd] eq "\x1b\[?7h"}] + #object value + set obj [opunk::console::instance settertest1] + punk::console::ansi_unset_mode -console $obj 20 + lappend result [expr {[read_emitted $rd] eq "\x1b\[20l"}] + #legacy channel-pair form still works + punk::console::dec_unset_mode -console [list $rd $wr] 7 + lappend result [expr {[read_emitted $rd] eq "\x1b\[?7l"}] + #cursor_style set path via instance name (flushes itself) + punk::console::cursor_style -console settertest1 2 + lappend result [expr {[read_emitted $rd] eq "\x1b\[2 q"}] + }\ + -cleanup { + catch {opunk::console::forget settertest1} + catch {chan close $wr} + catch {chan close $rd} + array unset ::punk::console::input_chunks_waiting $rd + }\ + -result [list\ + 1\ + 1\ + 1\ + 1\ + ] + + test dec_has_mode_cache_canonical {dec_has_mode caches on the canonical channel pair - instance-name spec hits the pair-primed cache}\ + -setup { + set result "" + lassign [chan pipe] rdi wri + lassign [chan pipe] rdo wro + chan configure $wri -buffering none -translation lf + chan configure $wro -buffering none -translation lf + chan configure $rdo -blocking 0 -translation lf + set saved_cache $::punk::console::dec_has_mode_cache + set ::punk::console::dec_has_mode_cache [dict create] + }\ + -body { + opunk::console::create cachetest1 $rdi $wro + after 40 [list apply [list {wchan} { + puts -nonewline $wchan "\x1b\[?7;1\$y" + flush $wchan + }] $wri] + #prime via channel-pair spec (query emitted, response fed) + set d1 [punk::console::dec_has_mode -console [list $rdi $wro] -return dict 7] + lappend result [dict get $d1 result] [dict get $d1 source] + lappend result [expr {[read_emitted $rdo] ne ""}] ;#drain the emitted query + #same console via anchored instance name - must hit the cache, no new emission + set d2 [punk::console::dec_has_mode -console cachetest1 -return dict 7] + lappend result [dict get $d2 result] [dict get $d2 source] + lappend result [read $rdo] + }\ + -cleanup { + set ::punk::console::dec_has_mode_cache $saved_cache + catch {opunk::console::forget cachetest1} + foreach c [list $wri $rdi $wro $rdo] { + catch {chan close $c} + } + array unset ::punk::console::input_chunks_waiting $rdi + }\ + -result [list\ + 1\ + query\ + 1\ + 1\ + cache\ + {}\ + ] + + test test_is_vt52_console_opt {test_is_vt52 takes -console (renamed from -inoutchannels) and reads the vt52 report}\ + -setup { + set result "" + lassign [chan pipe] rdi wri + lassign [chan pipe] rdo wro + chan configure $wri -buffering none -translation lf + chan configure $wro -buffering none -translation lf + chan configure $rdo -blocking 0 -translation lf + }\ + -body { + after 40 [list apply [list {wchan} { + puts -nonewline $wchan "\x1b/Z" + flush $wchan + }] $wri] + lappend result [punk::console::test_is_vt52 -console [list $rdi $wro]] + #old option name is gone + lappend result [catch {punk::console::test_is_vt52 -inoutchannels [list $rdi $wro]}] + }\ + -cleanup { + foreach c [list $wri $rdi $wro $rdo] { + catch {chan close $c} + } + array unset ::punk::console::input_chunks_waiting $rdi + }\ + -result [list\ + 1\ + 1\ + ] + } tcltest::cleanupTests ;#needed to produce test summary.