@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::console 0.8.0
# Application punk::console 0.8.1
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -17,7 +17,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::console 0 0.8.0 ]
#[manpage_begin punkshell_module_punk::console 0 0.8.1 ]
#[copyright "2024"]
#[titledesc {punk console}] [comment {-- Name section and table of contents description --}]
#[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]
}
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
#{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
@ -2828,7 +2852,10 @@ namespace eval punk::console {
#see PUNKARGS id ::punk::console::size_via_query_mechanisms
if {[console_fact_get $inoutchannels is_vt52]} {
#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 [dict create]
@ -2841,19 +2868,25 @@ namespace eval punk::console {
#we will also use the results for our initial return of the size.
set successful_mechs [list]
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"
if {![dict size $sizedict]} {
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"
if {![dict size $sizedict]} {
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"
if {![dict size $sizedict]} {
set sizedict $result
@ -2879,7 +2912,7 @@ namespace eval punk::console {
}
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
}
}
@ -2971,6 +3004,11 @@ namespace eval punk::console {
#this mechanism is much faster than ansi cursor movements
#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
#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
if {[string is integer -strict $cols] && [string is integer -strict $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"
}
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 {
@id -id ::punk::console::get_size_using_tput
@cmd -name punk::console::get_size_using_tput -summary\
@ -2993,7 +3059,44 @@ namespace eval punk::console {
if {$tputcmd eq ""} {
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]
}
@ -5787,7 +5890,7 @@ namespace eval punk::console::system {
# Standalone debug run example (from this directory):
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1
# 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
# 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
package provide punk::console [namespace eval punk::console {
variable version
set version 0.8.0
set version 0.8.1
}]
return