Bidirectionnal python/tk by talking to tk interpreter back and forth

Last time I exposed an old way learned in physical labs to do C or python/tk like in the old days: by summoning a tcl/tk interpreter and piping commands to it.

But what fun is it?

It's funnier if the tcl/tk interperpreter talks back to python :D as an hommage to the 25 years awaited TK9 versions that solves a lot of unicode trouble.

Beforehand, to make sense to the code a little warning is required : this code targets only POSIX environment and loses portability because I chose to use a way that is not the « one best way » for enabling bidirectionnal talks. By using os.set_blocking(p.stdout.fileno(), False) we can have portable non blocking IO, which means this trick has been tested on linux, freeBSD and windows successfully. It's pretty much advised when using non blocking IO to use select.select to check if file descriptors are ready to use.

First and foremost, the Popen now use p.stdout=PIPE enabling the channel on which tcl will talk. As a joke puts/gets are named from tcl/tk functions and are used in python to push/get strings from tcl.

Instead of using multithreading having one thread listen to the output and putting the events in a local queue that the main thread will consume I chose the funniest technique of setting tcl/tk output non blocking which does not work on windows. This is the fnctl part of the code.

Then, I chose not to parse the output of tcl/tk but exec it, making tcl/tk actually push python commands back to python. That's the exec part of the code.

For this I needed an excuse : so I added buttons to change minutes/hours back and forth.

That's the moment we all are gonna agree that tcl/tk that tcl/tk biggest sin is its default look. Don't worry, next part is about using themes.

Compared to the first post, changes are minimal :D This is how it should look :
And here is the code, largely still below 100 sloc (by 3 lines).
#!/usr/bin/env python
from subprocess import Popen, PIPE
from time import sleep, time, localtime
import select
# import fcntl
import os

# let's talk to tk/tcl directly through p.stdin
p = Popen(['wish'], stdin=PIPE, stdout=PIPE)

# best non portable answer on stackoverflow
#fd = p.stdout.fileno()
#flag = fcntl.fcntl(fd, fcntl.F_GETFL)
#fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)
# ^-- this 3 lines can be replaced with this one liner --v
# portable non blocking IO
os.set_blocking(p.stdout.fileno(), False)

def puts(s):
    for l in s.split("\n"):
        select.select([], [p.stdin], [])
        p.stdin.write((l + "\n").encode())
        p.stdin.flush()

def gets():
    ret=p.stdout.read()
    p.stdout.flush()
    return ret

WIDTH=HEIGHT=400

puts(f"""
canvas .c -width {WIDTH} -height {HEIGHT} -bg white
pack .c
. configure -background white

ttk::button  .ba -command {{  puts ch-=1 }} -text <<
pack .ba -side left   -anchor w
ttk::button .bb -command {{  puts cm-=1 }} -text  <
pack .bb -side left -anchor w
ttk::button .bc -command {{  puts ch+=1 }} -text >> 
pack .bc  -side right -anchor e
ttk::button .bd -command {{  puts cm+=1 }} -text > 
pack .bd  -side right -anchor e
""")

# Constant are CAPitalized in python by convention
from cmath import  pi as PI, e as E
ORIG=complex(WIDTH/2, HEIGHT/2)

# correcting python notations j => I  
I = complex("j")
rad_per_sec = 2.0 * PI /60.0
rad_per_min = rad_per_sec / 60
rad_per_hour = rad_per_min / 12

origin_vector_hand = WIDTH/2 *  I

size_of_sec_hand = .9
size_of_min_hand = .8
size_of_hour_hand = .65

rot_sec = lambda sec : -E ** (I * sec * rad_per_sec )
rot_min = lambda min : -E ** (I *  min * rad_per_min )
rot_hour = lambda hour : -E ** (I * hour * rad_per_hour )

to_real = lambda c1,c2 : "%f %f %f %f" % (c1.real,c1.imag,c2.real, c2.imag)
for n in range(60):
    direction= origin_vector_hand * rot_sec(n)
    start=.9 if n%5 else .85
    puts(f".c create line {to_real(ORIG+start*direction,ORIG+.95*direction)}")
    sleep(.01)

diff_offset_in_sec = (time() % (24*3600)) - \
    localtime()[3]*3600 -localtime()[4] * 60.0 \
    - localtime()[5] 
ch=cm=0
while True:
    # eventually parsing tcl output 
    back = gets()
    # trying is more concise than checking
    try:
        back = back.decode()
        exec(back)
    except Exception as e:
        pass

    t = time()
    s= t%60
    m = m_in_sec = t%(60 * 60) + cm * 60
    h = h_in_sec = (t- diff_offset_in_sec)%(24*60*60) + ch * 3600 + cm * 60
    puts(".c delete second")
    puts(".c delete minute")
    puts(".c delete hour")
    c0=ORIG+ -.1 * origin_vector_hand * rot_sec(s)
    c1=ORIG+ size_of_sec_hand * origin_vector_hand * rot_sec(s)
    puts( f".c create line {to_real(c0,c1)} -tag second -fill blue -smooth true")
    c1=ORIG+size_of_min_hand * origin_vector_hand * rot_min(m)
    puts(f".c create line {to_real(ORIG, c1)} -tag minute -fill green -smooth true")
    c1=ORIG+size_of_hour_hand * origin_vector_hand * rot_hour(h)
    puts(f".c create line {to_real(ORIG,c1)} -tag hour -fill red -smooth true")
    puts("flush stdout")
    sleep(.1)


Some history about this code.



I have been mentored in a physical lab where we where doing the pipe, fork, dup2 dance to tcl/tk from C to give a nice output to our simulations so we could control our instuition was right and could extract pictures for the publications. This is a trick that is almost as new as my arteries.
My mentor used to say : we are not coders, we need stuff to work fast and neither get drowned in computer complexity or endless quest for « the one best way » nor being drowned in bugs, we aim for the Keep It Simple Stupid Ways.

Hence, this is a Keep It Simple Stupid approach that I revived for the sake of seeing if it was still robust after 35 years without using it.

Well, if it's robust and it's working: it ain't stupid even if it isn't the « one best idiomatic way ». :P

No comments: