Tkcon as an IDE shell

aspect - I'm not a big fan of IDEs. Give me Vim and a shell and a REPL, such as Tkcon. To make Tkcon a better IDE I've developed some tools to make working between Tkcon and Vim much nicer. Hopefully others can use and improve these, and contribute their own inventions.


LV 2008 July 16 - what does REPL stand for? AM Read-Evaluate-Print Loop - the basic mode of working for a shell.


This toolkit allows you to start gvim from tkcon, with Tcl variables, arrays and procs, just by typing vi procname. When you save the file in gvim, it is evaluated in the running Tkcon. Suggested for use with the MiniBufExplorer vim plugin.

First, a few dependencies:


  • Namespace FTPD: Tcllib's ftpd package set up so that you can retrieve procs, arrays and variables from a running Tcl. It makes use of Tkcon's dump proc, which is really useful but also easily duplicated (or copied from Tkcon sources).

WARNING: this performs no authentication so it's really dangerous on any networked system. Any file uploaded to this FTP server will be executed as Tcl code in the running interp!

Call this file nsftpd.tcl.

  # TODO: evaluate uploaded code in the appropriate namespace
  # TODO: return ensembles for namespaces
  # TODO: user authentication
  
  package require ftpd
  package require schan
  package provide nsftpd 0.1
  
  namespace eval nsfscmd {
      proc append {path} {
          error "Not implemented: append "
      }
      proc delete {path channel} {
          error "Not implemented: delete "
      }
      proc dlist {path style channel} {
          set ns [string map {/ ""} $path]
  # puts "dlist $path $style $channel (::$ns)"
          set procs [namespace eval ::$ns {info procs}]
          set vars [namespace eval ::$ns {info vars}]
          set dirs [namespace eval ::$ns {namespace children}]
          foreach i [concat $dirs $vars $procs] {
              puts -nonewline $channel $i\r\n
          }
      }
      proc exists {path} {
          return 1
      }
      proc mtime {path channel} {
  # puts "mtime $path $channel"
          puts $channel "2008-05-10 21:50"
      }
      proc size {path channel} {
  # puts "size $path $channel"
          puts $channel "200 1337"
      }
      proc permissions {path} {
  # puts "perms $path"
          return 0750
      }
      proc rename {path newpath channel} {
          error "Not implemented: rename "
      }
      proc mkdir {path channel} {
          error "Not implemented: mkdir "
      }
      proc rmdir {path channel} {
          error "Not implemented: rmdir "
      }
      proc retr {path mode} {
  # puts "retr $path $mode"
  # puts "path $path"
          set ns [string trim [file dirname $path] /]
  # puts "ns $ns"
          set path [string trim $path /]
  # puts "path $path"
          set name [string range $path [expr 1+[string length $ns]] end]
  # puts "name $name"
          set ns [string map {/ ""} $ns]
  # puts "ns $ns"
          set fd [chan create [list read write] schan]
          fconfigure $fd -buffering none -translation {binary binary}
  # puts "dumping ${ns}::$name"
          puts $fd [dump ${ns}::$name]
          return $fd
      }
      proc receive {fd s} {
  # puts "receive $fd $s"
          uplevel #0 $s
      }
      proc store {path mode} {
  # puts "store $path $mode"
          set fd [chan create [list read write] schan]
          fconfigure $fd -buffering none -translation {binary binary}
          schan setccb $fd ::nsfscmd::receive
                  return $fd
      }
      namespace export *
      namespace ensemble create
  }
  
  
  proc true args { return 1 }
  
  proc start_nsftpd {port} {
      ::ftpd::config -authUsrCmd true -authFileCmd true -fsCmd nsfscmd
      set dir [uplevel 1 namespace current]
      set dir [string map {:: /::} $dir]
      set ::ftpd::port $port
      set ::ftpd::cwd $dir
      ::ftpd::server
  }

Finally, proc vim:

  # vim.tcl --
  #
  #   A little utility for Tkcon to start vim on files, variables, procs, ...
  
  package require nsftpd
  package provide vim 0.2
  
  namespace eval ::vim {
  
      proc edfile {fn} {
          exec gvim --servername TkCon --remote $fn &
      }
      
      proc edtclftp {fn} {
          exec gvim --servername TkCon --remote ftp://localhost:1337$fn &
      }
  
      proc ed {name} {
          set tclname [uplevel #0 "namespace which $name"]
          if {"" == $tclname} {
              set tclname [uplevel #0 "namespace which -var $name"]
          }
          puts "ed $name $tclname"
          if {"" != $tclname} {
              edtclftp [string map {:: /} $tclname]
          } else {
              edfile $name
          }
      }
  
      namespace export ed
  
      variable port 1337
  
      proc init {} {
          package require nsftpd
          variable port
          puts -nonewline "<*> Starting FTP server on port $port:"
          uplevel 1 start_nsftpd $port
          puts " done!"
      } 
  
  }
  
  interp alias {} vi {} ::vim::ed
  ::vim::init

jdb 2009/11/12 - I have never been able to get the above version to work completely. My gvim hangs after logging in. I can edit files using ftpd through gvim so it must be something in the nsftpd layer above.

Here is an alternate version, just a slightly modified vfs::ns to allow for writing. One key difference is that the gvim servername is tied to the tkcon pid and the ftpd port is dynamically generated so you can run this in multiple tkcon sessions and each uses it's own gvim session. Same warnings as above, no authentication and evals contents into interp.

  package require ftpd
  package require vfs::ns

  proc vfs::ns::open {ns name mode permissions} {
    ::vfs::log "open $name $mode $permissions"
    # return a list of two elements:
    # 1. first element is the Tcl channel name which has been opened
    # 2. second element (optional) is a command to evaluate when
    #    the channel is closed.
    switch -- $mode {
      "" -
      "r" {
        set nfd [vfs::memchan]
        fconfigure $nfd -translation binary
        puts -nonewline $nfd [_generate ::${ns}::${name}]
        fconfigure $nfd -translation auto
        seek $nfd 0
        return [list $nfd]
      }
      "w" {
        set nfd [vfs::memchan]
        fconfigure $nfd -translation auto
        return [list $nfd [list vfs::ns::do_close $nfd $ns $name]]
      }
      default {
        return -code error "illegal access mode \"$mode\""
      }
    }
  }

  proc vfs::ns::do_close { nfd ns name } {

    seek $nfd 0
    eval [read $nfd]
    close $nfd
  }

  proc vi { proc_name } {
    exec gvim --servername TkCon-[pid] --remote ftp://localhost:$::ftpd::port/$proc_name &
    return
  }

  vfs::ns::Mount :: /nsvfs

  proc do_log { args } { }
  proc true { args } { return 1 }

  ftpd::config -authUsrCmd true -authFileCmd true -logCmd do_log
  set ::ftpd::port 0
  set ::ftpd::cwd /nsvfs
  ftpd::server

Coupling Vim and TkCon

2014 Jul 02 Eugene Here's my approach to making some kind of makeshift IDE with TkCon and my text editor of choice - Vim (GVim on Windows to be more precise). I needed a simple, out-of-the-way option to run any Tcl script right from inside the editor, with results displayed in a separate window/console, and the ability to run arbitrary Tcl commands in that console. Vim/TkCon fit this niche nicely. My final solution allows me to run the contents of the current Vim buffer in a dedicated TkCon tab by pressing Ctrl-Enter inside Vim. The tab is "bound" to the buffer (or rather to the script file loaded into the buffer), so the script is always executed within it's own TkCon tab.

Setting up TkCon

TkCon in this case acts as a server listening on some TCP socket, waiting for commands to arrive from external application. Here's my ~/tkcon.cfg file:

# Socket server procedures.
proc server chan {
        if {[chan eof $chan]} {
                close $chan
                puts {SRV: remote connection closed}
        } else {
                set line [gets $chan]
                if {$line eq {}} return
                if {[regexp {^(\w+)\s+(.+)$} $line -> cmd args]} {
                        switch $cmd {
                                src {
                                        if {[file readable $args]} {
                                                lassign [list [file dirname $args] [file tail $args]] dir_name file_name
                                                set hash [::crc::crc32 -format %X $args]
                                                if {[info exists ::tkcon::TABS($hash)] && [winfo exists [set tab_name [lindex [split $::tkcon::TABS($hash) |] 0]]]} {
                                                        ::tkcon::GotoTab $tab_name
                                                } else {
                                                        ::tkcon::NewTab
                                                        set ::tkcon::TABS($hash) $::tkcon::PRIV(curtab)|$args
                                                        $::tkcon::PRIV(curtab) tag configure srvcmd -background #BBFFBB
                                                        $::tkcon::PRIV(statusbar).file_name configure -text $file_name
                                                }
                                                tkcon show
                                                tkcon console insert output "SOURCE $args\n" srvcmd
                                                cd $dir_name
                                                if {[catch {tkcon load $file_name} err]} {
                                                        puts stderr "SRV ERR: command {$line} failed with error:\n\t$err"
                                                }
                                        } else {
                                                puts stderr "SRV ERR: file '$args' is not readable."
                                        }
                                }
                        }
                } else {
                        puts stderr "SRV ERR: Unknown command '$line'"
                }
        }
        return
}
# Version display procedures.
proc getver {} {
        lassign [ list $::tcl_platform(os)\ $::tcl_platform(osVersion) unknown ] os arch
        if { [ array exists ::activestate::ActiveTcl ] } {
                lassign [ list $::activestate::ActiveTcl(product)\ $::activestate::ActiveTcl(release) $::activestate::ActiveTcl(arch) ] tcl arch
        } else {
                lassign [ list Tcl\ $::tcl_patchLevel $::tcl_platform(machine) ] tcl arch
        }
        return "$tcl on $os ($arch)"
}
proc addver status_bar {
        grid [label $status_bar.version -text [getver] -foreground darkmagenta -bd 1 -relief sunken -padx 10] -row 0 -column 2 -padx 1 -sticky news
        grid configure $status_bar.cursor -column 3
}
# Start our code.
after idle {
        set status_bar $::tkcon::PRIV(statusbar)
        addver $status_bar
        if {[catch {
                package require crc32
                socket -server {apply {{chan addr port} {chan event $chan readable [list server $chan]}}} 8007
        } err_msg]} {
                puts stderr "SRV ERR: couldn't open the server's socket.\n\t$err_msg"
        } else {
                set col 2
                grid [label $status_bar.file_name -text unknown -foreground darkgreen -bd 1 -relief sunken -padx 10] -row 0 -column $col -padx 1 -sticky news
                foreach widget {version cursor} {grid configure $status_bar.$widget -column [incr col]}
                trace add variable ::tkcon::PRIV(curtab) write [list apply {{status_bar n1 n2 op} {
                        upvar $n1 arr
                        set curr_tab $arr($n2)
                        if {[info exists ::tkcon::TABS]} {
                                set data [array get ::tkcon::TABS]
                                set text [expr {[set idx [lsearch -glob $data ${curr_tab}|*]] > -1 ? [file tail [lindex [split [lindex $data $idx] |] end]] : {unknown}}]
                                $status_bar.file_name configure -text $text
                        }
                }} $status_bar]
        }
        unset status_bar
}

Setting up Vim

We will need GVim compiled with Tcl support:

https://lh5.googleusercontent.com/-R4tTUztVmK4/U7QgX44JaGI/AAAAAAAAFzM/ZLJkVs9jn18/w757-h181-no/vim-version.PNG

Then the following lines need to be added to ~/.vimrc file:

" Tcl-related code.
if has("tcl")
        tclfile ~/.vimrc.tcl
        nnoremap <silent> <C-Enter> :tcl tkcon_src $::tkcon_ch<CR>
endif

Here's the contents of ~/.vimrc.tcl file:

proc tkcon_connect {} {
        set ch undefined
        if {[catch {set ch [socket localhost 8007]} err_msg]} {
                puts vimerr "Error: couldn't open a connection to TkCon instance, $err_msg"
        } else {
                chan configure $ch -buffering line -blocking no
        }
        return $ch
}

proc tkcon_src ch {
        if {[catch {puts $ch "src [$::vim::current(buffer) name]"} err_msg]} {
                puts vimerr "Error: couldn't send src command to TkCon, $err_msg"
        }
}

foreach sock [chan names sock*] {close $sock}
set ::tkcon_ch [tkcon_connect]
return

After everything is set and done, pressing Ctrl-Enter in Vim will send the currently loaded file name to TkCon through TCL socket, and TkCon will allocate a new tab for that (or switch to the corresponding tab if it already exists) and source the file there. Here's a couple of screenshots to show what it looks like in real life:

Editing some Tcl script in GVim

https://lh6.googleusercontent.com/-YEW1gE7A7L8/U7QfDVd_OFI/AAAAAAAAFy0/6Wn9s0563jM/w723-h275-no/vim.PNG

Having it sourced by TkCon after pressing Ctrl-Enter in GVim

https://lh4.googleusercontent.com/-7NNv4Scr2bc/U7QfDdFBo-I/AAAAAAAAFy4/9iPsi-WM0_Q/w684-h268-no/tkcon.PNG