@ -7,7 +7,7 @@
# (C) 2023
# (C) 2023
#
#
# @@ Meta Begin
# @@ Meta Begin
# Application punk::console 0.8.0
# Application punk::console 0.8.1
# Meta platform tcl
# Meta platform tcl
# Meta license <unspecified>
# Meta license <unspecified>
# @@ Meta End
# @@ Meta End
@ -17,7 +17,7 @@
# doctools header
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#*** !doctools
#[manpage_begin punkshell_module_punk::console 0 0.8.0 ]
#[manpage_begin punkshell_module_punk::console 0 0.8.1 ]
#[copyright "2024"]
#[copyright "2024"]
#[titledesc {punk console}] [comment {-- Name section and table of contents description --}]
#[titledesc {punk console}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk console}] [comment {-- Description at end of page heading --}]
#[moddesc {punk console}] [comment {-- Description at end of page heading --}]
@ -2809,6 +2809,30 @@ namespace eval punk::console {
return [dict create columns 80 rows 24]
return [dict create columns 80 rows 24]
}
}
lappend PUNKARGS [list {
@id -id ::punk::console::size_result_valid
@cmd -name punk::console::size_result_valid -summary\
"True if a size-mechanism result is a well-formed {columns <int> rows <int>} dict."
@values -min 1 -max 1
sizedict -type any -help\
"Candidate result from a get_size_using_* mechanism."
}]
proc size_result_valid {sizedict} {
#see PUNKARGS id ::punk::console::size_result_valid
#Guard for size_via_query_mechanisms: a mechanism 'success' must be well-formed.
#Mechanisms can otherwise return malformed data without erroring and win the
#cached-mechanism loop silently (the 2026-08-03 tput single-capname regression
#class - and the ANSI mechanisms return {columns {} rows {}} on a query timeout).
if {[catch {dict size $sizedict} pairs] || $pairs != 2} {
return 0
}
expr {
[dict exists $sizedict columns] && [dict exists $sizedict rows]
&& [string is integer -strict [dict get $sizedict columns]]
&& [string is integer -strict [dict get $sizedict rows]]
}
}
#ANSI/tput size mechanisms with per-console-pair timing cache. Returns a dict
#ANSI/tput size mechanisms with per-console-pair timing cache. Returns a dict
#{columns <int> rows <int>} or an empty dict when size cannot be determined.
#{columns <int> rows <int>} or an empty dict when size cannot be determined.
#Used by the legacy get_size channel path and (as console_size_provider) by the base
#Used by the legacy get_size channel path and (as console_size_provider) by the base
@ -2828,7 +2852,10 @@ namespace eval punk::console {
#see PUNKARGS id ::punk::console::size_via_query_mechanisms
#see PUNKARGS id ::punk::console::size_via_query_mechanisms
if {[console_fact_get $inoutchannels is_vt52]} {
if {[console_fact_get $inoutchannels is_vt52]} {
#vt52 doesn't support cursor save/restore or cursor position reports.
#vt52 doesn't support cursor save/restore or cursor position reports.
if {![catch {get_size_using_tput $inoutchannels} sizedict]} {
if {![catch {get_size_using_sttysize $inoutchannels} sizedict] && [size_result_valid $sizedict]} {
return $sizedict
}
if {![catch {get_size_using_tput $inoutchannels} sizedict] && [size_result_valid $sizedict]} {
return $sizedict
return $sizedict
}
}
return [dict create]
return [dict create]
@ -2841,19 +2868,25 @@ namespace eval punk::console {
#we will also use the results for our initial return of the size.
#we will also use the results for our initial return of the size.
set successful_mechs [list]
set successful_mechs [list]
set sizedict [dict create]
set sizedict [dict create]
if {![catch {get_size_using_cursorrestore $inoutchannels} result]} {
if {![catch {get_size_using_cursorrestore $inoutchannels} result] && [size_result_valid $result] } {
lappend successful_mechs "cursorrestore"
lappend successful_mechs "cursorrestore"
if {![dict size $sizedict]} {
if {![dict size $sizedict]} {
set sizedict $result
set sizedict $result
}
}
}
}
if {![catch {get_size_using_cursormove $inoutchannels} result]} {
if {![catch {get_size_using_cursormove $inoutchannels} result] && [size_result_valid $result] } {
lappend successful_mechs "cursormove"
lappend successful_mechs "cursormove"
if {![dict size $sizedict]} {
if {![dict size $sizedict]} {
set sizedict $result
set sizedict $result
}
}
}
}
if {![catch {get_size_using_tput $inoutchannels} result] } {
if {![catch {get_size_using_sttysize $inoutchannels} result] && [size_result_valid $result]} {
lappend successful_mechs "sttysize"
if {![dict size $sizedict]} {
set sizedict $result
}
}
if {![catch {get_size_using_tput $inoutchannels} result] && [size_result_valid $result]} {
lappend successful_mechs "tput"
lappend successful_mechs "tput"
if {![dict size $sizedict]} {
if {![dict size $sizedict]} {
set sizedict $result
set sizedict $result
@ -2879,7 +2912,7 @@ namespace eval punk::console {
}
}
foreach mech [dict get $get_size_mechanism $inoutchannels] {
foreach mech [dict get $get_size_mechanism $inoutchannels] {
if {![catch {get_size_using_$mech $inoutchannels} sizedict]} {
if {![catch {get_size_using_$mech $inoutchannels} sizedict] && [size_result_valid $sizedict] } {
return $sizedict
return $sizedict
}
}
}
}
@ -2971,6 +3004,11 @@ namespace eval punk::console {
#this mechanism is much faster than ansi cursor movements
#this mechanism is much faster than ansi cursor movements
#REVIEW check if any x-platform anomalies with this method?
#REVIEW check if any x-platform anomalies with this method?
#can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least
#can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least
#NOTE (verified 2026-08-03, tcl 9.0.2): unix tty/serial channels do NOT expose -winsize
#(windows console channels do) - so on unix this mechanism currently always falls through.
#If a future tcl adds unix -winsize, be aware a stacked/reflected stdout (e.g the repl's
#shellfilter stack) masks the underlying tty's options - probing the base tty channel
#(or /dev/tty) would be needed for it to be seen under stacked channels.
lassign [dict get $outconf -winsize] cols lines
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
return [dict create columns $cols rows $lines]
@ -2979,6 +3017,34 @@ namespace eval punk::console {
error "chan configure method of getting console size not supported or failed to get valid size info"
error "chan configure method of getting console size not supported or failed to get valid size info"
}
}
lappend PUNKARGS [list {
@id -id ::punk::console::get_size_using_sttysize
@cmd -name punk::console::get_size_using_sttysize -summary\
"Console size via 'stty size' on the input channel's tty (single fork) - errors when unavailable or input is not a tty."
@values -min 0 -max 1
inoutchannels -type list -default {stdin stdout} -optional 1 -help\
"Canonical {in out} channel pair (internal size mechanism - not spec-form aware)."
}]
proc get_size_using_sttysize {{inoutchannels {stdin stdout}}} {
#see PUNKARGS id ::punk::console::get_size_using_sttysize
#'stty size' emits "<rows> <cols>" from the tty on its stdin - a single fork with a
#fixed output shape (GNU, busybox and BSD stty all support it - not strictly POSIX).
#Preferred over tput for the exec-based fallback tier: tput multi-capname handling
#varies by implementation (see get_size_using_tput autodetect). Non-tty input makes
#stty error, which correctly drops this mechanism from size_via_query_mechanisms.
set in [lindex $inoutchannels 0]
set sttycmd [auto_execok stty]
if {$sttycmd eq ""} {
error "stty command not found - cannot use stty size method to get console size"
}
set sizeinfo [string trim [exec {*}$sttycmd size <@$in]]
lassign $sizeinfo lines cols
if {![string is integer -strict $lines] || ![string is integer -strict $cols] || $lines < 1 || $cols < 1} {
error "stty size returned '$sizeinfo' - not usable as '<rows> <cols>'"
}
return [dict create columns $cols rows $lines]
}
lappend PUNKARGS [list {
lappend PUNKARGS [list {
@id -id ::punk::console::get_size_using_tput
@id -id ::punk::console::get_size_using_tput
@cmd -name punk::console::get_size_using_tput -summary\
@cmd -name punk::console::get_size_using_tput -summary\
@ -2993,7 +3059,44 @@ namespace eval punk::console {
if {$tputcmd eq ""} {
if {$tputcmd eq ""} {
error "tput command not found - cannot use tput method to get console size"
error "tput command not found - cannot use tput method to get console size"
}
}
lassign [exec {*}$tputcmd lines cols] lines cols
#tput multi-capname behaviour varies by implementation: ncurses >= 6.1 and the BSD
#tputs emit one value per capname ("66\n266"), but older ncurses treats the second
#capname as an (ignored) parameter of the first and emits only "66" - which used to
#lassign cols to "" here and return 'columns {} rows 66' (2026-08-03 WSL get_size
#regression once the mechanism-timing cache ranked tput first). Autodetect by result
#shape on first use and remember per tput path - capable tputs stay single-fork,
#single-cap-only tputs settle into two forks per call.
variable tput_multicap
if {![info exists tput_multicap]} {set tput_multicap [dict create]}
if {[dict exists $tput_multicap $tputcmd] && ![dict get $tput_multicap $tputcmd]} {
set lines [string trim [exec {*}$tputcmd lines]]
set cols [string trim [exec {*}$tputcmd cols]]
} else {
set outdata [exec {*}$tputcmd lines cols]
set values [list]
foreach v [split $outdata \n] {
set v [string trim $v]
if {$v ne ""} {lappend values $v}
}
switch -- [llength $values] {
2 {
lassign $values lines cols
dict set tput_multicap $tputcmd 1
}
1 {
#single-cap-only tput evaluated 'lines' and ignored 'cols'
dict set tput_multicap $tputcmd 0
set lines [lindex $values 0]
set cols [string trim [exec {*}$tputcmd cols]]
}
default {
error "tput lines cols returned '$outdata' - unrecognised output shape"
}
}
}
if {![string is integer -strict $lines] || ![string is integer -strict $cols] || $lines < 1 || $cols < 1} {
error "tput returned lines='$lines' cols='$cols' - not usable as positive integers"
}
return [dict create columns $cols rows $lines]
return [dict create columns $cols rows $lines]
}
}
@ -5787,7 +5890,7 @@ namespace eval punk::console::system {
# Standalone debug run example (from this directory):
# Standalone debug run example (from this directory):
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1
# Protocol (one line per named-pipe connection): enableraw | disableraw | ping | exit
# Protocol (one line per named-pipe connection): enableraw | disableraw | ping | exit
# MAINTENANCE: src/modules/punk/console-0.8.0 .tm carries this file's text as
# MAINTENANCE: src/modules/punk/console-0.8.1 .tm carries this file's text as
# punk::console::system::ps_consolemode_script_embedded (the last-resort resolution for kits and
# punk::console::system::ps_consolemode_script_embedded (the last-resort resolution for kits and
# unusual cwds). Keep the two in sync - the console testsuite (psfallback.test) fails when they diverge.
# unusual cwds). Keep the two in sync - the console testsuite (psfallback.test) fails when they diverge.
;
;
@ -6906,7 +7009,7 @@ namespace eval ::punk::args::register {
## Ready
## Ready
package provide punk::console [namespace eval punk::console {
package provide punk::console [namespace eval punk::console {
variable version
variable version
set version 0.8.0
set version 0.8.1
}]
}]
return
return