Writing an interactive tcl/tk interpreter proxy to wish in python

Maybe, you want to experiment small stuffs in wish (the tcl/tk) interpreter because of a post claiming that direct python tcl/tk is simpler in some simple cases than tkinter.

As a convinced tkinter/FreeSimpleGUI user, I see this as an extreme claim that requires solid evidences.

When all is said and done, wish interpreter is not interactive, and for testing simple stuff it can get annoying very fast. Thus, it would be nice to add readline to the interface.

So here is a less than 100 line of code exercice of soing exactly so while having fun with : readline and multiprocessing (I would have taken multithreading if threads were easy to terminate).

Readline I quote
The readline module defines a number of functions to facilitate completion and reading/writing of history files from the Python interpreter.
Basically, it adds arrow navigation in history, back search with Ctrl+R, Ctrl+K for cuting on the right, Ctrl+Y for yanking ... all the facilities of interaction you have in bash or ipython for instance.

We are gonna use multiprocessing because tcl/tl is event oriented, hence, asynchronuous hence, we may have string coming from the tcl stdout while we do nothing and we would like to print them.

We also introduce like in ipython some magic prefixed with # (comment in tcl) like #? for the help. A session should look like this :
# pack [ button .c -text that -command { puts "hello" } ]
# 
tcl output> hello # here we pressed the button "that"
tcl output> hello # here we pressed the button "that"


# set name 32
# puts $name

tcl output> 32

# #?

#l print current recorded session
#? print current help
#! calls python code like
  #!save(name="temp") which saves the current session in current dir in "temp" file
bye exit quit quit the current session

# #l
pack [ button .c -text that -command { puts "hello" } ]
set name 32
puts $name

# #!save("my_test.tcl")
# quit
The code in itself is fairly easy to read the only catch is that wish accepts multiline input. I can't because I don't know how to parse tcl. As a result I « eval in tcl » every line to know if there is an error and ask politely tcl to do the job of signaling the error with a « catch/error » (the equivalent of python try + raise an exception).
#!/usr/bin/env python3
# -*- coding: utf8 -*-

from subprocess import Popen, PIPE, STDOUT
from multiprocessing import Process
import sys, os
import atexit
import os
import readline
from select import select
from time import sleep

### interactive session with history with readline
histfile = os.path.join(os.path.expanduser("~"), ".wish_history")
try:
    readline.read_history_file(histfile)
    # default history len is -1 (infinite), which may grow unruly
    readline.set_history_length(-1)
except FileNotFoundError:
    pass

### saving history at the end of the session
atexit.register(readline.write_history_file, histfile)

### opening wish
wish = Popen(['wish'], 
        stdin=PIPE,
        stdout=PIPE,
        stderr=PIPE,
        bufsize=-1,
        )

os.set_blocking(wish.stdout.fileno(), False)
os.set_blocking(wish.stderr.fileno(), False)
os.set_blocking(wish.stdin.fileno(), False)

def puts(s):
    out = f"""set code [ catch {{ {s} }} p ]
if {{$code}} {{ error $p }}
"""
    select([], [wish.stdin], [])
    wish.stdin.write(out.encode())


def gets():
    while True:
        wish.stdout.flush()
        tin = wish.stdout.read()
        if tin:
            print("\ntcl output> " + tin.decode())
        sleep(.1)

def save(fn="temp"):
    with open(fn,"wt") as f:
        f.write(session)

session=s=""
def load(fn="temp"):
    global session
    with open(fn, "rt") as f:
        while l:= f.readline():
            session+=l + "\n"
            puts(l)


# async io in tcl requires a background process to read the input
t =Process(target=gets, arwish=())
t.start()

while True:
    s = input("# ")
    if s in { "bye", "quit", "exit" }:
        t.terminate()
        wish.stdin.write("destroy .".encode())
        break
    elif s == "#l":
        print(session)
    elif s == "#?":
        print("""
#l print current recorded session
#? print current help
#! calls python code like
  #!save(name="temp") which saves the current session in current dir in "temp" file
  #!load(name="temp") which load the session stored in current dir in "temp" file
bye exit quit quit the current session
""" )
        continue
    elif s.startswith("#!"):
        print(eval(s[2:]))
        continue
    else:
        puts(s)
        if err:=wish.stderr.readline():
            sys.stderr.write(err.decode())
        else:
            if s and not s.startswith("#"):
                session += s + "\n"

This code is available on pypi as iwish (interactive wish) and the git link is in the README.

No comments: