trent
trent

Reputation: 27905

Can I use Tkinter to create a Tcl interactive shell?

I am currently using ctypes in Python 3 to make a Tcl application that wraps a library written in Python.

from ctypes import *
import sys

tcl = cdll.LoadLibrary("libtcl.so")

TCL_OK = 0
TCL_ERROR = 1

def say_hello(cdata: c_void_p, interp: c_void_p, argc: c_int, argv: POINTER(c_char_p)) -> int:
    "This function wraps some functionality that is written in Python"
    print("hello, world")
    return TCL_OK

# useful type definitions
Tcl_AppInitProc = CFUNCTYPE(c_int, c_void_p)
Tcl_CmdProc = CFUNCTYPE(c_int, c_void_p, c_void_p, c_int, POINTER(c_char_p))
Tcl_CmdDeleteProc = CFUNCTYPE(None, c_int)

def app_init(interp: c_void_p) -> int:
    # initialize the interpreter
    tcl.Tcl_Init.restype = c_int
    tcl.Tcl_Init.argtypes = [c_void_p]
    if tcl.Tcl_Init(interp) == TCL_ERROR:
        return TCL_ERROR

    # create custom commands
    tcl.Tcl_CreateCommand.restype = c_void_p
    tcl.Tcl_CreateCommand.argtypes = [c_void_p, c_char_p, Tcl_CmdProc, c_int, Tcl_CmdDeleteProc]
    tcl.Tcl_CreateCommand(interp, b"say_hello", Tcl_CmdProc(say_hello), 0, Tcl_CmdDeleteProc(0))

    return TCL_OK

if __name__ == "__main__":
    # initialize argv
    Argv = c_char_p * (1 + len(sys.argv))
    argv = Argv(*(bytes(arg, "utf-8") for arg in sys.argv), 0)

    # summon the chief interpreter
    tcl.Tcl_Main.argtypes = [c_int, POINTER(c_char_p), Tcl_AppInitProc]
    tcl.Tcl_Main(len(sys.argv), argv, Tcl_AppInitProc(app_init))

From the command line, this works like a Tcl interpreter with extra commands, which is just what I want. It parses sys.argv and works both as an interactive shell and to run Tcl scripts.

bash$ python3 hello.py
% say_hello
hello, world
% ls 
foo.tcl  hello.py
% exit
bash$ cat foo.tcl
say_hello
bash$ python3 hello.py foo.tcl
hello, world
bash$

However, I know that Python comes with a Tcl interpreter already in the tkinter module. I'd like to use that, instead, because it already has a nice Python-wrapped API and could save me some futzing about with ctypes.

I can create an interpreter and add commands easily enough.

from tkinter import *

def say_hello():
    print("hello, world")

if __name__ == "__main__":
    tcl = Tcl()
    tcl.createcommand("say_hello", say_hello)
    tcl.eval("say_hello")

But I can't find any way to call Tcl_Init or Tcl_Main, and without those I can't run it interactively. While I don't care that much about the command line parser, it would be a lot of work to try to replicate the Tcl interactive shell in Python with all its features, like running external programs as if they were Tcl commands (such as ls in the above example). If that's my only option, I'll just stick with using ctypes.

Is there any way, even a hacky or unsupported one, to run the Tcl interpreter that comes with Tkinter as an interactive shell?

Upvotes: 4

Views: 525

Answers (1)

Donal Fellows
Donal Fellows

Reputation: 137567

The REPL implemented in Tcl_Main() is really very simple minded; you can do a (slightly cut down) version of it in a few lines of Tcl:

set cmd ""
set prompt "% "
while true {
    # Prompt for input
    puts -nonewline $prompt
    flush stdout
    if {[gets stdin line] < 0} break

    # Handle multiline commands
    append cmd $line "\n"
    if {![info complete $cmd]} {
        set prompt ""
        continue
    }
    set prompt "% "

    # Evaluate the command and print error/non-empty results
    if {[catch $cmd msg]} {
        puts stderr "Error: $msg"
    } elseif {$msg ne ""} {
        puts $msg
    }
    set cmd ""
}

All you need to do is run that inside the Tcl interpreter inside the Python code. You could also reimplement most of the REPL in Python; the only pieces you really need Tcl for then will be info complete $cmd (testing if a complete command is in the input buffer) and catch $cmd msg (evaluating the input buffer in the Tcl interpreter and trapping results & errors).

Upvotes: 4

Related Questions