# -*- tcl -*- # Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt # module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm # # 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) 2025 # # @@ Meta Begin # Application punk::winshell 999999.0a1.0 # Meta platform tcl # Meta license # @@ Meta End # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[manpage_begin shellspy_module_punk::winshell 0 999999.0a1.0] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] #[require punk::winshell] #[keywords module] #[description] #[para] - # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[section Overview] #[para] overview of punk::winshell #[subsection Concepts] #[para] - # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Requirements # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[subsection dependencies] #[para] packages used by punk::winshell #[list_begin itemized] package require Tcl 8.6- #*** !doctools #[item] [package {Tcl 8.6}] # #package require frobz # #*** !doctools # #[item] [package {frobz}] #*** !doctools #[list_end] # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[section API] # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # oo::class namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #tcl::namespace::eval punk::winshell::class { #*** !doctools #[subsection {Namespace punk::winshell::class}] #[para] class definitions #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { #*** !doctools #[list_begin enumerated] # oo::class create interface_sample1 { # #*** !doctools # #[enum] CLASS [class interface_sample1] # #[list_begin definitions] # method test {arg1} { # #*** !doctools # #[call class::interface_sample1 [method test] [arg arg1]] # #[para] test method # puts "test: $arg1" # } # #*** !doctools # #[list_end] [comment {-- end definitions interface_sample1}] # } #*** !doctools #[list_end] [comment {--- end class enumeration ---}] #} #} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Base namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::winshell { tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase #variable xyz #*** !doctools #[subsection {Namespace punk::winshell}] #[para] Core API functions for punk::winshell #[list_begin definitions] #The windows api we need here is createPseudoConsole et al. variable autoshellid 0 variable shellinfo [dict create] #test of exec and named pipes. #we don't get a console proc cmdexec {{id ""}} { variable autoshellid variable shellinfo package require twapi set pipebase {\\.\pipe\punkwinshell} if {$id eq ""} { incr autoshellid set shellid $autoshellid } else { set shellid $id } set pipename_stdin $pipebase$shellid-stdin set pipename_stdout $pipebase$shellid-stdout set pipename_stderr $pipebase$shellid-stderr #swapped thisend/child - labels now wrong - todo - relabel or swap back? set h_stdin [twapi::namedpipe_server $pipename_stdin] ;#handle for child process redirection set p_stdin [twapi::namedpipe_client $pipename_stdin] ;#this end set h_stdout [twapi::namedpipe_server $pipename_stdout] ;#handle for child process redirection set p_stdout [twapi::namedpipe_client $pipename_stdout] ;#this end chan configure $p_stdout -blocking 0 set h_stderr [twapi::namedpipe_server $pipename_stderr] ;#handle for child process redirection set p_stderr [twapi::namedpipe_client $pipename_stderr] ;#this end chan configure $p_stderr -blocking 0 set pid [exec cmd.exe /k >@$p_stdout 2>@$p_stderr <@$p_stdin &] dict set shellinfo $shellid id $shellid dict set shellinfo $shellid pid $pid dict set shellinfo $shellid stdin $h_stdin dict set shellinfo $shellid stdout $h_stdout dict set shellinfo $shellid stderr $h_stderr return [dict get $shellinfo $shellid] } variable ack 0 proc handle_out {chan args} { variable ack #if {[catch { # if {$ack} { # punk::console::move_emit_return 3 79 "\\" # set ack 0 # } else { # punk::console::move_emit_return 3 79 / # set ack 1 # } #} errM]} { # puts "err on move_emit_return" #} puts -nonewline stdout [punk::ansi::ansistring VIEW [read $chan]] } proc handle_err {chan args} { variable ack #if {$ack} { # punk::console::move_emit_return 3 79 - # set ack 0 #} else { # punk::console::move_emit_return 3 79 | # set ack 1 #} puts -nonewline stderr [read $chan] } proc cmdtest {{id ""}} { set cinfo [cmdexec $id] set o [dict get $cinfo stdout] chan conf $o -buffering none -blocking 0 set e [dict get $cinfo stderr] chan conf $e -buffering none -blocking 0 chan event $o readable [list ::punk::winshell::handle_out $o] chan event $e readable [list ::punk::winshell::handle_err $e] return $cinfo } #test with twapi create_process proc cmdcreate {{id ""}} { variable autoshellid variable shellinfo package require twapi set pipebase {\\.\pipe\punkwinshell} if {$id eq ""} { incr autoshellid set shellid $autoshellid } else { set shellid $id } #Method 1) - using windows named pipes set pipename_stdin $pipebase$shellid-stdin set pipename_stdout $pipebase$shellid-stdout set pipename_stderr $pipebase$shellid-stderr #set h_stdin [twapi::namedpipe_server $pipename_stdin] ;#handle for child process redirection - child to read #set p_stdin [twapi::namedpipe_client $pipename_stdin] ;#this end for writing #set h_stdout [twapi::namedpipe_server $pipename_stdout] ;#handle for child process redirection - child to write #set p_stdout [twapi::namedpipe_client $pipename_stdout] ;#this end for reading #set h_stderr [twapi::namedpipe_server $pipename_stderr] ;#handle for child process redirection - child to write #set p_stderr [twapi::namedpipe_client $pipename_stderr] ;#this end for reading #test set p_stdout [twapi::namedpipe_server $pipename_stdout] ;#handle for child process redirection - child to write set p_stdin "" set p_stderr "" chan configure $p_stdout -blocking 0 #Method 2) - using tcl's 'chan pipe' which creates OS level channels #chan pipe returns rd wr channels in that order #lassign [chan pipe] h_stdin p_stdin #lassign [chan pipe] p_stdout h_stdout #lassign [chan pipe] p_stderr h_stderr #chan configure $p_stdout -blocking 0 #chan configure $p_stderr -blocking 0 #set cmd {C:\Users\sleek\scoop\apps\windows-terminal\current\WindowsTerminal.exe} ;#doesn't work? #set cmd "[auto_execok cmd.exe] /k" #set cmd "[auto_execok powershell] -nop" #set cmd "[auto_execok tclsh]" set cmd "[auto_execok tclsh90]" set flagdict [dict create\ -cmdline "$cmd"\ -newconsole 1\ -inherithandles 0\ -background blue\ -title "punk::winshell $shellid" ] #dict set flagdict -stdchannels [list $h_stdin $h_stdout $h_stderr] set program "" lassign [twapi::create_process $program {*}$flagdict] pid tid puts stdout "launched with pid:$pid tid:$tid" #set pid [exec cmd.exe /k >@$h_stdout 2>@$h_stderr <@$h_stdin &] dict set shellinfo $shellid id $shellid dict set shellinfo $shellid pid $pid dict set shellinfo $shellid type "create_process" dict set shellinfo $shellid stdin $p_stdin dict set shellinfo $shellid stdout $p_stdout dict set shellinfo $shellid stderr $p_stderr return [dict get $shellinfo $shellid] } proc cmdexit {shellid} { variable shellinfo set info [dict get $shellinfo $shellid] switch -- [dict get $info type] { "create_process" { set exitresult [twapi::end_process [dict get $info pid]] } "exec" { puts stderr "todo.." puts stderr "manually kill exec process [dict get $info pid]" set exitresult 0 } } return [dict create exitresult $exitresult] } proc cmdkill {shellid} { variable shellinfo set info [dict get $shellinfo $shellid] set pid [dict get $info pid] set killcmd [list taskkill /PID $pid] if {[catch { exec {*}$killcmd } errMsg]} { puts stderr "$killcmd returned an error:" puts stderr $errMsg #if {!$forcekill} { # puts stderr "(try 'kill -9 $pid' ??)" #} #review - *no running instance* works with windows taskkill - "*No such process*" works with kill -9 on FreeBSD and linux - other platforms? if {![string match "*no running instance*" $errMsg] && ![string match "*No such process*" $errMsg]} { lappend failed_kits [list reason "could not kill running process for shellid $shellid (using '$killcmd')"] continue } else { puts stderr "" } } else { puts stderr "$killcmd ran without error" incr count_killed } } proc shellinfo {} { variable shellinfo return $shellinfo } proc cmdinfo {{id ""}} { variable autoshellid variable shellinfo if {$id eq ""} { #last created set shellid $autoshellid } else { set shellid $id } set info [dict get $shellinfo $shellid] set pid [dict get $info pid] catch { set statusresult [tcl::process status $pid] dict set info status $statusresult } set cmdline [twapi::get_process_commandline $pid] dict set info cmdline $cmdline return [showdict $info] } #quick n dirty - status of last (or identified) winshell proc cmdstat {{id ""}} { variable autoshellid variable shellinfo if {$id eq ""} { #last created set shellid $autoshellid } else { set shellid $id } set pid [dict get $shellinfo $shellid pid] set statusresult "" catch { #not in 8.6? set statusresult [tcl::process status $pid] } return [dict create id $shellid status $statusresult] } #proc sample1 {p1 n args} { # #*** !doctools # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] # #[para]Description of sample1 # #[para] Arguments: # # [list_begin arguments] # # [arg_def tring p1] A description of string argument p1. # # [arg_def integer n] A description of integer argument n. # # [list_end] # return "ok" #} #*** !doctools #[list_end] [comment {--- end definitions namespace punk::winshell ---}] } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Secondary API namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::winshell::lib { tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase tcl::namespace::path [tcl::namespace::parent] #*** !doctools #[subsection {Namespace punk::winshell::lib}] #[para] Secondary functions that are part of the API #[list_begin definitions] #proc utility1 {p1 args} { # #*** !doctools # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] # #[para]Description of utility1 # return 1 #} #*** !doctools #[list_end] [comment {--- end definitions namespace punk::winshell::lib ---}] } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[section Internal] #tcl::namespace::eval punk::winshell::system { #*** !doctools #[subsection {Namespace punk::winshell::system}] #[para] Internal functions that are not part of the API #} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Ready package provide punk::winshell [tcl::namespace::eval punk::winshell { variable pkg punk::winshell variable version set version 999999.0a1.0 }] return #*** !doctools #[manpage_end]