You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2250 lines
98 KiB
2250 lines
98 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt |
|
# |
|
# 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) 2023 |
|
# |
|
# @@ Meta Begin |
|
# Application punk::console 0.1.1 |
|
# Meta platform tcl |
|
# Meta license <unspecified> |
|
# @@ Meta End |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# doctools header |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[manpage_begin punkshell_module_punk::console 0 0.1.1] |
|
#[copyright "2024"] |
|
#[titledesc {punk console}] [comment {-- Name section and table of contents description --}] |
|
#[moddesc {punk console}] [comment {-- Description at end of page heading --}] |
|
#[require punk::console] |
|
#[keywords module console terminal] |
|
#[description] |
|
#[para] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section Overview] |
|
#[para] overview of punk::console |
|
#[subsection Concepts] |
|
#[para] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Requirements |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[subsection dependencies] |
|
#[para] packages used by punk::console |
|
#[list_begin itemized] |
|
|
|
package require Tcl 8.6- |
|
package require Thread ;#tsv required to sync is_raw |
|
package require punk::ansi |
|
package require punk::args |
|
#*** !doctools |
|
#[item] [package {Tcl 8.6-}] |
|
#[item] [package {Thread}] |
|
#[item] [package {punk::ansi}] |
|
#[item] [package {punk::args}] |
|
|
|
|
|
#*** !doctools |
|
#[list_end] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section API] |
|
|
|
|
|
|
|
#if {"windows" eq $::tcl_platform(platform)} { |
|
# #package require zzzload |
|
# #zzzload::pkg_require twapi |
|
#} |
|
|
|
#see https://learn.microsoft.com/en-us/windows/console/classic-vs-vt |
|
#https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
namespace eval punk::console { |
|
#*** !doctools |
|
#[subsection {Namespace punk::console}] |
|
#[para] |
|
|
|
#*** !doctools |
|
#[list_begin definitions] |
|
|
|
variable PUNKARGS |
|
|
|
variable tabwidth 8 ;#default only - will attempt to detect and set to that configured in terminal |
|
#Note that windows terminal cooked mode seems to use 8 for interactive use even if set differently |
|
#e.g typing tab characters may still be echoed 8-spaced while writing to stdout my obey the terminal's tab stops. |
|
variable has_twapi 0 |
|
variable previous_stty_state_stdin "" |
|
variable previous_stty_state_stdout "" |
|
variable previous_stty_state_stderr "" |
|
|
|
#variable is_raw 0 |
|
if {![tsv::exists console is_raw]} { |
|
tsv::set console is_raw 0 |
|
} |
|
|
|
variable input_chunks_waiting |
|
if {![info exists input_chunks_waiting(stdin)]} { |
|
set input_chunks_waiting(stdin) [list] |
|
} |
|
variable ansi_response_chunk ;#array keyed on callid |
|
variable ansi_response_wait ;#array keyed on callid |
|
variable ansi_response_queue ;#list of callids |
|
variable ansi_response_queuedata ;#dict keyed on callid - with function params |
|
|
|
# -- |
|
variable ansi_available -1 ;#default -1 for unknown. Leave it this way so test for ansi support is run. |
|
#-1 still evaluates to true - as the modern assumption for ansi availability is true |
|
#only false if ansi_available has been set 0 by test_can_ansi |
|
#support ansistrip for legacy windows terminals |
|
# -- |
|
|
|
variable ansi_wanted 2 ;#2 for default assumed yes, will be set to -1 for automatically unwanted when ansi unavailable values of 0 or 1 won't be autoset |
|
|
|
|
|
|
|
#punk::console namespace - contains *directly* acting functions - some based on ansi escapes from the 'ansi' sub namespace, some on local system calls or executable calls wrapped in the 'local' sub namespace |
|
#directly acting means they write to stdout to cause the console to peform the action, or they perform the action immediately via other means. |
|
#punk::console::ansi contains a subset of punk::ansi, but with emission to stdout as opposed to simply returning the ansi sequence. |
|
#punk::console::local functions are used by punk::console commands when there is no ansi equivalent |
|
#ansi escape sequences are possibly preferable esp if terminal is remote to process running punk::console |
|
# punk::local commands may be more performant in some circumstances where console is directly attached, but it shouldn't be assumed. e.g ansi::titleset outperforms local::titleset on windows with twapi. |
|
|
|
namespace eval ansi { |
|
#ansi escape sequence based terminal/console control functions |
|
namespace export * |
|
} |
|
namespace eval local { |
|
#non-ansi terminal/console control functions |
|
#e.g external utils system API's. |
|
namespace export * |
|
} |
|
|
|
if {"windows" eq $::tcl_platform(platform)} { |
|
#accept args for all dummy/load functions so we don't have to match/update argument signatures here |
|
|
|
proc enableAnsi {args} { |
|
#loopavoidancetoken (don't remove) |
|
internal::define_windows_procs |
|
internal::abort_if_loop |
|
tailcall enableAnsi {*}$args |
|
} |
|
#review what raw mode means with regard to a specific channel vs terminal as a whole |
|
proc enableRaw {args} { |
|
#loopavoidancetoken (don't remove) |
|
internal::define_windows_procs |
|
internal::abort_if_loop |
|
tailcall enableRaw {*}$args |
|
} |
|
proc disableRaw {args} { |
|
#loopavoidancetoken (don't remove) |
|
internal::define_windows_procs |
|
internal::abort_if_loop |
|
tailcall disableRaw {*}$args |
|
} |
|
proc enableVirtualTerminal {args} { |
|
#loopavoidancetoken (don't remove) |
|
internal::define_windows_procs |
|
internal::abort_if_loop |
|
tailcall enableVirtualTerminal {*}$args |
|
} |
|
proc disableVirtualTerminal {args} { |
|
#loopavoidancetoken (don't remove) |
|
internal::define_windows_procs |
|
internal::abort_if_loop |
|
tailcall disableVirtualTerminal {*}$args |
|
} |
|
set funcs [list disableAnsi enableProcessedInput disableProcessedInput] |
|
foreach f $funcs { |
|
proc $f {args} [string map [list %f% $f] { |
|
set mybody [info body %f%] |
|
internal::define_windows_procs |
|
set newbody [info body %f%] |
|
if {$newbody ne $mybody} { |
|
tailcall %f% {*}$args |
|
} else { |
|
#error vs noop? |
|
puts stderr "Unable to set implementation for %f% - check twapi?" |
|
} |
|
}] |
|
} |
|
|
|
} else { |
|
proc enableAnsi {} { |
|
#todo? |
|
} |
|
proc disableAnsi {} { |
|
|
|
} |
|
|
|
#todo - something better - the 'channel' concept may not really apply on unix, as raw mode is set for input and output modes currently - only valid to set on a readable channel? |
|
#on windows they can be set independently (but not with stty) - REVIEW |
|
|
|
#NOTE - the is_raw is only being set in current interp - but the channel is shared. |
|
#this is problematic with the repl thread being separate. - must be a tsv? REVIEW |
|
proc enableRaw {{channel stdin}} { |
|
#variable is_raw |
|
|
|
variable previous_stty_state_$channel |
|
set sttycmd [auto_execok stty] |
|
if {[set previous_stty_state_$channel] eq ""} { |
|
if {[catch {exec {*}$sttycmd -g <@$channel} previous_stty_state_$channel]} { |
|
set previous_stty_state_$channel "" |
|
} |
|
} |
|
|
|
exec {*}$sttycmd raw -echo <@$channel |
|
tsv::set console is_raw 1 |
|
return [dict create previous [set previous_stty_state_$channel]] |
|
} |
|
proc disableRaw {{channel stdin}} { |
|
#variable is_raw |
|
variable previous_stty_state_$channel |
|
set sttycmd [auto_execok stty] |
|
if {[set previous_stty_state_$channel] ne ""} { |
|
exec {*}$sttycmd [set previous_stty_state_$channel] |
|
set previous_stty_state_$channel "" |
|
tsv::set console is_raw 0 |
|
return restored |
|
} |
|
exec {*}$sttycmd -raw echo <@$channel |
|
tsv::set console is_raw 0 |
|
return done |
|
} |
|
proc enableVirtualTerminal {{channels {input output}}} { |
|
|
|
} |
|
proc disableVirtualTerminal {args} { |
|
|
|
} |
|
} |
|
|
|
#review - document and decide granularity required. should we enable/disable more than one at once? |
|
proc enable_mouse {} { |
|
puts -nonewline stdout \x1b\[?1000h |
|
puts -nonewline stdout \x1b\[?1003h |
|
puts -nonewline stdout \x1b\[?1015h |
|
puts -nonewline stdout \x1b\[?1006h |
|
flush stdout |
|
} |
|
proc disable_mouse {} { |
|
puts -nonewline stdout \x1b\[?1000l |
|
puts -nonewline stdout \x1b\[?1003l |
|
puts -nonewline stdout \x1b\[?1015l |
|
puts -nonewline stdout \x1b\[?1006l |
|
flush stdout |
|
} |
|
proc enable_bracketed_paste {} { |
|
puts -nonewline stdout \x1b\[?2004h |
|
} |
|
proc disable_bracketed_paste {} { |
|
puts -nonewline stdout \x1b\[?2004l |
|
} |
|
proc start_application_mode {} { |
|
#need loop to read events? |
|
puts -nonewline stdout \x1b\[?1049h ;#alt screen |
|
enable_mouse |
|
#puts -nonewline stdout \x1b\[?25l ;#hide cursor |
|
puts -nonewline stdout \x1b\[?1003h\n |
|
enable_bracketed_paste |
|
|
|
} |
|
#todo stop_application_mode {} {} |
|
|
|
proc mode {{raw_or_line query}} { |
|
#variable is_raw |
|
variable ansi_available |
|
set raw_or_line [string tolower $raw_or_line] |
|
if {$raw_or_line eq "query"} { |
|
if {[tsv::get console is_raw]} { |
|
return "raw" |
|
} else { |
|
return "line" |
|
} |
|
} elseif {$raw_or_line eq "raw"} { |
|
if {[catch { |
|
punk::console::enableRaw |
|
} errM]} { |
|
puts stderr "Warning punk::console::enableRaw failed - $errM" |
|
} |
|
if {[can_ansi]} { |
|
punk::console::enableVirtualTerminal both |
|
} |
|
} elseif {$raw_or_line eq "line"} { |
|
#review -order. disableRaw has memory from enableRaw.. but but for line mode we want vt disabled - so call it after disableRaw (?) |
|
if {[catch { |
|
punk::console::disableRaw |
|
} errM]} { |
|
puts stderr "Warning punk::console::disableRaw failed - $errM" |
|
} |
|
if {[can_ansi]} { |
|
punk::console::disableVirtualTerminal input ;#default readline arrow behaviour etc |
|
punk::console::enableVirtualTerminal output ;#display/use ansi codes |
|
} |
|
} else { |
|
error "punk::console::mode expected 'raw' or 'line' or default value 'query'" |
|
} |
|
} |
|
|
|
namespace eval internal { |
|
proc abort_if_loop {{failmsg ""}} { |
|
#puts "il1 [info level 1]" |
|
#puts "thisproc: [lindex [info level 0] 0]" |
|
set would_loop [uplevel 1 {expr {[string match *loopavoidancetoken* [info body [namespace tail [lindex [info level 0] 0]]]]}}] |
|
#puts "would_loop: $would_loop" |
|
if {$would_loop} { |
|
set procname [uplevel 1 {namespace tail [lindex [info level 0] 0]}] |
|
if {$failmsg eq ""} { |
|
set errmsg "[namespace current] Failed to redefine procedure $procname" |
|
} else { |
|
set errmsg $failmsg |
|
} |
|
error $errmsg |
|
} |
|
} |
|
proc define_windows_procs {} { |
|
package require zzzload |
|
set loadstate [zzzload::pkg_require twapi] |
|
|
|
#loadstate could also be stuck on loading? - review - zzzload not very ripe |
|
#Twapi can be relatively slow to load (on some systems) - can be 1s plus in some cases - and much longer if there are disk performance issues. |
|
if {$loadstate ni [list failed]} { |
|
#possibly still 'loading' |
|
#review zzzload usage |
|
#puts stdout "=========== console loading twapi =============" |
|
set loadstate [zzzload::pkg_wait twapi] ;#can return 'failed' will return version if already loaded or loaded during wait |
|
} |
|
|
|
if {$loadstate ni [list failed]} { |
|
package require twapi ;#should be fast once twapi dll loaded in zzzload thread |
|
set ::punk::console::has_twapi 1 |
|
|
|
#todo - move some of these to the punk::console::local sub-namespace - as they use APIs rather than in-band ANSI to do their work. |
|
#enableAnsi seems like it should be directly under punk::console .. but then it seems inconsistent if other local console-mode setting functions aren't. |
|
#Find a compromise to organise things somewhat sensibly.. |
|
|
|
#this is really enableAnsi *processing* |
|
proc [namespace parent]::enableAnsi {} { |
|
#output handle modes |
|
#Enable virtual terminal processing (sometimes off in older windows terminals) |
|
#ENABLE_PROCESSED_OUTPUT = 0x0001 |
|
#ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 |
|
#ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 |
|
#DISABLE_NEWLINE_AUTO_RETURN = 0x0008 |
|
set h_out [twapi::get_console_handle stdout] |
|
set oldmode_out [twapi::GetConsoleMode $h_out] |
|
set newmode_out [expr {$oldmode_out | 4}] ;#don't enable processed output too, even though it's required. keep symmetrical with disableAnsi? |
|
|
|
twapi::SetConsoleMode $h_out $newmode_out |
|
|
|
#what does window_input have to do with it?? |
|
#input handle modes |
|
#ENABLE_PROCESSED_INPUT 0x0001 ;#set to zero will allow ctrl-c to be reported as keyboard input rather than as a signal |
|
#ENABLE_LINE_INPUT 0x0002 |
|
#ENABLE_ECHO_INPUT 0x0004 |
|
#ENABLE_WINDOW_INPUT 0x0008 (default off when a terminal created) |
|
#ENABLE_MOUSE_INPUT 0x0010 |
|
#ENABLE_INSERT_MODE 0X0020 |
|
#ENABLE_QUICK_EDIT_MODE 0x0040 |
|
#ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200 (default off when a terminal created) (512) |
|
set h_in [twapi::get_console_handle stdin] |
|
set oldmode_in [twapi::GetConsoleMode $h_in] |
|
set newmode_in [expr {$oldmode_in | 8}] |
|
#set newmode_in [expr {$oldmode_in | 0x208}] |
|
|
|
twapi::SetConsoleMode $h_in $newmode_in |
|
|
|
return [list stdout [list from $oldmode_out to $newmode_out] stdin [list from $oldmode_in to $newmode_in]] |
|
} |
|
proc [namespace parent]::disableAnsi {} { |
|
set h_out [twapi::get_console_handle stdout] |
|
set oldmode_out [twapi::GetConsoleMode $h_out] |
|
set newmode_out [expr {$oldmode_out & ~4}] |
|
twapi::SetConsoleMode $h_out $newmode_out |
|
|
|
#??? review |
|
set h_in [twapi::get_console_handle stdin] |
|
set oldmode_in [twapi::GetConsoleMode $h_in] |
|
set newmode_in [expr {$oldmode_in & ~8}] |
|
twapi::SetConsoleMode $h_in $newmode_in |
|
|
|
|
|
return [list stdout [list from $oldmode_out to $newmode_out] stdin [list from $oldmode_in to $newmode_in]] |
|
} |
|
|
|
# |
|
proc [namespace parent]::enableVirtualTerminal {{channels {input output}}} { |
|
set ins [list in input stdin] |
|
set outs [list out output stdout stderr] |
|
set known [concat $ins $outs both] |
|
set directions [list] |
|
foreach v $channels { |
|
if {$v in $ins} { |
|
lappend directions input |
|
} elseif {$v in $outs} { |
|
lappend directions output |
|
} elseif {$v eq "both"} { |
|
lappend directions input output |
|
} |
|
if {$v ni $known} { |
|
error "enableVirtualTerminal expected channel values to be one of '$known'. (all values mapped to input and/or output)" |
|
} |
|
} |
|
set channels $directions ;#don't worry about dups. |
|
if {"both" in $channels} { |
|
lappend channels input output |
|
} |
|
set result [dict create] |
|
if {"output" in $channels} { |
|
#note setting stdout makes stderr have the same settings - ie there is really only one output to configure |
|
set h_out [twapi::get_console_handle stdout] |
|
set oldmode [twapi::GetConsoleMode $h_out] |
|
set newmode [expr {$oldmode | 4}] |
|
twapi::SetConsoleMode $h_out $newmode |
|
dict set result output [list from $oldmode to $newmode] |
|
} |
|
|
|
if {"input" in $channels} { |
|
set h_in [twapi::get_console_handle stdin] |
|
set oldmode_in [twapi::GetConsoleMode $h_in] |
|
set newmode_in [expr {$oldmode_in | 0x200}] |
|
twapi::SetConsoleMode $h_in $newmode_in |
|
dict set result input [list from $oldmode_in to $newmode_in] |
|
} |
|
|
|
return $result |
|
} |
|
proc [namespace parent]::disableVirtualTerminal {{channels {input output}}} { |
|
set ins [list in input stdin] |
|
set outs [list out output stdout stderr] |
|
set known [concat $ins $outs both] |
|
set directions [list] |
|
foreach v $channels { |
|
if {$v in $ins} { |
|
lappend directions input |
|
} elseif {$v in $outs} { |
|
lappend directions output |
|
} elseif {$v eq "both"} { |
|
lappend directions input output |
|
} |
|
if {$v ni $known} { |
|
error "disableVirtualTerminal expected channel values to be one of '$known'. (all values mapped to input and/or output)" |
|
} |
|
} |
|
set channels $directions ;#don't worry about dups. |
|
if {"both" in $channels} { |
|
lappend channels input output |
|
} |
|
set result [dict create] |
|
if {"output" in $channels} { |
|
#as above - configuring stdout does stderr too |
|
set h_out [twapi::get_console_handle stdout] |
|
set oldmode [twapi::GetConsoleMode $h_out] |
|
set newmode [expr {$oldmode & ~4}] |
|
twapi::SetConsoleMode $h_out $newmode |
|
dict set result output [list from $oldmode to $newmode] |
|
} |
|
if {"input" in $channels} { |
|
set h_in [twapi::get_console_handle stdin] |
|
set oldmode_in [twapi::GetConsoleMode $h_in] |
|
set newmode_in [expr {$oldmode_in & ~0x200}] |
|
twapi::SetConsoleMode $h_in $newmode_in |
|
dict set result input [list from $oldmode_in to $newmode_in] |
|
} |
|
|
|
#return [list stdout [list from $oldmode_out to $newmode_out] stdin [list from $oldmode_in to $newmode_in]] |
|
return $result |
|
} |
|
|
|
proc [namespace parent]::enableProcessedInput {} { |
|
set h_in [twapi::get_console_handle stdin] |
|
set oldmode_in [twapi::GetConsoleMode $h_in] |
|
set newmode_in [expr {$oldmode_in | 1}] |
|
twapi::SetConsoleMode $h_in $newmode_in |
|
return [list stdin [list from $oldmode_in to $newmode_in]] |
|
} |
|
proc [namespace parent]::disableProcessedInput {} { |
|
set h_in [twapi::get_console_handle stdin] |
|
set oldmode_in [twapi::GetConsoleMode $h_in] |
|
set newmode_in [expr {$oldmode_in & ~1}] |
|
twapi::SetConsoleMode $h_in $newmode_in |
|
return [list stdin [list from $oldmode_in to $newmode_in]] |
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
puts stderr "punk::console falling back to stty because twapi load failed" |
|
proc [namespace parent]::enableAnsi {} { |
|
puts stderr "punk::console::enableAnsi todo" |
|
} |
|
proc [namespace parent]::disableAnsi {} { |
|
} |
|
#? |
|
proc [namespace parent]::enableVirtualTerminal {{channels {input output}}} { |
|
} |
|
proc [namespace parent]::disableVirtualTerminal {{channels {input output}}} { |
|
} |
|
proc [namespace parent]::enableProcessedInput {args} { |
|
|
|
} |
|
proc [namespace parent]::disableProcessedInput {args} { |
|
|
|
} |
|
|
|
} |
|
|
|
proc [namespace parent]::enableRaw {{channel stdin}} { |
|
#variable is_raw |
|
variable previous_stty_state_$channel |
|
|
|
if {[package provide twapi] ne ""} { |
|
set console_handle [twapi::get_console_handle stdin] |
|
#returns dictionary |
|
#e.g -processedinput 1 -lineinput 1 -echoinput 1 -windowinput 0 -mouseinput 0 -insertmode 1 -quickeditmode 1 -extendedmode 1 -autoposition 0 |
|
set oldmode [twapi::get_console_input_mode] |
|
twapi::modify_console_input_mode $console_handle -lineinput 0 -echoinput 0 |
|
# Turn off the echo and line-editing bits |
|
#set newmode [dict merge $oldmode [dict create -lineinput 0 -echoinput 0]] |
|
set newmode [twapi::get_console_input_mode] |
|
|
|
tsv::set console is_raw 1 |
|
#don't disable handler - it will detect is_raw |
|
### twapi::set_console_control_handler {} |
|
return [list stdin [list from $oldmode to $newmode]] |
|
} elseif {[set sttycmd [auto_execok stty]] ne ""} { |
|
if {[set previous_stty_state_$channel] eq ""} { |
|
set previous_stty_state_$channel [exec {*}$sttycmd -g <@$channel] |
|
} |
|
|
|
exec {*}$sttycmd raw -echo <@$channel |
|
tsv::set console is_raw 1 |
|
#review - inconsistent return dict |
|
return [dict create stdin [list from [set previous_stty_state_$channel] to "" note "fixme - to state not shown"]] |
|
} else { |
|
error "punk::console::enableRaw Unable to use twapi or stty to set raw mode - aborting" |
|
} |
|
} |
|
|
|
#note: twapi GetStdHandle & GetConsoleMode & SetConsoleCombo unreliable - fails with invalid handle (somewhat intermittent.. after stdin reopened?) |
|
#could be we were missing a step in reopening stdin and console configuration? |
|
|
|
proc [namespace parent]::disableRaw {{channel stdin}} { |
|
#variable is_raw |
|
variable previous_stty_state_$channel |
|
|
|
if {[package provide twapi] ne ""} { |
|
set console_handle [twapi::get_console_handle stdin] |
|
set oldmode [twapi::get_console_input_mode] |
|
# Turn on the echo and line-editing bits |
|
twapi::modify_console_input_mode $console_handle -lineinput 1 -echoinput 1 |
|
set newmode [twapi::get_console_input_mode] |
|
tsv::set console is_raw 0 |
|
return [list stdin [list from $oldmode to $newmode]] |
|
} elseif {[set sttycmd [auto_execok stty]] ne ""} { |
|
#stty can return info on windows - but doesn't seem to be able to set anything. |
|
#review - is returned info even valid? |
|
|
|
set sttycmd [auto_execok stty] |
|
if {[set previous_stty_state_$channel] ne ""} { |
|
exec {*}$sttycmd [set previous_stty_state_$channel] |
|
set previous_stty_state_$channel "" |
|
return restored |
|
} |
|
exec {*}$sttycmd -raw echo <@$channel |
|
tsv::set console is_raw 0 |
|
#do we really want to exec stty yet again to show final 'to' state? |
|
#probably not. We should work out how to read the stty result flags and set a result.. or just limit from,to to showing echo and lineedit states. |
|
return [list stdin [list from "[set previous_stty_state_$channel]" to "" note "fixme - to state not shown"]] |
|
} else { |
|
error "punk::console::disableRaw Unable to use twapi or stty to unset raw mode - aborting" |
|
} |
|
} |
|
|
|
|
|
} |
|
|
|
#capturingendregex should capture ANY prefix, whole escape match - and a subcapture of the data we're interested in and match at end of string. |
|
#ie {(.*)(ESC(info)end)$} |
|
#e.g {(.*)(\x1bP44!~([:alnum:])\x1b\\)$} |
|
#we expect 4 results from regexp -indices -inline (overallmatch, prefix, wholeescape,info) |
|
#todo - check capturingendregex value supplied has appropriate captures and tail-anchor |
|
proc get_ansi_response_payload {query capturingendregex {inoutchannels {stdin stdout}}} { |
|
lassign $inoutchannels input output |
|
|
|
#chunks from input that need to be handled by readers |
|
upvar ::punk::console::input_chunks_waiting input_chunks_waiting |
|
|
|
#we need to cooperate with other stdin/$input readers and put data here if we overconsume. |
|
#Main repl reader may be currently active - or may be inactive. |
|
#This call could come from within code called by the main reader - or from user code running while main read-loop is temporarily disabled |
|
#In other contexts there may not even be another input reader |
|
|
|
#REVIEW - what if there is existing data in input_chunks_waiting - is it for us? |
|
#temp - let's keep alert to it until we decide if it's legit/required.. |
|
if {[info exists input_chunks_waiting($input)] && [llength $input_chunks_waiting($input)]} { |
|
#puts stderr "[punk::ansi::a+ cyan bold]get_ansi_response_payload called while input_chunks_waiting($input) contained data: $input_chunks_waiting($input)[punk::ansi::a]" |
|
} |
|
if {!$::punk::console::ansi_available} { |
|
return "" |
|
} |
|
# -- --- |
|
#set callid [info cmdcount] ;#info cmdcount is fast, though not as fast as clock clicks - and whilst not unique in a long-running app(will wrap?) - fine for this context |
|
#clock clicks is approx 2x faster - but can sometimes give duplicates if called sequentially e.g list [clock clicks] [clock clicks] |
|
#Either is suitable here, where subsequent calls will be relatively far apart in time |
|
#speed of call insignificant compared to function |
|
set callid [clock clicks] |
|
# -- --- |
|
# |
|
|
|
upvar ::punk::console::ansi_response_chunk accumulator |
|
upvar ::punk::console::ansi_response_wait waitvar |
|
upvar ::punk::console::ansi_response_queue queue |
|
upvar ::punk::console::ansi_response_queuedata queuedata |
|
upvar ::punk::console::ansi_response_clock clock |
|
upvar ::punk::console::ansi_response_timeoutid timeoutid |
|
set accumulator($callid) "" |
|
set waitvar($callid) "" |
|
lappend queue $callid |
|
|
|
|
|
|
|
#todo - use a linked array and an accumulatorid and waitvar id? When can there be more than one terminal query in-flight? |
|
|
|
set existing_handler [fileevent $input readable] ;#review! |
|
set this_handler ::punk::console::internal::ansi_response_handler_regex |
|
if {[lindex $existing_handler 0] eq $this_handler} { |
|
puts stderr "[punk::ansi::a+ red]Warning for callid $callid get_ansi_response_payload called while existing ansi response handler in place[a]: $this_handler" |
|
puts stderr "queue state: $queue" |
|
flush stderr |
|
|
|
if {[lindex $queue 0] ne $callid} { |
|
} |
|
error "get_ansi_response_payload - re-entrancy unrecoverable" |
|
} |
|
|
|
fileevent $input readable {} |
|
|
|
set input_state [fconfigure $input] |
|
#todo - make timeout configurable? |
|
set waitvarname "::punk::console::ansi_response_wait($callid)" |
|
#500ms is generally plenty for a terminal to respond.. but not in some cases. e.g event loop busy with stdin keypress?? review |
|
set timeoutid($callid) [after 1000 [list set $waitvarname timedout]] |
|
|
|
#JMN |
|
# - stderr vs stdout |
|
#It has to be same channel as used by functions such as test_char_width or erroneous results returned for those functions |
|
#(presumably race conditions as to when data hits console?) |
|
#review - experiment changing this and calling functions to stderr and see if it works |
|
#review - Are there disadvantages to using stdout vs stderr? |
|
|
|
#puts stdout "sending console request [ansistring VIEW $query]" |
|
puts -nonewline $output $query;flush $output |
|
|
|
#todo - test and save rawstate so we don't disableRaw if console was already raw |
|
if {![tsv::get console is_raw]} { |
|
set was_raw 0 |
|
punk::console::enableRaw |
|
} else { |
|
set was_raw 1 |
|
} |
|
fconfigure $input -blocking 0 |
|
# |
|
#in handler - its used for a boolean match (capturing aspect not used) |
|
set clock($callid) [clock millis] ;#time of launch - may be delay before first event depending on what's going on |
|
|
|
#first shot without using filevent, call the stdin reader directly - maybe it's there already |
|
#This can be significantly faster than setting up a fileevent (2024 e.g 1.5ms vs 65ms) |
|
$this_handler $input $callid $capturingendregex |
|
if {$waitvar($callid) ne "ok"} { |
|
fileevent $input readable [list $this_handler $input $callid $capturingendregex] |
|
} |
|
|
|
#JMN |
|
|
|
#response from terminal |
|
#e.g for cursor position \033\[46;1R |
|
|
|
|
|
if {[set waitvar($callid)] eq ""} { |
|
vwait ::punk::console::ansi_response_wait($callid) |
|
#puts stderr ">>>> end vwait1 $waitvar($callid)<<<<" |
|
while {[string match extend-* $waitvar($callid)]} { |
|
set extension [lindex [split $waitvar($callid) -] 1] |
|
#puts stderr "get_ansi_response_payload Extending timeout by $extension" |
|
#after cancel $timeoutid($callid) |
|
set timeoutid($callid) [after $extension [list set $waitvarname timedout]] |
|
vwait ::punk::console::ansi_response_wait($callid) |
|
} |
|
} |
|
#response handler automatically removes it's own fileevent |
|
fileevent $input readable {} ;#explicit remove anyway - review |
|
|
|
if {$waitvar($callid) ne "timedout"} { |
|
after cancel $timeoutid($callid) |
|
} else { |
|
puts stderr "timeout in get_ansi_response_payload. callid $callid Ansi request was:[ansistring VIEW -lf 1 -vt 1 $query]" |
|
} |
|
|
|
if {$was_raw == 0} { |
|
punk::console::disableRaw |
|
} |
|
#restore $input state |
|
fconfigure $input -blocking [dict get $input_state -blocking] |
|
|
|
|
|
|
|
set response [set accumulator($callid)] |
|
|
|
if {$response ne ""} { |
|
set got_match [regexp -indices $capturingendregex $response _match_indices prefix_indices response_indices payload_indices] |
|
if {$got_match} { |
|
set responsedata [string range $response {*}$response_indices] |
|
set payload [string range $response {*}$payload_indices] |
|
set prefixdata [string range $response {*}$prefix_indices] |
|
if {$prefixdata ne ""} { |
|
#puts stderr "Warning - get_ansi_response_payload read extra data at start - '[ansistring VIEW -lf 1 $prefixdata]' (response=[ansistring VIEW -lf 1 $responsedata])" |
|
lappend input_chunks_waiting($input) $prefixdata |
|
} |
|
} else { |
|
#timedout - or eof? |
|
puts stderr "get_ansi_response_payload callid:$callid regex match '$capturingendregex' to data '[ansistring VIEW -lf 1 -vt 1 $response]' not found" |
|
lappend input_chunks_waiting($input) $response |
|
set payload "" |
|
} |
|
} else { |
|
#timedout or eof? and nothing read |
|
set payload "" |
|
} |
|
|
|
#is there a way to know if existing_handler is input_chunks_waiting aware? |
|
if {[string length $existing_handler] && [lindex $existing_handler 0] ne $this_handler} { |
|
#puts "get_ansi_response_payload reinstalling ------>$existing_handler<------" |
|
fileevent $input readable $existing_handler |
|
#we may have consumed all pending input on $input - so there may be no trigger for the readable fileevent |
|
if {[llength $input_chunks_waiting($input)]} { |
|
#This is experimental If a handler is aware of input_chunks_waiting - there should be no need to schedule a trigger |
|
#If it isn't, but the handler can accept an existing chunk of data as a 'waiting' argument - we could trigger and pass it the waiting chunks - but there's no way to know its API. |
|
#we could look at info args - but that's not likely to tell us much in a robust way. |
|
#we could create a reflected channel for stdin? That is potentially an overreach..? |
|
#triggering it manually... as it was already listening - this should generally do no harm as it was the active reader anyway, but won't help with the missing data if it's input_chunks_waiting-unaware. |
|
set handler_args [info args [lindex $existing_handler 0]] |
|
if {[lindex $handler_args end] eq "waiting"} { |
|
#Looks like the existing handler is setup for punk repl cooperation. |
|
|
|
puts stdout "\n\n[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload callid $callid triggering existing handler\n $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel[punk::ansi::a]" |
|
puts stdout "[punk::ansi::a+ yellow bold]-->waiting: [ansistring VIEW -lf 1 -vt 1 $input_chunks_waiting($input)][punk::ansi::a]" |
|
flush stdout |
|
|
|
#concat and supply to existing handler in single text block - review |
|
#Note will only |
|
set waitingdata [join $input_chunks_waiting($input) ""] |
|
set input_chunks_waiting($input) [list] |
|
#after idle [list after 0 [list {*}$existing_handler $waitingdata]] |
|
after idle [list {*}$existing_handler $waitingdata] ;#after 0 may be put ahead of events it shouldn't be - review |
|
unset waitingdata |
|
} else { |
|
#! todo? for now, emit a clue as to what's happening. |
|
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload cannot trigger existing handler $existing_handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]" |
|
if {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { |
|
if {[eof $input]} { |
|
puts stdout "restarting repl" |
|
repl::reopen_stdin |
|
} |
|
} |
|
} |
|
} |
|
#Note - we still may be in_repl_handler here (which disables its own reader while executing commandlines) |
|
#The input_chunks_waiting may really belong to the existing_handler we found - but if it doesn't consume them they will end up being read by the repl_handler when it eventually re-enables. |
|
#todo - some better structure than just a list of waiting chunks indexed by input channel, so repl/other handlers can determine the context in which these waiting chunks were generated? |
|
} elseif {[package provide punk::repl::codethread] ne "" && [punk::repl::codethread::is_running]} { |
|
if {[llength $input_chunks_waiting($input)]} { |
|
#don't trigger the repl handler manually - we will inevitably get things out of order - as it knows when to enable/disable itself based on whether chunks are waiting. |
|
#triggering it by putting it on the eventloop will potentially result in re-entrancy |
|
#The cooperating reader must be designed to consume waiting chunks and only reschedule it's channel read handler once all waiting chunks have been consumed. |
|
#puts stderr "[punk::ansi::a+ green bold]--> repl_handler has chunks to consume [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]" |
|
} |
|
if {[eof $input]} { |
|
#test |
|
puts stdout "restarting repl" |
|
repl::reopen stdin |
|
} |
|
} |
|
|
|
catch { |
|
unset accumulator($callid) |
|
unset waitvar($callid) |
|
dict unset queuedata $callid |
|
} |
|
if {[llength $queue] > 1} { |
|
set next_callid [lindex $queue 1] |
|
set waitvar($callid) go_ahead |
|
} |
|
lpop queue 0 |
|
|
|
|
|
#set punk::console::chunk "" |
|
return $payload |
|
} |
|
|
|
|
|
#review - reading 1 byte at a time and repeatedly running the small capturing/completion regex seems a little inefficient... but we don't have a way to peek or put back chars (?) |
|
#review (we do have the punk::console::input_chunks_waiting($chan) array to cooperatively put back data - but this won't work for user scripts not aware of this) |
|
#review - timeout - what if terminal doesn't put data on stdin? error vs stderr report vs empty results |
|
#review - Main loop may need to detect some terminal responses and store them for lookup instead-of or as-well-as this handler? |
|
#e.g what happens to mouse-events while user code is executing? |
|
#we may still need this handler if such a loop doesn't exist. |
|
proc ansi_response_handler_regex {chan callid endregex} { |
|
upvar ::punk::console::ansi_response_chunk chunks |
|
upvar ::punk::console::ansi_response_wait waits |
|
upvar ::punk::console::ansi_response_clock clock ;#initial time in millis was set when fileevent was created |
|
|
|
#endregex should explicitly have a trailing $ |
|
set status [catch {read $chan 1} bytes] |
|
if { $status != 0 } { |
|
# Error on the channel |
|
fileevent $chan readable {} |
|
puts "ansi_response_handler_regex error reading $chan: $bytes" |
|
set waits($callid) [list error_read status $status bytes $bytes] |
|
} elseif {$bytes ne ""} { |
|
# Successfully read the channel |
|
#puts "got: [string length $bytes]bytes" |
|
append chunks($callid) $bytes |
|
#puts stderr [ansistring VIEW $chunks($callid)] |
|
if {[regexp $endregex $chunks($callid)]} { |
|
fileevent $chan readable {} |
|
#puts stderr "matched - setting ansi_response_wait($callid) ok" |
|
set waits($callid) ok |
|
} else { |
|
if {[string length $chunks($callid)] % 10 == 0 || $clock($callid) - [clock millis] > 50} { |
|
after cancel $::punk::console::ansi_response_timeoutid($callid) |
|
set waits($callid) extend-1000 |
|
} |
|
} |
|
} elseif {[catch {eof $chan}] || [eof $chan]} { |
|
catch {fileevent $chan readable {}} |
|
# End of file on the channel |
|
#review |
|
puts stderr "ansi_response_handler_regex end of file on channel $chan" |
|
set waits($callid) eof |
|
} elseif {![catch {fblocked $chan}] && [fblocked $chan]} { |
|
# Read blocked. Just return |
|
# Caller should be using timeout on the wait variable |
|
} else { |
|
fileevent $chan readable {} |
|
# Something else |
|
puts stderr "ansi_response_handler_regex Situation shouldn't be possible. No error and no bytes read on channel $chan but chan is not fblocked or EOF" |
|
set waits($callid) error_unknown_zerobytes_while_not_blocked_or_eof |
|
} |
|
} |
|
} ;#end namespace eval internal |
|
|
|
variable colour_disabled 0 |
|
#todo - move to punk::config |
|
# https://no-color.org |
|
if {[info exists ::env(NO_COLOR)]} { |
|
if {$::env(NO_COLOR) ne ""} { |
|
set colour_disabled 1 |
|
} |
|
} |
|
|
|
#a and a+ functions are not very useful when emitting directly to console |
|
#e.g puts [punk::console::a red]test[punk::console::a cyan] would produce a cyan coloured test as the commands are evaluated first |
|
|
|
proc a? {args} { |
|
#stdout |
|
variable ansi_wanted |
|
if {$ansi_wanted <= 0} { |
|
puts -nonewline [punk::ansi::ansistripraw [::punk::ansi::a?]] |
|
} else { |
|
tailcall ansi::a? {*}$args |
|
} |
|
} |
|
|
|
punk::args::set_alias ::punk::console::code_a+ ::punk::ansi::a+ |
|
proc code_a+ {args} { |
|
variable ansi_wanted |
|
if {$ansi_wanted <= 0} { |
|
return |
|
} |
|
#a and a+ are called a *lot* - avoid even slight overhead of tailcall as it doesn't give us anything useful here |
|
#tailcall punk::ansi::a+ {*}$args |
|
::punk::ansi::a+ {*}$args |
|
} |
|
proc code_a {args} { |
|
variable ansi_wanted |
|
if {$ansi_wanted <= 0} { |
|
return |
|
} |
|
#tailcall punk::ansi::a {*}$args |
|
::punk::ansi::a {*}$args |
|
} |
|
proc code_a? {args} { |
|
variable ansi_wanted |
|
if {$ansi_wanted <= 0} { |
|
return [punk::ansi::ansistripraw [::punk::ansi::a? {*}$args]] |
|
} else { |
|
tailcall ::punk::ansi::a? {*}$args |
|
} |
|
} |
|
|
|
#REVIEW! this needs reworking. |
|
#It needs to be clarified as to what ansi off is supposed to do. |
|
#Turning ansi off only stops new ansi being generated - but what about codes stored in configurations of existing elements such as tables/frames? |
|
#It will stop underlines/bold/reverse as well as SGR colours |
|
#what about ansi movement codes etc? |
|
#we already have colour on|off which disables SGR codes in a+ etc as well as stderr/stdout channel transforms |
|
proc ansi {{onoff {}}} { |
|
variable ansi_wanted |
|
if {[string length $onoff]} { |
|
set onoff [string tolower $onoff] |
|
switch -- $onoff { |
|
1 - |
|
on - |
|
true - |
|
yes { |
|
set ansi_wanted 1 |
|
} |
|
0 - |
|
off - |
|
false - |
|
no { |
|
set ansi_wanted 0 |
|
punk::ansi::sgr_cache -action clear |
|
} |
|
default { |
|
set ansi_wanted 2 |
|
} |
|
default { |
|
error "punk::console::ansi expected 0|1|on|off|true|false|yes|no|default" |
|
} |
|
} |
|
} |
|
catch {punk::repl::reset_prompt} |
|
puts stderr "::punk::console::ansi - use 'colour' command to turn SGR codes on/off" |
|
return [expr {$ansi_wanted}] |
|
} |
|
|
|
#colour |
|
# Turning colour off will stop SGR colour codes from being generated unless 'forcecolour' is added to the argument list for the punk::ans::a functions |
|
proc colour {{on {}}} { |
|
variable colour_disabled |
|
if {$on ne ""} { |
|
if {![string is boolean -strict $on]} { |
|
error "punk::console::colour expected a boolean e.g 0|1|on|off|true|false|yes|no" |
|
} |
|
#an experiment with complete disabling vs test of state for each call |
|
if {$on} { |
|
if {$colour_disabled} { |
|
#change of state |
|
punk::ansi::sgr_cache -action clear |
|
catch {punk::repl::reset_prompt} |
|
set colour_disabled 0 |
|
} |
|
} else { |
|
#we don't disable a/a+ entirely - they must still emit underlines/bold/reverse |
|
if {!$colour_disabled} { |
|
#change of state |
|
punk::ansi::sgr_cache -action clear |
|
catch {punk::repl::reset_prompt} |
|
set colour_disabled 1 |
|
} |
|
} |
|
} |
|
return [expr {!$colour_disabled}] |
|
} |
|
|
|
|
|
namespace eval ansi { |
|
proc a {args} { |
|
puts -nonewline [::punk::ansi::a {*}$args] |
|
} |
|
proc a? {args} { |
|
puts -nonewline stdout [::punk::ansi::a? {*}$args] |
|
} |
|
proc a+ {args} { |
|
puts -nonewline [::punk::ansi::a+ {*}$args] |
|
} |
|
proc clear {} { |
|
puts -nonewline stdout [punk::ansi::clear] |
|
} |
|
proc clear_above {} { |
|
puts -nonewline stdout [punk::ansi::clear_above] |
|
} |
|
proc clear_below {} { |
|
puts -nonewline stdout [punk::ansi::clear_below] |
|
} |
|
proc clear_all {} { |
|
puts -nonewline stdout [punk::ansi::clear_all] |
|
} |
|
proc reset {} { |
|
puts -nonewline stdout [punk::ansi::reset] |
|
} |
|
} |
|
namespace import ansi::clear |
|
namespace import ansi::clear_above |
|
namespace import ansi::clear_below |
|
namespace import ansi::clear_all |
|
namespace import ansi::reset |
|
|
|
namespace eval local { |
|
proc set_codepage_output {cpname} { |
|
#todo |
|
if {"windows" eq $::tcl_platform(platform)} { |
|
twapi::set_console_output_codepage $cpname |
|
} else { |
|
error "set_codepage_output unimplemented on $::tcl_platform(platform)" |
|
} |
|
} |
|
proc set_codepage_input {cpname} { |
|
#todo |
|
if {"windows" eq $::tcl_platform(platform)} { |
|
twapi::set_console_input_codepage $cpname |
|
} else { |
|
error "set_codepage_input unimplemented on $::tcl_platform(platform)" |
|
} |
|
} |
|
} |
|
namespace import local::set_codepage_output |
|
namespace import local::set_codepage_input |
|
|
|
# -- --- --- --- --- --- --- |
|
#get_ansi_response functions |
|
#review - can these functions sensibly be used on channels not attached to the local console? |
|
#ie can we default to {stdin stdout} but allow other channel pairs? |
|
# -- --- --- --- --- --- --- |
|
proc get_cursor_pos {{inoutchannels {stdin stdout}}} { |
|
#response from terminal |
|
#e.g \033\[46;1R |
|
set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload |
|
|
|
set request "\033\[6n" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
proc get_checksum_rect {id page t l b r {inoutchannels {stdin stdout}}} { |
|
#e.g \x1b\[P44!~E797\x1b\\ |
|
#re e.g {(.*)(\x1b\[P44!~([[:alnum:]])\x1b\[\\)$} |
|
set capturingregex [string map [list %id% $id] {(.*)(\x1bP%id%!~([[:alnum:]]+)\x1b\\)$}] |
|
set request "\x1b\[${id}\;${page}\;$t\;$l\;$b\;$r*y" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
proc get_device_status {{inoutchannels {stdin stdout}}} { |
|
set capturingregex {(.*)(\x1b\[([0-9]+)n)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[5n" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
|
|
variable last_da1_result "" |
|
#TODO - 22? 28? 32? |
|
#1 132 columns |
|
#2 Printer port extension |
|
#4 Sixel extension |
|
#6 Selective erase |
|
#7 DRCS |
|
#8 UDK |
|
#9 NRCS |
|
#12 SCS extension |
|
#15 Technical character set |
|
#18 Windowing capability |
|
#21 Horizontal scrolling |
|
#23 Greek extension |
|
#24 Turkish extension |
|
#42 ISO Latin 2 character set |
|
#44 PCTerm |
|
#45 Soft key map |
|
#46 ASCII emulation |
|
|
|
#https://vt100.net/docs/vt510-rm/DA1.html |
|
# |
|
proc get_device_attributes {{inoutchannels {stdin stdout}}} { |
|
#DA1 |
|
variable last_da1_result |
|
#first element in result is the terminal's architectural class 61,62,63,64.. ? |
|
#for vt100 we get things like: "ESC\[?1;0c" |
|
#for vt102 "ESC\[?6c" |
|
|
|
#set capturingregex {(.*)(\x1b\[\?6[0-9][;]*([;0-9]+)c)$} ;#must capture prefix,entire-response,response-payload |
|
set capturingregex {(.*)(\x1b\[\?([0-9]*[;0-9]+)c)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[c" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
set last_da1_result $payload |
|
return $payload |
|
} |
|
#https://vt100.net/docs/vt510-rm/DA2.html |
|
proc get_device_attributes_secondary {{inoutchannels {stdin stdout}}} { |
|
#DA2 |
|
set capturingregex {(.*)(\x1b\[\>([0-9]*[;][0-9]*[;][0-1]c))$} ;#must capture prefix,entire-response,response-payload |
|
#expect CSI > X;10|20;0|1c - docs suggest X should be something like 61. In practice we see 0 or 1 (windows) REVIEW |
|
set request "\x1b\[>c" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
proc get_device_attributes_tertiary {{inoutchannels {stdin stdout}}} { |
|
#DA3 |
|
set capturingregex {(.*)(\x1bP!\|([0-9]{8})\x1b\\)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[=c" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
proc get_terminal_id {{inoutchannels {stdin stdout}}} { |
|
#DA3 - alias |
|
get_device_attributes_tertiary $inoutchannels |
|
} |
|
|
|
proc get_tabstops {{inoutchannels {stdin stdout}}} { |
|
#DECTABSR \x1b\[2\$w |
|
#response example " ^[P2$u9/17/25/33/41/49/57/65/73/81^[\ " (where ^[ is \x1b) |
|
#set capturingregex {(.*)(\x1b\[P2$u()\x1b\[\\)} |
|
#set capturingregex {(.*)(\x1bP2$u((?:[0-9]+)*(?:\/[0-9]+)*)\x1b\\)$} |
|
set capturingregex {(.*)(\x1bP2\$u(.*)\x1b\\)$} |
|
set request "\x1b\[2\$w" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
set tabstops [split $payload "/"] |
|
return $tabstops |
|
} |
|
|
|
#a simple estimation of tab-width under assumption console is set with even spacing. |
|
#It's known this isn't always the case - but things like textutil::untabify2 take only a single value |
|
#on some systems test_char_width is a similar speed to get_tabstop_apparent_width - but on some test_char_width is much slower |
|
#we will use test_char_width as a fallback |
|
proc get_tabstop_apparent_width {{inoutchannels {stdin stdout}}} { |
|
set tslist [get_tabstops $inoutchannels] |
|
if {![llength $tslist]} { |
|
#either terminal failed to report - or none set. |
|
set testw [test_char_width \t] |
|
if {[string is integer -strict $testw]} { |
|
return $testw |
|
} |
|
#We don't support none - default to 8 |
|
return 8 |
|
} |
|
#we generally expect to see a tabstop at column 1 - but it may not be set. |
|
if {[lindex $tslist 0] eq "1"} { |
|
if {[llength $tslist] == 1} { |
|
set testw [test_char_width \t] |
|
if {[string is integer -strict $testw]} { |
|
return $testw |
|
} |
|
return 8 |
|
} else { |
|
set next [lindex $tslist 1] |
|
return [expr {$next - 1}] |
|
} |
|
} else { |
|
#simplistic guess at width - review - do we need to consider leftmost tabstops as more likely to be non-representative and look further into the list? |
|
if {[llength $tslist] == 1} { |
|
return [lindex $tslist 0] |
|
} else { |
|
return [expr {[lindex $tslist 1] - [lindex $tslist 0]}] |
|
} |
|
} |
|
} |
|
#default to 8 just because it seems to be most common default in terminals |
|
proc set_tabstop_width {{w 8}} { |
|
set tsize [get_size] |
|
set width [dict get $tsize columns] |
|
set mod [expr {$width % $w}] |
|
set max [expr {$width - $mod}] |
|
set tstops "" |
|
set c 1 |
|
while {$c <= $max} { |
|
append tstops [string repeat " " $w][punk::ansi::set_tabstop] |
|
incr c $w |
|
} |
|
set punk::console::tabwidth $w ;#we also attempt to read terminal's tabstops and set tabwidth to the apparent spacing of first non-1 value in tabstops list. |
|
catch {textutil::tabify::untabify2 "" $w} ;#textutil tabify can end up uninitialised and raise errors like "can't read Spaces(<n>).." after a tabstop change This call seems to keep tabify happy - review. |
|
puts -nonewline "[punk::ansi::clear_all_tabstops]\n[punk::ansi::set_tabstop]$tstops" |
|
} |
|
|
|
|
|
proc get_cursor_pos_list {{inoutchannels {stdin stdout}}} { |
|
return [split [get_cursor_pos $inoutchannels] ";"] |
|
} |
|
|
|
#todo - work out how to query terminal and set cell size in pixels |
|
#for now use the windows default |
|
variable cell_size |
|
set cell_size "" |
|
set cell_size_fallback 10x20 |
|
|
|
#todo - change -inoutchannels to -terminalobject with prebuilt default |
|
|
|
punk::args::define { |
|
@id -id ::punk::console::cell_size |
|
-inoutchannels -default {stdin stdout} -type list |
|
@values -min 0 -max 1 |
|
newsize -default "" -help\ |
|
"character cell pixel dimensions WxH" |
|
} |
|
proc cell_size {args} { |
|
set argd [punk::args::get_by_id ::punk::console::cell_size $args] |
|
set inoutchannels [dict get $argd opts -inoutchannels] |
|
set newsize [dict get $argd values newsize] |
|
|
|
variable cell_size |
|
if {$newsize eq ""} { |
|
#query existing setting |
|
if {$cell_size eq ""} { |
|
#not set - try to query terminal's overall dimensions |
|
set pixeldict [punk::console::get_xterm_pixels $inoutchannels] |
|
lassign $pixeldict _w sw _h sh |
|
if {[string is integer -strict $sw] && [string is integer -strict $sh]} { |
|
lassign [punk::console::get_size] _cols columns _rows rows |
|
#review - is returned size in pixels always a multiple of rows and cols? |
|
set w [expr {$sw / $columns}] |
|
set h [expr {$sh / $rows}] |
|
set cell_size ${w}x${h} |
|
return $cell_size |
|
} else { |
|
set cell_size $::punk::console::cell_size_fallback |
|
puts stderr "punk::console::cell_size unable to query terminal for pixel data - using default $cell_size" |
|
return $cell_size |
|
} |
|
} |
|
return $cell_size |
|
} |
|
#newsize supplied - try to set |
|
lassign [split [string tolower $newsize] x] w h |
|
if {![string is integer -strict $w] || ![string is integer -strict $h] || $w < 1 || $h < 1} { |
|
error "punk::sixel::cell_size error - expected format WxH where W and H are positive integers - got '$newsize'" |
|
} |
|
set cell_size ${w}x${h} |
|
} |
|
|
|
#todo - determine cursor on/off state before the call to restore properly. |
|
proc get_size {{inoutchannels {stdin stdout}}} { |
|
lassign $inoutchannels in out |
|
#we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810 |
|
#chan eof is faster whether chan exists or not than |
|
if {[catch {chan eof $out} is_eof]} { |
|
error "punk::console::get_size output channel $out seems to be closed ([info level 1])" |
|
} else { |
|
if {$is_eof} { |
|
error "punk::console::get_size eof on output channel $out ([info level 1])" |
|
} |
|
} |
|
#we don't need to care about the input channel if chan configure on the output can give us the info. |
|
#short circuit ansi cursor movement method if chan configure supports the -winsize value |
|
set outconf [chan configure $out] |
|
if {[dict exists $outconf -winsize]} { |
|
#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 |
|
lassign [dict get $outconf -winsize] cols lines |
|
if {[string is integer -strict $cols] && [string is integer -strict $lines]} { |
|
return [list columns $cols rows $lines] |
|
} |
|
#continue on to ansi mechanism if we didn't get 2 ints |
|
} |
|
|
|
if {[catch {chan eof $in} is_eof]} { |
|
error "punk::console::get_size input channel $in seems to be closed ([info level 1])" |
|
} else { |
|
if {$is_eof} { |
|
error "punk::console::get_size eof on input channel $in ([info level 1])" |
|
} |
|
} |
|
|
|
#keep out of catch - no point in even trying a restore move if we can't get start position - just fail here. |
|
lassign [get_cursor_pos_list $inoutchannels] start_row start_col |
|
|
|
if {[catch { |
|
#some terminals (conemu on windows) scroll the viewport when we make a big move down like this - a move to 1 1 immediately after cursor_save doesn't seem to fix that. |
|
#This issue also occurs when switching back from the alternate screen buffer - so perhaps that needs to be addressed elsewhere. |
|
puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::move 2000 2000] |
|
lassign [get_cursor_pos_list $inoutchannels] lines cols |
|
puts -nonewline $out [punk::ansi::move $start_row $start_col][punk::console::cursor_on];flush stdout |
|
set result [list columns $cols rows $lines] |
|
} errM]} { |
|
puts -nonewline $out [punk::ansi::move $start_row $start_col] |
|
puts -nonewline $out [punk::ansi::cursor_on] |
|
error "$errM" |
|
} else { |
|
return $result |
|
} |
|
} |
|
|
|
#faster than get_size when it is using ansi mechanism - but uses cursor_save - which we may want to avoid if calling during another operation which uses cursor save/restore |
|
proc get_size_cursorrestore {{inoutchannels {stdin stdout}}} { |
|
lassign $inoutchannels in out |
|
#we use the same shortcircuit mechanism as get_size to avoid ansi at all if the output channel will give us the info directly |
|
set outconf [chan configure $out] |
|
if {[dict exists $outconf -winsize]} { |
|
lassign [dict get $outconf -winsize] cols lines |
|
if {[string is integer -strict $cols] && [string is integer -strict $lines]} { |
|
return [list columns $cols rows $lines] |
|
} |
|
} |
|
|
|
if {[catch { |
|
#some terminals (conemu on windows) scroll the viewport when we make a big move down like this - a move to 1 1 immediately after cursor_save doesn't seem to fix that. |
|
#This issue also occurs when switching back from the alternate screen buffer - so perhaps that needs to be addressed elsewhere. |
|
puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000] |
|
lassign [get_cursor_pos_list $inoutchannels] lines cols |
|
puts -nonewline $out [punk::ansi::cursor_restore][punk::console::cursor_on];flush $out |
|
set result [list columns $cols rows $lines] |
|
} errM]} { |
|
puts -nonewline $out [punk::ansi::cursor_restore_dec] |
|
puts -nonewline $out [punk::ansi::cursor_on] |
|
error "$errM" |
|
} else { |
|
return $result |
|
} |
|
} |
|
proc get_dimensions {{inoutchannels {stdin stdout}}} { |
|
lassign [get_size $inoutchannels] _c cols _l lines |
|
return "${cols}x${lines}" |
|
} |
|
#the (xterm?) CSI 18t query is supported by *some* terminals |
|
proc get_xterm_size {{inoutchannels {stdin stdout}}} { |
|
set capturingregex {(.*)(\x1b\[8;([0-9]+;[0-9]+)t)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[18t" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
lassign [split $payload {;}] rows cols |
|
return [list columns $cols rows $rows] |
|
} |
|
proc get_xterm_pixels {{inoutchannels {stdin stdout}}} { |
|
set capturingregex {(.*)(\x1b\[4;([0-9]+;[0-9]+)t)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[14t" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
lassign [split $payload {;}] height width |
|
return [list width $width height $height] |
|
} |
|
proc get_mode_line_wrap {{inoutchannels {stdin stdout}}} { |
|
set capturingregex {(.*)(\x1b\[\?7;([0-9]+)\$y)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[?7\$p" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
#Terminals generally default to LNM being reset (off) ie enter key sends a lone <cr> |
|
#Terminals tested on windows either don't respond to this query, or respond with 0 (meaning mode not understood) |
|
#I presume from this that almost nobody is using LNM 1 (which sends both <cr> and <lf>) |
|
proc get_mode_LNM {{inoutchannels {stdin stdout}}} { |
|
set capturingregex {(.*)(\x1b\[\?20;([0-9]+)\$y)$} ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[?20\$p" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
#DECRPM responses e.g: |
|
# \x1b\[?7\;1\$y |
|
# \x1b\[?7\;2\$y |
|
#where 1 = set, 2 = unset. (0 = mode not recognised, 3 = permanently set, 4 = permanently unset) |
|
proc get_mode {num_or_name {inoutchannels {stdin stdout}}} { |
|
if {[string is integer -strict $num_or_name]} { |
|
set m $num_or_name |
|
} else { |
|
upvar ::punk::ansi::decmode_names decmode_names |
|
if {[dict exists $decmode_names $num_or_name]} { |
|
set m [dict get $decmode_names $num_or_name] |
|
} else { |
|
error "punk::console::get_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $decmode_names]" |
|
} |
|
} |
|
set capturingregex [string map [list %MODE% $m] {(.*)(\x1b\[\?%MODE%;([0-9]+)\$y)$}] ;#must capture prefix,entire-response,response-payload |
|
set request "\x1b\[?$m\$p" |
|
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels] |
|
return $payload |
|
} |
|
proc set_mode {num_or_name {inoutchannels {stdin stdout}}} { |
|
if {[string is integer -strict $num_or_name]} { |
|
set m $num_or_name |
|
} else { |
|
upvar ::punk::ansi::decmode_names decmode_names |
|
if {[dict exists $decmode_names $num_or_name]} { |
|
set m [dict get $decmode_names $num_or_name] |
|
} else { |
|
error "punk::console::set_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $decmode_names]" |
|
} |
|
} |
|
return "\x1b\[?${m}h" |
|
} |
|
proc unset_mode {num_or_name {inoutchannels {stdin stdout}}} { |
|
if {[string is integer -strict $num_or_name]} { |
|
set m $num_or_name |
|
} else { |
|
upvar ::punk::ansi::decmode_names decmode_names |
|
if {[dict exists $decmode_names $num_or_name]} { |
|
set m [dict get $decmode_names $num_or_name] |
|
} else { |
|
error "punk::console::unset_mode unrecognised mode '$num_or_name'. Known mode names: [dict keys $decmode_names]" |
|
} |
|
} |
|
return "\x1b\[?${m}l" |
|
} |
|
|
|
|
|
#terminals lie. This should be a reasonable (albeit relatively slow) test of actual width - but some terminals seem to miscalculate. |
|
#todo - a visual interactive test/questionnaire to ask user if things are lining up or if the terminal is telling fibs about cursor position. |
|
#todo - determine if these anomalies are independent of font |
|
#punk::ansi should be able to glean widths from unicode data files - but this may be incomplete - todo - compare with what terminal actually does. |
|
#review - vertical movements (e.g /n /v will cause emit 0 to be ineffective - todo - disallow?) |
|
proc test_char_width {char_or_string {emit 0}} { |
|
#return 1 |
|
#JMN |
|
#puts stderr "cwtest" |
|
variable ansi_available |
|
if {!$ansi_available} { |
|
puts stderr "No ansi - cannot test char_width of '$char_or_string' returning [string length $char_or_string]" |
|
return [string length $char_or_string] |
|
} |
|
|
|
if {!$emit} { |
|
puts -nonewline stdout \033\[2K\033\[1G ;#2K erase line, 1G cursor at col1 |
|
} |
|
set response "" |
|
if {[catch { |
|
set response [punk::console::get_cursor_pos] |
|
} errM]} { |
|
puts stderr "Cannot test_char_width for '[punk::ansi::ansistring VIEW $char_or_string]' - may be no console? Error message from get_cursor_pos: $errM" |
|
return |
|
} |
|
lassign [split $response ";"] _row1 col1 |
|
if {![string length $response] || ![string is integer -strict $col1]} { |
|
puts stderr "test_char_width Could not interpret response from get_cursor_pos for initial cursor pos. Response: '[punk::ansi::ansistring VIEW $response]'" |
|
flush stderr |
|
return |
|
} |
|
|
|
puts -nonewline stdout $char_or_string |
|
set response [punk::console::get_cursor_pos] |
|
lassign [split $response ";"] _row2 col2 |
|
if {![string is integer -strict $col2]} { |
|
puts stderr "test_char_width could not interpret response from get_cursor_pos for post-emit cursor pos. Response:'[punk::ansi::ansistring VIEW $response]'" |
|
flush stderr |
|
return |
|
} |
|
|
|
if {!$emit} { |
|
puts -nonewline stdout \033\[2K\033\[1G |
|
} |
|
flush stdout;#if we don't flush - a subsequent stderr write could move the cursor to a newline and interfere with our 2K1G erasure and cursor repositioning. |
|
return [expr {$col2 - $col1}] |
|
} |
|
|
|
#get reported cursor position after emitting teststring. |
|
#The row is more likely to be a lie than the column |
|
#With wrapping on we should be able to test if the terminal has an inconsistency between reported width and when it actually wraps. |
|
#(but as line wrapping generally occurs based on width - we probably won't see this - just 'apparently' early wrapping due to printing mismatch with width) |
|
#unfortunately if terminal reports something like \u200B as width 1, but doesn't print it - we can't tell. (vs reporting 1 wide and printing replacement char/space) |
|
#When cursor is already at bottom of screen, scrolling will occur so rowoffset will be zero |
|
#we either need to move cursor up before test - or use alt screen ( or scroll_up then scroll_down?) |
|
#for now we will use alt screen to reduce scrolling effects - REVIEW |
|
proc test_string_cursor {teststring {emit 0}} { |
|
variable ansi_available |
|
if {!$ansi_available} { |
|
puts stderr "No ansi - cannot test char_width of '$teststring' returning [string length $test_string]" |
|
return [string length $teststring] |
|
} |
|
punk::console::enable_alt_screen |
|
punk::console::move 0 0 |
|
if {!$emit} { |
|
puts -nonewline stdout \033\[2K\033\[1G ;#2K erase line, 1G cursor at col1 |
|
} |
|
set response "" |
|
if {[catch { |
|
set response [punk::console::get_cursor_pos] |
|
} errM]} { |
|
puts stderr "Cannot test_string_cursor for '[punk::ansi::ansistring VIEW $teststring]' - may be no console? Error message from get_cursor_pos: $errM" |
|
return |
|
} |
|
lassign [split $response ";"] row1 col1 |
|
if {![string length $response] || ![string is integer -strict $col1] || ![string is integer -strict $row1]} { |
|
puts stderr "test_string_cursor Could not interpret response from get_cursor_pos for initial cursor pos. Response: '[punk::ansi::ansistring VIEW $response]'" |
|
flush stderr |
|
return |
|
} |
|
|
|
puts -nonewline stdout $teststring |
|
flush stdout |
|
set response [punk::console::get_cursor_pos] |
|
lassign [split $response ";"] row2 col2 |
|
if {![string is integer -strict $col2] || ![string is integer -strict $row2]} { |
|
puts stderr "test_string_cursor could not interpret response from get_cursor_pos for post-emit cursor pos. Response:'[punk::ansi::ansistring VIEW $response]'" |
|
flush stderr |
|
return |
|
} |
|
|
|
if {!$emit} { |
|
puts -nonewline stdout \033\[2K\033\[1G |
|
} |
|
flush stdout;#if we don't flush - a subsequent stderr write could move the cursor to a newline and interfere with our 2K1G erasure and cursor repositioning. |
|
punk::console::disable_alt_screen |
|
return [list rowoffset [expr {$col2 - $col1}] columnoffset [expr {$row2 - $row1}]] |
|
} |
|
|
|
#todo! - improve ideally we want to use VT sequences to determine - and make a separate utility for testing via systemcalls/os api |
|
proc test_can_ansi {} { |
|
#don't set ansi_avaliable here - we want to be able to change things, retest etc. |
|
if {"windows" eq "$::tcl_platform(platform)"} { |
|
if {[package provide twapi] ne ""} { |
|
set h_out [twapi::get_console_handle stdout] |
|
set existing_mode [twapi::GetConsoleMode $h_out] |
|
if {[expr {$existing_mode & 4}]} { |
|
#virtual terminal processing happens to be enabled - so it's supported |
|
return 1 |
|
} |
|
#output mode |
|
#ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 |
|
|
|
#try temporarily setting it - if we get an error - ansi not supported |
|
if {[catch { |
|
twapi::SetConsoleMode $h_out [expr {$existing_mode | 4}] |
|
} errM]} { |
|
return 0 |
|
} |
|
#restore |
|
twapi::SetConsoleMode $h_out [expr {$existing_mode & ~4}] |
|
return 1 |
|
} else { |
|
#todo - try a cursorpos query and read stdin to see if we got a response? |
|
puts stderr "Unable to verify terminal ansi support - assuming modern default of true" |
|
puts stderr "to force disable, use command: ansi off" |
|
return 1 |
|
} |
|
} else { |
|
return 1 |
|
} |
|
} |
|
|
|
#review |
|
proc can_ansi {} { |
|
variable ansi_available |
|
if {!$ansi_available} { |
|
return 0 |
|
} |
|
#ansi_available defaults to -1 (unknown) |
|
if {$ansi_available == -1} { |
|
set ansi_available [test_can_ansi] |
|
return $ansi_available |
|
} |
|
return 1 |
|
} |
|
|
|
|
|
variable grapheme_cluster_support [dict create] ;#default empty dict for unknown/untested |
|
#todo - flag to retest? (for consoles where grapheme cluster support can be disabled e.g via decmode 2027) |
|
proc grapheme_cluster_support {} { |
|
variable grapheme_cluster_support |
|
if {[dict size $grapheme_cluster_support]} { |
|
return $grapheme_cluster_support |
|
} |
|
|
|
if {[info exists ::env(TERM_PROGRAM)]} { |
|
#terminals known to support grapheme clusters, but unable to respond to decmode request 2027 |
|
#wezterm (on windows as at 2024-12 decmode 2027 doesn't work) |
|
#REVIEW - what if terminal is remote wezterm? can/will this env variable |
|
# iterm and apple terminal also set TERM_PROGRAM |
|
if {[string tolower $::env(TERM_PROGRAM)] in [list wezterm]} { |
|
set is_available 1 |
|
return [dict create available 1 mode set] |
|
} |
|
} |
|
#where 1 = set, 2 = unset. (0 = mode not recognised, 3 = permanently set, 4 = permanently unset) |
|
set state [get_mode grapheme_clusters] ;#decmode 2027 extension |
|
set is_available 0 |
|
switch -- $state { |
|
0 { |
|
set m unsupported ;# the dec query is unsupported - but it's possible the terminal still has grapheme support |
|
} |
|
1 { |
|
set m set |
|
set is_available 1 |
|
} |
|
2 { |
|
set m unset |
|
} |
|
3 { |
|
set m permanently_set |
|
set is_available 1 |
|
} |
|
4 { |
|
set m permanently_unset |
|
} |
|
default { |
|
set m "BAD_RESPONSE" |
|
} |
|
} |
|
return [dict create available $is_available mode $m] |
|
} |
|
|
|
namespace eval ansi { |
|
proc cursor_on {} { |
|
puts -nonewline stdout [punk::ansi::cursor_on] |
|
} |
|
proc cursor_off {} { |
|
puts -nonewline stdout [punk::ansi::cursor_off] |
|
} |
|
} |
|
namespace import ansi::cursor_on |
|
namespace import ansi::cursor_off |
|
|
|
#review - the concept of using local mechanisms at all (ie apis) vs ansi is not necessarily something we want/need to support. |
|
#For the system to be really useful if needs to operate in conditions where the terminal is remote |
|
#This seems to be why windows console is deprecating various non-ansi api methods for interacting with the console. |
|
namespace eval local { |
|
proc titleset {windowtitle} { |
|
if {"windows" eq $::tcl_platform(platform)} { |
|
if {![catch {twapi::set_console_title $windowtitle} result]} { |
|
return $windowtitle |
|
} else { |
|
error "punk::console::local::titleset failed to set title - try punk::console::ansi::titleset" |
|
} |
|
} else { |
|
error "punk::console::local::titleset has no local mechanism to set the window title on this platform. try punk::console::ansi::titleset" |
|
} |
|
} |
|
proc titleget {} { |
|
if {"windows" eq $::tcl_platform(platform)} { |
|
if {![catch {twapi::get_console_title} result]} { |
|
return $result |
|
} else { |
|
error "punk::console::local::titleset failed to set title - ensure twapi is available" |
|
} |
|
} else { |
|
#titleget - https://invisible-island.net/xterm/xterm.faq.html#how2_title |
|
# won't work on all platforms/terminals - but may be worth implementing (for overtype::renderspace / frames etc) |
|
error "punk::console::local::titleget has no local mechanism to get the window title on this platform." |
|
} |
|
} |
|
} |
|
|
|
namespace eval ansi { |
|
proc titleset {windowtitle} { |
|
puts -nonewline stdout [punk::ansi::titleset $windowtitle] |
|
} |
|
} |
|
#namespace import ansi::titleset |
|
proc titleset {windowtitle} { |
|
variable ansi_wanted |
|
if { $ansi_wanted <= 0} { |
|
punk::console::local::titleset $windowtitle |
|
} else { |
|
ansi::titleset $windowtitle |
|
} |
|
} |
|
#no known pure-ansi solution |
|
proc titleget {} { |
|
return [local::titleget] |
|
} |
|
|
|
proc infocmp {} { |
|
set cmd1 [auto_execok infocmp] |
|
if {[string length $cmd1]} { |
|
puts stderr "" |
|
return [exec {*}$cmd1] |
|
} else { |
|
puts stderr "infocmp doesn't seem to be present" |
|
if {$::tcl_platform(platform) eq "FreeBSD"} { |
|
puts stderr "For FreeBSD - install ncurses to get infocmp and related binaries and also install terminfo-db" |
|
} |
|
set tcmd [auto_execok tput] |
|
if {[string length $tcmd]} { |
|
puts stderr "tput seems to be available. Try something like: tput -S - (freebsd)" |
|
} |
|
#todo - what? can tput query all caps? OS differences? |
|
} |
|
} |
|
|
|
|
|
#todo - compare speed with get_cursor_pos - work out why the big difference |
|
proc test_cursor_pos {} { |
|
if {![tsv::get console is_raw]} { |
|
set was_raw 0 |
|
enableRaw |
|
} else { |
|
set was_raw 1 |
|
} |
|
puts -nonewline stdout \033\[6n ;flush stdout |
|
fconfigure stdin -blocking 0 |
|
set info [read stdin 20] ;# |
|
after 1 |
|
if {[string first "R" $info] <=0} { |
|
append info [read stdin 20] |
|
} |
|
if {!$was_raw} { |
|
disableRaw |
|
} |
|
set data [string range [string trim $info] 2 end-1] |
|
return [split $data ";"] |
|
} |
|
|
|
#channel? |
|
namespace eval ansi { |
|
proc move {row col} { |
|
puts -nonewline stdout [punk::ansi::move $row $col] |
|
} |
|
proc move_forward {n} { |
|
puts -nonewline stdout [punk::ansi::move_forward $n] |
|
} |
|
proc move_back {n} { |
|
puts -nonewline stdout [punk::ansi::move_back $n] |
|
} |
|
proc move_up {n} { |
|
puts -nonewline stdout [punk::ansi::move_up $n] |
|
} |
|
proc move_down {n} { |
|
puts -nonewline stdout [punk::ansi::move_down $n] |
|
} |
|
proc move_column {col} { |
|
puts -nonewline stdout [punk::ansi::move_column $col] |
|
} |
|
proc move_row {row} { |
|
puts -nonewline stdout [punk::ansi::move_row $row] |
|
} |
|
proc move_emit {row col data args} { |
|
puts -nonewline stdout [punk::ansi::move_emit $row $col $data {*}$args] |
|
} |
|
proc move_emit_return {row col data args} { |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
set out "" |
|
append out [punk::ansi::move_emit $row $col $data {*}$args] |
|
if {!$is_in_raw} { |
|
incr orig_row -1 |
|
} |
|
move $orig_row $orig_col |
|
} |
|
proc scroll_up {n} { |
|
puts -nonewline stdout [punk::ansi::scroll_up $n] |
|
} |
|
proc scroll_down {n} { |
|
puts -nonewline stdout [punk::ansi::scroll_down $n] |
|
} |
|
proc enable_alt_screen {} { |
|
puts -nonewline stdout [punk::ansi::enable_alt_screen] |
|
} |
|
proc disable_alt_screen {} { |
|
puts -nonewline stdout [punk::ansi::disable_alt_screen] |
|
} |
|
|
|
#review - worth the extra microseconds to inline? might be if used in for example prompt on every keypress. |
|
#caller should build as much as possible using the punk::ansi versions to avoid extra puts calls |
|
proc cursor_save {} { |
|
#*** !doctools |
|
#[call [fun cursor_save]] |
|
puts -nonewline stdout \x1b\[s |
|
} |
|
proc cursor_restore {} { |
|
#*** !doctools |
|
#[call [fun cursor_restore]] |
|
puts -nonewline stdout \x1b\[u |
|
} |
|
#DEC equivalents of cursor_save/cursor_restore - perhaps more widely supported? |
|
proc cursor_save_dec {} { |
|
#*** !doctools |
|
#[call [fun cursor_save_dec]] |
|
puts -nonewline \x1b7 |
|
} |
|
proc cursor_restore_dec {} { |
|
#*** !doctools |
|
#[call [fun cursor_restore_dec]] |
|
puts -nonewline \x1b8 |
|
} |
|
|
|
proc insert_spaces {count} { |
|
puts -nonewline stdout \x1b\[${count}@ |
|
} |
|
proc delete_characters {count} { |
|
puts -nonewline \x1b\[${count}P |
|
} |
|
proc erase_characters {count} { |
|
puts -nonewline \x1b\[${count}X |
|
} |
|
proc insert_lines {count} { |
|
puts -nonewline \x1b\[${count}L |
|
} |
|
proc delete_lines {count} { |
|
puts -nonewline \x1b\[${count}M |
|
} |
|
} |
|
namespace import ansi::move |
|
namespace import ansi::move_emit |
|
namespace import ansi::move_forward |
|
namespace import ansi::move_back |
|
namespace import ansi::move_up |
|
namespace import ansi::move_down |
|
namespace import ansi::move_column |
|
namespace import ansi::move_row |
|
namespace import ansi::cursor_save |
|
namespace import ansi::cursor_restore |
|
namespace import ansi::cursor_save_dec |
|
namespace import ansi::cursor_restore_dec |
|
namespace import ansi::scroll_up |
|
namespace import ansi::scroll_down |
|
namespace import ansi::enable_alt_screen |
|
namespace import ansi::disable_alt_screen |
|
namespace import ansi::insert_spaces |
|
namespace import ansi::delete_characters |
|
namespace import ansi::erase_characters |
|
namespace import ansi::insert_lines |
|
namespace import ansi::delete_lines |
|
|
|
|
|
#experimental |
|
proc rhs_prompt {col text} { |
|
package require textblock |
|
lassign [textblock::size $text] _w tw _h th |
|
if {$th > 1} { |
|
#move up first.. need to know current line? |
|
} |
|
#set blanks [string repeat " " [expr {$col + $tw}]] |
|
#puts -nonewline [punk::ansi::erase_eol]$blanks;move_emit_return this $col $text |
|
#puts -nonewline [move_emit_return this $col [punk::ansi::insert_spaces 150]$text] |
|
cursor_save_dec |
|
#move_emit_return this $col [punk::ansi::move_forward 50][punk::ansi::insert_spaces 150][punk::ansi::move_back 50][punk::ansi::move_forward $col]$text |
|
#puts -nonewline [punk::ansi::insert_spaces 150][punk::ansi::move_column $col]$text |
|
puts -nonewline [punk::ansi::erase_eol][punk::ansi::move_column $col]$text |
|
cursor_restore |
|
} |
|
proc move_emit_return {row col data args} { |
|
#todo detect if in raw mode or not? |
|
set is_in_raw 0 |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
|
|
set commands "" |
|
append commands [punk::ansi::move_emit $row $col $data] |
|
foreach {row col data} $args { |
|
append commands [punk::ansi::move_emit $row $col $data] |
|
} |
|
if {!$is_in_raw} { |
|
incr orig_row -1 |
|
} |
|
append commands [punk::ansi::move $orig_row $orig_col] |
|
puts -nonewline stdout $commands |
|
return "" |
|
} |
|
#we can be (slightly?) faster and more efficient if we use the consoles cursor_save_dec command - but each savecursor overrides any previous one. |
|
#leave cursor_off/cursor_on to caller who can wrap more efficiently.. |
|
proc cursorsave_move_emit_return {row col data args} { |
|
set commands "" |
|
append commands [punk::ansi::cursor_save_dec] |
|
append commands [punk::ansi::move_emit $row $col $data] |
|
foreach {row col data} $args { |
|
append commands [punk::ansi::move_emit $row $col $data] |
|
} |
|
append commands [punk::ansi::cursor_restore_dec] |
|
puts -nonewline stdout $commands; flush stdout |
|
} |
|
proc move_emitblock_return {row col textblock} { |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
|
|
set commands "" |
|
foreach ln [split $textblock \n] { |
|
append commands [punk::ansi::move_emit $row $col $ln] |
|
incr row |
|
} |
|
append commands [punk::ansi::move $orig_row $orig_col] |
|
puts -nonewline $commands |
|
return |
|
} |
|
proc cursorsave_move_emitblock_return {row col textblock} { |
|
set commands "" |
|
append commands [punk::ansi::cursor_save_dec] |
|
foreach ln [split $textblock \n] { |
|
append commands [punk::ansi::move_emit $row $col $ln] |
|
incr row |
|
} |
|
append commands [punk::ansi::cursor_restore_dec] |
|
puts -nonewline stdout $commands;flush stdout |
|
return |
|
} |
|
proc move_call_return {row col script} { |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
move $row $col |
|
uplevel 1 $script |
|
move $orig_row $orig_col |
|
} |
|
|
|
#this doesn't work - we would need an internal virtual screen structure to pick up cursor attributes from arbitrary locations? |
|
# ncurses and its ilk may have something like that - but we specifically want to avoid curses libraries |
|
proc pick {row col} { |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
set test "" |
|
#set test [a green Yellow] |
|
move_emit $row $col $test\0337 |
|
puts -nonewline \0338\033\[${orig_row}\;${orig_col}H |
|
} |
|
proc pick_emit {row col data} { |
|
set test "" |
|
#set test [a green Purple] |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
move_emit $row $col $test\0337 |
|
puts -nonewline \0338\033\[${orig_row}\;${orig_col}H$data |
|
} |
|
|
|
# -- --- --- --- --- --- |
|
namespace eval ansi { |
|
proc test_decaln {} { |
|
puts -nonewline stdout [punk::ansi::test_decaln] |
|
} |
|
} |
|
namespace import ansi::test_decaln |
|
|
|
namespace eval clock { |
|
|
|
#map chars of chars "0" to "?"" ie 0x30 to x3f |
|
variable fontmap1 { |
|
7C CE DE F6 E6 C6 7C 00 |
|
30 70 30 30 30 30 FC 00 |
|
78 CC 0C 38 60 CC FC 00 |
|
78 CC 0C 38 0C CC 78 00 |
|
1C 3C 6C CC FE 0C 1E 00 |
|
FC C0 F8 0C 0C CC 78 00 |
|
38 60 C0 F8 CC CC 78 00 |
|
FC CC 0C 18 30 30 30 00 |
|
78 CC CC 78 CC CC 78 00 |
|
78 CC CC 7C 0C 18 70 00 |
|
00 18 18 00 00 18 18 00 |
|
00 18 18 00 00 18 18 30 |
|
18 30 60 C0 60 30 18 00 |
|
00 00 7E 00 7E 00 00 00 |
|
60 30 18 0C 18 30 60 00 |
|
3C 66 0C 18 18 00 18 00 |
|
} |
|
#libungif extras |
|
append fontmap1 { |
|
7c 82 9a aa aa 9e 7c 00 |
|
38 6c c6 c6 fe c6 c6 00 |
|
fc c6 c6 fc c6 c6 fc 00 |
|
} |
|
|
|
#https://github.com/Distrotech/libungif/blob/master/lib/gif_font.c |
|
variable fontmap { |
|
} |
|
#ascii row 0x00 to 0x1F control chars |
|
#(cp437 glyphs) |
|
append fontmap { |
|
00 00 00 00 00 00 00 00 |
|
3c 42 a5 81 bd 42 3c 00 |
|
3c 7e db ff c3 7e 3c 00 |
|
00 ee fe fe 7c 38 10 00 |
|
10 38 7c fe 7c 38 10 00 |
|
00 3c 18 ff ff 08 18 00 |
|
10 38 7c fe fe 10 38 00 |
|
00 00 18 3c 18 00 00 00 |
|
ff ff e7 c3 e7 ff ff ff |
|
00 3c 42 81 81 42 3c 00 |
|
ff c3 bd 7e 7e bd c3 ff |
|
1f 07 0d 7c c6 c6 7c 00 |
|
00 7e c3 c3 7e 18 7e 18 |
|
04 06 07 04 04 fc f8 00 |
|
0c 0a 0d 0b f9 f9 1f 1f |
|
00 92 7c 44 c6 7c 92 00 |
|
00 00 60 78 7e 78 60 00 |
|
00 00 06 1e 7e 1e 06 00 |
|
18 7e 18 18 18 18 7e 18 |
|
66 66 66 66 66 00 66 00 |
|
ff b6 76 36 36 36 36 00 |
|
7e c1 dc 22 22 1f 83 7e |
|
00 00 00 7e 7e 00 00 00 |
|
18 7e 18 18 7e 18 00 ff |
|
18 7e 18 18 18 18 18 00 |
|
18 18 18 18 18 7e 18 00 |
|
00 04 06 ff 06 04 00 00 |
|
00 20 60 ff 60 20 00 00 |
|
00 00 00 c0 c0 c0 ff 00 |
|
00 24 66 ff 66 24 00 00 |
|
00 00 10 38 7c fe 00 00 |
|
00 00 00 fe 7c 38 10 00 |
|
} |
|
#chars SP to "/" row 0x20 to 0x2f |
|
append fontmap { |
|
00 00 00 00 00 00 00 00 |
|
30 30 30 30 30 00 30 00 |
|
66 66 00 00 00 00 00 00 |
|
6c 6c fe 6c fe 6c 6c 00 |
|
10 7c d2 7c 86 7c 10 00 |
|
f0 96 fc 18 3e 72 de 00 |
|
30 48 30 78 ce cc 78 00 |
|
0c 0c 18 00 00 00 00 00 |
|
10 60 c0 c0 c0 60 10 00 |
|
10 0c 06 06 06 0c 10 00 |
|
00 54 38 fe 38 54 00 00 |
|
00 18 18 7e 18 18 00 00 |
|
00 00 00 00 00 00 18 70 |
|
00 00 00 7e 00 00 00 00 |
|
00 00 00 00 00 00 18 00 |
|
02 06 0c 18 30 60 c0 00 |
|
} |
|
#chars "0" to "?"" row 0x30 to 0x3f |
|
append fontmap { |
|
7c c6 c6 c6 c6 c6 7c 00 |
|
18 38 78 18 18 18 3c 00 |
|
7c c6 06 0c 30 60 fe 00 |
|
7c c6 06 3c 06 c6 7c 00 |
|
0e 1e 36 66 fe 06 06 00 |
|
fe c0 c0 fc 06 06 fc 00 |
|
7c c6 c0 fc c6 c6 7c 00 |
|
fe 06 0c 18 30 60 60 00 |
|
7c c6 c6 7c c6 c6 7c 00 |
|
7c c6 c6 7e 06 c6 7c 00 |
|
00 30 00 00 00 30 00 00 |
|
00 30 00 00 00 30 20 00 |
|
00 1c 30 60 30 1c 00 00 |
|
00 00 7e 00 7e 00 00 00 |
|
00 70 18 0c 18 70 00 00 |
|
7c c6 0c 18 30 00 30 00 |
|
} |
|
#chars "@" to "O" row 0x40 to 0x4f |
|
append fontmap { |
|
7c 82 9a aa aa 9e 7c 00 |
|
38 6c c6 c6 fe c6 c6 00 |
|
fc c6 c6 fc c6 c6 fc 00 |
|
7c c6 c6 c0 c0 c6 7c 00 |
|
f8 cc c6 c6 c6 cc f8 00 |
|
fe c0 c0 fc c0 c0 fe 00 |
|
fe c0 c0 fc c0 c0 c0 00 |
|
7c c6 c0 ce c6 c6 7e 00 |
|
c6 c6 c6 fe c6 c6 c6 00 |
|
78 30 30 30 30 30 78 00 |
|
1e 06 06 06 c6 c6 7c 00 |
|
c6 cc d8 f0 d8 cc c6 00 |
|
c0 c0 c0 c0 c0 c0 fe 00 |
|
c6 ee fe d6 c6 c6 c6 00 |
|
c6 e6 f6 de ce c6 c6 00 |
|
7c c6 c6 c6 c6 c6 7c 00 |
|
} |
|
#chars "P" to "_" row 0x50 to 0x5f |
|
append fontmap { |
|
fc c6 c6 fc c0 c0 c0 00 |
|
7c c6 c6 c6 c6 c6 7c 06 |
|
fc c6 c6 fc c6 c6 c6 00 |
|
78 cc 60 30 18 cc 78 00 |
|
fc 30 30 30 30 30 30 00 |
|
c6 c6 c6 c6 c6 c6 7c 00 |
|
c6 c6 c6 c6 c6 6c 38 00 |
|
c6 c6 c6 d6 fe ee c6 00 |
|
c6 c6 6c 38 6c c6 c6 00 |
|
c3 c3 66 3c 18 18 18 00 |
|
fe 0c 18 30 60 c0 fe 00 |
|
3c 30 30 30 30 30 3c 00 |
|
c0 60 30 18 0c 06 03 00 |
|
3c 0c 0c 0c 0c 0c 3c 00 |
|
00 38 6c c6 00 00 00 00 |
|
00 00 00 00 00 00 00 ff |
|
} |
|
#chars "`" to "o" row 0x60 to 0x6f |
|
append fontmap { |
|
30 30 18 00 00 00 00 00 |
|
00 00 7c 06 7e c6 7e 00 |
|
c0 c0 fc c6 c6 e6 dc 00 |
|
00 00 7c c6 c0 c0 7e 00 |
|
06 06 7e c6 c6 ce 76 00 |
|
00 00 7c c6 fe c0 7e 00 |
|
1e 30 7c 30 30 30 30 00 |
|
00 00 7e c6 ce 76 06 7c |
|
c0 c0 fc c6 c6 c6 c6 00 |
|
18 00 38 18 18 18 3c 00 |
|
18 00 38 18 18 18 18 f0 |
|
c0 c0 cc d8 f0 d8 cc 00 |
|
38 18 18 18 18 18 3c 00 |
|
00 00 cc fe d6 c6 c6 00 |
|
00 00 fc c6 c6 c6 c6 00 |
|
00 00 7c c6 c6 c6 7c 00 |
|
} |
|
#chars "p" to DEL row 0x70 to 0x7f |
|
append fontmap { |
|
00 00 fc c6 c6 e6 dc c0 |
|
00 00 7e c6 c6 ce 76 06 |
|
00 00 6e 70 60 60 60 00 |
|
00 00 7c c0 7c 06 fc 00 |
|
30 30 7c 30 30 30 1c 00 |
|
00 00 c6 c6 c6 c6 7e 00 |
|
00 00 c6 c6 c6 6c 38 00 |
|
00 00 c6 c6 d6 fe 6c 00 |
|
00 00 c6 6c 38 6c c6 00 |
|
00 00 c6 c6 ce 76 06 7c |
|
00 00 fc 18 30 60 fc 00 |
|
0e 18 18 70 18 18 0e 00 |
|
18 18 18 00 18 18 18 00 |
|
e0 30 30 1c 30 30 e0 00 |
|
00 00 70 9a 0e 00 00 00 |
|
00 00 18 3c 66 ff 00 00 |
|
} |
|
|
|
proc bigstr {str row col} { |
|
variable fontmap |
|
#curses attr off reverse |
|
#a noreverse |
|
set reverse 0 |
|
set output "" |
|
set charno 0 |
|
foreach char [split $str {}] { |
|
binary scan $char c f |
|
set index [expr {$f * 8}] |
|
for {set line 0} {$line < 8} {incr line} { |
|
set bitline 0x[lindex $fontmap [expr {$index + $line}]] |
|
binary scan [binary format c $bitline] B8 charline |
|
set cix 0 |
|
foreach c [split $charline {}] { |
|
if {$c} { |
|
append output [punk::ansi::move_emit [expr {$row + $line}] [expr {$col + $charno * 8 + $cix}] "[a+ reverse] [a+ noreverse]"] |
|
#curses attr on reverse |
|
#curses move [expr $row + $line] [expr $col + $charno * 8 + $cix] |
|
#curses puts " " |
|
} |
|
incr cix |
|
} |
|
} |
|
incr charno |
|
} |
|
return $output |
|
} |
|
proc get_time {} { |
|
overtype::left -width 70 "" [bigstr [clock format [clock seconds] -format %H:%M:%S] 1 1] |
|
} |
|
|
|
|
|
proc display1 {} { |
|
#punk::console::clear |
|
punk::console::move_call_return 20 20 {punk::console::clear_above} |
|
flush stdout |
|
punk::console::move_call_return 0 0 {puts stdout [bigstr [clock format [clock seconds] -format %H:%M:%S] 10 5]} |
|
after 2000 {punk::console::clock::display} |
|
} |
|
proc display {} { |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
punk::console::move 20 20 |
|
punk::console::clear_above |
|
punk::console::move 0 0 |
|
puts -nonewline [bigstr [clock format [clock seconds] -format %H:%M:%S] 10 5] |
|
|
|
punk::console::move $orig_row $orig_col |
|
#after 2000 {punk::console::clock::display} |
|
} |
|
|
|
proc displaystr {str} { |
|
lassign [punk::console::get_cursor_pos_list] orig_row orig_col |
|
punk::console::move 20 20 |
|
punk::console::clear_above |
|
punk::console::move 0 0 |
|
puts -nonewline [bigstr $str 10 5] |
|
|
|
punk::console::move $orig_row $orig_col |
|
} |
|
|
|
|
|
} |
|
|
|
proc test {} { |
|
set high_unicode_length [string length \U00010000] |
|
set can_high_unicode 0 |
|
set can_regex_high_unicode 0 |
|
set can_terminal_report_dingbat_width 0 |
|
set can_terminal_report_diacritic_width 0 |
|
if {$high_unicode_length != 1} { |
|
puts stderr "punk::console WARNING: no modern unicode support in this Tcl version. High unicode values not properly supported. (string length \\U00010000 : $high_unicode_length should be 1)" |
|
} else { |
|
set can_high_unicode 1 |
|
set can_regex_high_unicode [string match [regexp -all -inline {[\U80-\U0010FFFF]} \U0001F525] \U0001F525] |
|
if {!$can_regex_high_unicode} { |
|
puts stderr "punk::console warning: TCL version cannot perform braced regex of high unicode" |
|
} |
|
} |
|
set dingbat_heavy_plus_width [punk::console::test_char_width \U2795] ;#review - may be font dependent. We chose a wide dingbat as a glyph that is hopefully commonly renderable - and should display 2 wide. |
|
#This will give a false report that terminal can't report width if the glyph (or replacement glyph) is actually being rendered 1 wide. |
|
#we can't distinguish without user interaction? |
|
if {$dingbat_heavy_plus_width == 2} { |
|
set can_terminal_report_dingbat_width 1 |
|
} else { |
|
puts stderr "punk::console warning: terminal either not displaying wide unicode as wide, or unable to report width properly." |
|
} |
|
set diacritic_width [punk::console::test_char_width a\u0300] |
|
if {$diacritic_width == 1} { |
|
set can_terminal_report_diacritic_width 1 |
|
} else { |
|
puts stderr "punk::console warning: terminal unable to report diacritic width properly." |
|
} |
|
|
|
if {$can_high_unicode && $can_regex_high_unicode && $can_terminal_report_dingbat_width && $can_terminal_report_diacritic_width} { |
|
set result [list result ok] |
|
} else { |
|
set result [list result error] |
|
} |
|
return $result |
|
} |
|
#run the test and allow warnings to be emitted to stderr on package load. User should know the terminal and/or Tcl version are not optimal for unicode character work |
|
#set testresult [test1] |
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace punk::console ---}] |
|
} |
|
|
|
namespace eval punk::console::check { |
|
variable has_bug_legacysymbolwidth -1 ;#undetermined |
|
proc has_bug_legacysymbolwidth {} { |
|
#some terminals (on windows as at 2024) miscount width of these single-width blocks internally |
|
#resulting in extra spacing that is sometimes well removed from the character itself (e.g at next ansi reset) |
|
#This was fixed in windows-terminal based systems (2021) but persists in others. |
|
#https://github.com/microsoft/terminal/issues/11694 |
|
variable has_bug_legacysymbolwidth |
|
if {!$has_bug_legacysymbolwidth} { |
|
return 0 |
|
} |
|
if {$has_bug_legacysymbolwidth == -1} { |
|
#run the test using ansi movement |
|
#we only test a specific character from the known problematic set |
|
set w [punk::console::test_char_width \U1fb7d] |
|
if {$w == 1} { |
|
set has_bug_legacysymbolwidth 0 |
|
} else { |
|
#can return 2 on legacy window consoles for example |
|
set has_bug_legacysymbolwidth 1 |
|
} |
|
return $has_bug_legacysymbolwidth |
|
} |
|
return 1 |
|
} |
|
variable has_bug_zwsp -1 ;#undetermined |
|
proc has_bug_zwsp {} { |
|
#Note that some terminals behave differently regarding a leading zwsp vs one that is inline between other chars. |
|
#we are only testing the inline behaviour here. |
|
variable has_bug_zwsp |
|
if {!$has_bug_zwsp} { |
|
return 0 |
|
} |
|
if {$has_bug_zwsp == -1} { |
|
set w [punk::console::test_char_width X\u200bY] |
|
} |
|
if {$w == 2} { |
|
return 0 |
|
} else { |
|
#may return 3 - but this gives no indication of whether terminal hides it or not. |
|
return 1 |
|
} |
|
return 1 |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide punk::console [namespace eval punk::console { |
|
variable version |
|
set version 0.1.1 |
|
}] |
|
return |
|
|
|
#*** !doctools |
|
#[manpage_end]
|
|
|