mini debugging console

A mini debugging console

ET Here is an enhanced console for windows, to be used mostly with tclkits. It uses the existing console but adds some buttons, some simple debugging commands and a widget tree inspection tool.

Normally, if you have a tclkit.exe program, you might use it on windows to run a script using a command line as such,

> tclkit.exe aprogram.tcl ?args...?

It's likely you'll need to supply the path, but for simplicity, I'll leave that out here. With a windows command console, you can drag/drop files into the command window and it will place the full paths to your files in the console window, though you might need to quote them.

You can also create shortcuts in the same way to run a tcl script.

To use this enhanced console, one would do this instead, just slipping in myconsole.tcl between the program and the tcl source file, as so:

> tclkit.exe myconsole.tcl aprogram.tcl ?args...?

This lets the myconsole.tcl program run first and it will do the following

  1. Add several buttons and a menu item to the console window (but not open it)
  2. Add several debugging 2 letter procs (all lx for several x, with lh for help)
  3. Add a colorized puts command: cputs
  4. Add a widget tree analysis tool in the extra menu (requires bwidgets)
  5. Adjusts the argc, argv, and argv0 global variables to remove itself
  6. Binds the control-right-click (in console) to a copy/paste of some selected text
  7. Source the next argument (the aprogram.tcl)

lh - list help

The lh command will output to the console the following small help. The word aproc was used here just so my syntax editor won't confuse it as a proc.

aproc {ls} {{namepat *}}  # list directory
aproc {lt} {file}         # list text of file
aproc {lp} {{namepat *}}  # list procedure(s)
aproc {lw} {widget}       # list a widget
aproc {la} {array_name {pattern *} {rev 0} {xp 0}} # list an array
aproc {lg} {pat {delimeter /} {max 50}}            # list globals
aproc {ll} {name {max 50} {sort 0}}                # list lists

These commands are all optional and easily removed or changed if desired. However, the lw command is used also by the widget tree procedures used to display the configure attributes to the console.

Widget Tree tool

In order for this to work, the bwidgets package must be available, either built in to the tclkit or the auto_path global variable must have an element pointing to the lib directory of bwidgets. For example,

lappend auto_path {C:\tcl\kits\lib}

This is invoked using the Extra2 menu added to the right of the Help menu.

The widget tree proc has 2 buttons to open or close the bwidget tree fully. It has the usual + and - for opening and closing individual elements. A left click on any widget in the tree will produce a 2 column output of the configure attributes of the widget in the console window and also the packing information if pack is used (does not support place or grid however).

It also places the full widget path into the clipboard so it can be pasted into the console for debugging uses.

cputs

The cputs command works like a puts command except that the first argument would be a color argument. There are several defined in the cputs proc itself, which tests for a first time use and defines them one time. It's easy to add more, just look at some of the example ones. The tree display widget uses one.

It's called, as

cputs <color> <string to output>

Unlike the normal puts, cputs does not output a newline. Here are a few examples,

cputs green "text green on black " ; cputs yellowonblack "and yellow on black\n" 

This lets you have as many color combinations on a single line as you wish. Some background colors will fill the entire line. You can mix these with regular puts commands.

myconsole.tcl

The console will also include a column of buttons on the left side. Several are just a button version of the menu commands, such as clear, and the font increase and decrease commands, plus exit. The bottom command will move to the bottom after scrolling. The next 2 are -x and +x which just quickly increase and decrease the width of the console. There's one last item, a checkbox which will disable scrolling of the console window.

In the below code, you may want to modify the console size with the line wm geom . or from your own script, using the console eval command, like so,

console eval {wm geom . 80x24+0+0}

Here's the myconsole.tcl code:

namespace eval myconsole {

}
proc myconsole::myapp-shift {} {
    #puts "argc=$::argc  argv = $::argv"
    #puts "::argv0= |$::argv0| "
    set ::argv0 [lindex $::argv 1]
    set ::argv [lreplace $::argv 1 1]
    incr ::argc -1
    #puts "argc=$::argc  argv = $::argv"
    #update
}

proc lh {} { ;# list help
puts {
aproc {ls} {{namepat *}}  # list directory
aproc {lt} {file}         # list text of file
aproc {lp} {{namepat *}}  # list procedure(s)
aproc {lw} {widget}       # list a widget
aproc {la} {array_name {pattern *} {rev 0} {xp 0}} # list an array
aproc {lg} {pat {delimeter /} {max 50}}            # list globals
aproc {ll} {name {max 50} {sort 0}}                # list lists
}
}
    

proc {ls} {{namepat *}} { # list directory
    set files [glob $namepat]
    #puts $files
    for {set n 0} {$n < 2} {incr n} {        
        foreach file [lsort $files] {
            if {[file isdirectory $file] && $n == 0} {
                set d directory
                set s ""
            } elseif {![file isdirectory $file] && $n == 1}  {
                set d file
                set s [file size $file]
            } else {
                continue
            }
            puts "[format {%10s %12s %s} $d $s $file]"   ;#with each file in the list
        }
    }
}
proc {lt} {file} {                  # list text of file
    set io [open $file]
    fcopy $io stdout
    close $io
}
    

proc {lp} {{namepat *}} { # list procedure(s)
    foreach proc [info procs $namepat] {
        set space ""
        puts -nonewline "---------------------\nproc $proc {"
            foreach arg [info args $proc] {
                if [info default $proc $arg value] {
                    puts -nonewline "$space{$arg $value}"
                } else {
                    puts -nonewline $space$arg
                }
                set space " "
            }
            # No newline needed because info body may return a
            # value that starts with a newline
            puts -nonewline "} {"
            puts -nonewline  [info body $proc]
        puts "}"
    }
}

proc {lw} {widget} {          # list a widget
    set w [$widget configure]
    foreach item $w {
        set opt [lindex $item 0]
        set val "---"
        catch {set val [$widget cget $opt]}
        set wid($opt) $val 
    }        
    #la wid
    set names [lsort -dictionary [array names wid]]
    set n [llength $names]
    set n2 [expr ( $n/2 )]
    set odd [expr ( $n % 2 )]
    #puts "odd=$odd n=$n n2=$n2"
    if { $odd } {
        incr n2  ;# so this one is 1 more than half
    }
    #puts "Odd=$odd n=$n n2=$n2"
    if { $odd } {
        for {set m 0;set m2 [expr ( $m+$n2 )]} {$m < $n2} {incr m;incr m2} {
            
            if { $m == $n2-1 } {
                #puts "m = $m"        
                set left [lindex $names $m]
                #puts "l $left      $wid($left)"
                set leftt [format {%-20s %-20s} $left $wid($left)]
                puts "$leftt|"
            
            } else {
                #puts "m = $m,   m2 = $m2"        
                set left [lindex $names $m]
                set right [lindex $names $m2]
                #puts "l $left      $wid($left)"
                #puts "r $right      $wid($right)"
                set leftt [format {%-20s %-20s} $left $wid($left)]
                set rightt [format {%-20s %-20s} $right $wid($right)]
                puts "$leftt|[string trimright $rightt]"
            }
        }

    } else {
        for {set m 0;set m2 [expr ( $m+$n2 )]} {$m < $n2} {incr m;incr m2} {
            #puts "m = $m,   m2 = $m2"        
                set left [lindex $names $m]
                set right [lindex $names $m2]
                #puts "l $left      $wid($left)"
                #puts "r $right      $wid($right)"
                set leftt [format {%-20s %-20s} $left $wid($left)]
                set rightt [format {%-20s %-20s} $right $wid($right)]
                puts "$leftt|[string trimright $rightt]"
        }
        
    }
}

proc {la} {array_name {pattern *} {rev 0} {xp 0}} { # list an array
upvar 1 $array_name array
    if {![array exists array]} {
        error "\"$array_name\" isn't an array"
    }
    set maxl 0
    foreach name [lsort [array names array $pattern]] {
        if {[string length $name] > $maxl} {
            set maxl [string length $name]
        }
    }
    set maxl [expr {$maxl + [string length $array_name] + 2}]
    if {$rev == 0} {
        foreach name [lsort -dictionary [array names array $pattern]] {
            set nameString [format %s(%s) $array_name $name]
            if {$xp} {
                        xputs [format "%-*s = %s" $maxl $nameString $array($name)]
            } else {
                        puts stdout [format "%-*s = %s" $maxl $nameString $array($name)]
            }
        }
    } else {
        foreach name [lsort [array names array]] {
            #puts stdout "compare pattern $pattern with $name - $array($name)"
            if {[string match "*$pattern*" $array($name)]} {
                set nameString [format %s(%s) $array_name $name]
                if {$xp} {
                    xputs [format "%-*s = %s" $maxl $nameString $array($name)]
                } else {
                    puts stdout [format "%-*s = %s" $maxl $nameString $array($name)]
                }
            }
        }

    }
}
###########################################################
## Procedure:  lg

proc {lg} {{pat *} {delimeter /} {max 50}} {                        # list globals
    set a [lsort -dictionary [info global ${pat}*]]
    foreach _the_var_ $a {
        #global $_the_var_
        set that ""
        set this "set that \[array names ::$_the_var_\]"
        #puts "this1=$this"
        eval $this
        #puts "that=$that"
        if {$that != ""} {
            set this "set val  \[lsort -dictionary \[array names ::$_the_var_\]\]"
            #puts "---$this"
        } else {
            set this "set val $delimeter\$::$_the_var_$delimeter"
        }
        #puts "this2=$this"
        catch {
            eval $this
            set foo [format "--- %-20s = %s" $_the_var_ [string range $val 0 $max]]
            puts $foo
            #puts "$_the_var_ \t= $val"
        }
    }
}
###########################################################
## Procedure:  ll

proc {ll} {name {max 50} {sort 0}} {                                # list lists
    if { [info exist $name] } {
        upvar $name v
    } else {
        set v $name
    }
    if { $sort } {
        foreach {i}  [lsort -dictionary $v]  { 
            puts [lrange $i 0 $max]
        }

    } else {
        foreach {i}   $v  {     
            puts [lrange $i 0 $max]
        }
    }
}

###########################################################
## Procedure:  myconsole::wtree


proc myconsole::wtree_node_puts {args} {
    puts ""
    puts ""
    cputs green "$args\n"
    lw $args
    catch {cputs green "[pack info $args]\n"}
    clipboard clear ; clipboard append $args
}
proc myconsole::wtree_node_openclose {which} {
    set nodes [.wtree_top.sw.t nodes root]
    #puts "which - $which - $nodes"
    if { $which == "open" } {
        foreach item $nodes {
            .wtree_top.sw.t opentree $item
        }
    
    } else {
        foreach item $nodes {
            .wtree_top.sw.t closetree $item
        }
        
    }
}

proc myconsole::wtree {{root .} {level 0}} {
    set top .wtree_top
    if {  $level == 0} {
        package require BWidget
        catch {
            $top.sw.t delete [$top.sw.t nodes root]
            destroy $top
        }
        toplevel $top
        frame $top.f
        button $top.f.b1 -text open -command {myconsole::wtree_node_openclose open}
        button $top.f.b2 -text close -command {myconsole::wtree_node_openclose close}
        pack $top.f  -side top -fill x
        pack $top.f.b1 $top.f.b2 -side left -expand yes -fill both
        ScrolledWindow $top.sw
        pack $top.sw -fill both -expand 1 -side top
        Tree $top.sw.t  -deltay 25 -deltax 25 -padx 5 -borderwidth 8 -linesfill orange -padx 5
        #pack $top.sw.t
        $top.sw setwidget $top.sw.t   ;# Make ScrolledWindow manage the Tree widget
        update                ;# Process all UI events before moving on.
        $top.sw.t bindText <1> +myconsole::wtree_node_puts
        set ::wtree_queued_inserts {}
        wm geom $top 466x326+52+52
        
    }
    set children [winfo children $root]
    set class [winfo class $root]
    set info ""
    if       { $class == "Button" } {
        set info [split [$root cget -text] \n]
        
    } elseif { $class == "TLabelframe" } {
        set info [split [$root cget -text] \n]
        
    } elseif { $class == "TButton" } {
        set info [split [$root cget -text] \n]
        
    } elseif { $class == "TEntry" } {
        set info "var: [$root cget -textvariable]"
    } elseif { $class == "TCheckbutton" } {
        set info [split [$root cget -text] \n]
    } elseif { $class == "TButton2" } {
        set info [split [$root cget -text] \n]
    } else {
        
    }
    
    #cputs normal "[string repeat " |  " $level]"
    #cputs green "$root"
    #cputs red  [split $root .]
    #cputs normal " - $class  $info\n"
    set root [regsub -all : $root _]
    set parts [split [string range $root 1 end] .]
    #puts "parts = $parts"
    if { $parts == "" } {
        set parent root
    } else {
        set parent {}
        foreach item [lrange $parts 0 end-1] {
            append parent .$item
        }
    }
    #puts "parent = /$parent/"
    if { $parent == "" } {
        set parent root
    }
    set cmd "$top.sw.t insert end \{$parent\}   \{$root\}       -font {courier 12} -text  \{$root - $class $info\}"
    #puts "cmd = $cmd"
    lappend ::wtree_queued_inserts $cmd
    
    
    
    if { $children == "" } {
        return $root
    } else {
        foreach child $children {
            set tout [myconsole::wtree $child [expr ( $level + 1 )]]
        }
        
    }
    if { $level == 0 } {
        #puts "\n\ndone here\n\n"
        foreach item $::wtree_queued_inserts {
            #puts "do - $item"
            eval $item
        }
    }
}


proc cputs {dest string} {
    if { ! [info exist ::tk::my_color_set] } {
        set ::tk::my_color_set 1
        console eval {
            .console tag configure green                         -foreground \#00ff00 -background black
            .console tag configure yellowonblack         -foreground yellow -background black -font {ariel 14 bold}
            .console tag configure yellow                         -foreground yellow
            .console tag configure whiteonred                 -foreground white -background red
            .console tag configure red                                 -foreground red
        }
    }
    console eval [list ::tk::ConsoleOutput $dest $string]
}


# proc script start
package require Tk
set ::argv [linsert $::argv 0 [info nameof]]
incr argc
if { $::tcl_platform(platform)    != "windows" } {
    #source [file join [info nameof] lib/app-tst/console.tclkit]
    #use the console code that allows one to use a console on linux etc.
} else {
    if [catch {
        console eval {
            
            if { ![info exist ::tk::do_scroll] } {
                
                # proc console stuff
                set vers [split [info patch] .]
                if { [string compare [info patch] "8.6"] >= 0 } {
                    set menuincr 1
                } else {
                    set menuincr 0
                }
                .console config -inactiveselectbackground SystemInactiveBorder -tabs {32 left} -tabstyle wordprocessor -width 30
                wm geom . 87x24+0+0
                # optional, if you want right click to do a paste
                # bind all <Button-3> {.menubar.edit invoke 2}
                bind all <Control-Button-3> {.menubar.edit invoke 1;.menubar.edit invoke 2}
                pack forget .console .sb .consoleframe
                
                pack [frame  .frame -bg black] -side left -fill y -ipady 10 -pady 10
                #pack [button .frame.eval -text Cev  -command {clipboard clear; clipboard append "console eval \{\}"; .menubar.edit invoke 2}] -side top -fill x
                pack [button .frame.clear -bg white -fg black -text Clear -command {.menubar.file invoke 2}] -side top -fill x
                pack [button .frame.smaller -bg white -text {font -} -command {.menubar.edit invoke [expr ( $menuincr+6 )];after 100 {.frame.repos invoke}}] -side top -fill x
                pack [button .frame.bigger  -bg white -text {font +} -command {.menubar.edit invoke [expr ( $menuincr+5 )];after 100 {.frame.repos invoke}}] -side top -fill x
                pack [button .frame.exit     -bg white -text Exit -command {exit}] -side top -fill x
                pack [button .frame.repos -bg white -text BottOM -command {.console see end; .console mark set insert end}] -side top -fill x
                pack [frame  .frame.frame -bg black] -side top -fill x
                pack [button .frame.frame.pm20 -bg gray -text -x -command {wm geom . [expr [lindex [split [wm geom .] x] 0]-10]x[lindex [split [wm geom .] x] 1]}] -side left -fill x -expand 1
                pack [button .frame.frame.pp20 -bg gray -text +x -command {wm geom . [expr [lindex [split [wm geom .] x] 0]+10]x[lindex [split [wm geom .] x] 1]}] -side left -fill x -expand 1
                
                .frame.bigger invoke;.frame.bigger invoke;.frame.bigger invoke;.frame.bigger invoke
                
                pack [checkbutton .frame.scroll -bg white -fg black  -text scroll -variable ::tk::do_scroll] -side top -fill x
                pack .consoleframe -in . -anchor center -expand 1 -fill both -ipadx 0 -ipady 0 -padx 0 -pady 0 -side left
                pack .console -in .consoleframe -anchor center -expand 1 -fill both -ipadx 0 -ipady 0 -padx 1 -pady 1 -side left
                pack .sb -in .consoleframe -anchor center -expand 0 -fill both -ipadx 0 -ipady 0 -padx 1 -pady 1 -side right
                set ::tk::console::maxLines 10000
                .menubar add casc -label Extra2 -menu [menu .menubar.extra -tearoff 0]
                proc menu+ {head label cmd} {
                    set cmd2 [list consoleinterp eval $cmd]
                    .menubar.$head add command -label $label -command $cmd2
                }
                menu+ extra {Widget Tree} {myconsole::wtree}
                proc ::tk::ConsoleOutput {dest string} {
                    set w .console
                    $w insert output $string $dest
                    ::tk::console::ConstrainBuffer $w $::tk::console::maxLines
                    if {$::tk::do_scroll} {$w see insert}
                }
                set ::tk::do_scroll 1
            }
        }
    } errstr ] {
        puts stderr "error = $errstr"
    }
}

if { $::argc > 1 } {
    set myconsole::theprog [lindex $argv 1]
    myconsole::myapp-shift
    incr ::argc -1
    set ::argv [lrange $::argv 1 end]
    source $myconsole::theprog
    #unset myconsole::theprog
}
    

enter key = repeat last

Just paste this into a windows console and it will replace one internal proc. Runs in 8.6.9 9/8/2019. This is useful if you want to lean on the enter key and very rapidly repeat the last command.

console eval {
    namespace eval tk { ; # replace this so we can capture a null command and repeat the last one
        proc ::tk::ConsoleInvoke {args} {
            set ranges [.console tag ranges input]
            set cmd ""
            if {[llength $ranges]} {
                set pos 0
                while {[lindex $ranges $pos] ne ""} {
                    set start [lindex $ranges $pos]
                    set end [lindex $ranges [incr pos]]
                    append cmd [.console get $start $end]
                    incr pos
                }
            }
            if {$cmd eq ""} {
                ConsolePrompt
            } elseif {[info complete $cmd]} {
                if { $cmd == "\n" } { #patch starts here, just this one if block
                    set cmd_next [consoleinterp eval {history nextid}]
                    set cmd_event [consoleinterp eval "history event [expr {( $cmd_next - 1 )}]"]
                    if { $cmd_event != "" } {
                        set cmd $cmd_event
                        consoleinterp eval {namespace eval ::tcl {incr history(nextid) -1;incr history(oldest) -1}}  ;# don't store this one again in history
                    }
                }
                .console mark set output end
                .console tag delete input
                set result [consoleinterp record $cmd]
                if {$result ne ""} {
                    puts $result
                }
                ConsoleHistory reset
                ConsolePrompt
            } else {
                ConsolePrompt partial
            }
            .console yview -pickplace insert
        }
        
    }
}