Browse Source

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: <codes> 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
master
Julian Noble 2 weeks ago
parent
commit
3a85fa85f5
  1. 2
      src/modules/punk/AGENTS.md
  2. 81
      src/modules/punk/console-999999.0a1.0.tm
  3. 7
      src/modules/punk/console-buildversion.txt
  4. 121
      src/tests/modules/punk/console/testsuites/console/consolespec.test

2
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 `<subpath>/<modulename>-999999.0a1.0.tm` following the namespace-to-path convention.
- punk::console is incrementally migrating positional `inoutchannels` arguments to the documented `-console` convention. A `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Converted procs accept the legacy leading positional channel-pair during the transition; convert remaining positional procs opportunistically when touched, with tests.
- 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 <consolespec>` 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.

81
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 [ ? <codes> h
Where <codes> is a colon delimited set of integers
Where <codes> 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 [ ? <codes> l
Where <codes> is a colon delimited set of integers
Where <codes> 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 [ <codes> h
Where <codes> is a colon delimited set of integers
Where <codes> 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 [ <codes> l
Where <codes> is a colon delimited set of integers
Where <codes> 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 <s> 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
}

7
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: <codes> 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 <consolespec>'; the TERM_PROGRAM shortcut in grapheme_cluster_support only applies to the default console (env hints describe the process's own terminal)

121
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.

Loading…
Cancel
Save