How to make tkinter text box a stdin input receiver? How to make tkinter text box a stdin input receiver? tkinter tkinter

How to make tkinter text box a stdin input receiver?


Ok so after researching on the web, in docs, and inside the code of the queue, idlelib and subprocess modules, I figured out the simplest way to make tkinter Textbox interact with python console as stdin, stdout, and stderr receiver. Here's the code:

import tkinter as tkimport subprocessimport queueimport osfrom threading import Threadclass Console(tk.Frame):    def __init__(self, parent=None, **kwargs):        tk.Frame.__init__(self, parent, **kwargs)        self.parent = parent        # create widgets        self.ttytext = tk.Text(self, wrap=tk.WORD)        self.ttytext.pack(fill=tk.BOTH, expand=True)        self.ttytext.linenumbers.pack_forget()        self.p = subprocess.Popen(["jupyter", "qtconsole"], stdout=subprocess.PIPE, stdin=subprocess.PIPE,                                  stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)        # make queues for keeping stdout and stderr whilst it is transferred between threads        self.outQueue = queue.Queue()        self.errQueue = queue.Queue()        # keep track of where any line that is submitted starts        self.line_start = 0        # a daemon to keep track of the threads so they can stop running        self.alive = True                # start the functions that get stdout and stderr in separate threads        Thread(target=self.readfromproccessout).start()        Thread(target=self.readfromproccesserr).start()        # start the write loop in the main thread        self.writeloop()        # key bindings for events        self.ttytext.bind("<Return>", self.enter)        self.ttytext.bind('<BackSpace>', self.on_bkspace)        self.ttytext.bind('<Delete>', self.on_delete)        self.ttytext.bind('<<Copy>>', self.on_copy)        self.ttytext.bind('<<Paste>>', self.on_paste)        self.ttytext.bind('<Control-c>', self.on_copy)        self.ttytext.bind('<Control-v>', self.on_paste)    def destroy(self):        """This is the function that is automatically called when the widget is destroyed."""        self.alive = False        # write exit() to the console in order to stop it running        self.p.stdin.write("exit()\n".encode())        self.p.stdin.flush()        # call the destroy methods to properly destroy widgets        self.ttytext.destroy()        tk.Frame.destroy(self)            def enter(self, event):        """The <Return> key press handler"""        cur_ind = str(self.ttytext.index(tk.INSERT))        if int(cur_ind.split('.')[0]) < int(self.ttytext.search(': ', tk.END, backwards=True).split('.')[0]):            try:                selected = self.ttytext.get('sel.first', 'sel.last')                if len(selected) > 0:                    self.ttytext.insert(tk.END, selected)                    self.ttytext.mark_set(tk.INSERT, tk.END)                    self.ttytext.see(tk.INSERT)                    return 'break'            except:                selected = self.ttytext.get(                    self.ttytext.search(': ', tk.INSERT, backwards=True), tk.INSERT)                self.ttytext.insert(tk.END, selected.strip(': '))                self.ttytext.mark_set(tk.INSERT, tk.END)                self.ttytext.see(tk.INSERT)            return 'break'        string = self.ttytext.get(1.0, tk.END)[self.line_start:]        self.line_start += len(string)        self.p.stdin.write(string.encode())        self.p.stdin.flush()    def on_bkspace(self, event):        pass    def on_delete(self, event):        pass    def on_key(self, event):        """The typing control (<KeyRelease>) handler"""        cur_ind = str(self.ttytext.index(tk.INSERT))        try:            if int(cur_ind.split('.')[0]) < int(self.ttytext.search(r'In [0-9]?', tk.END, backwards=True).split('.')[0]):                return 'break'        except:            return    def on_copy(self, event):        """<Copy> event handler"""        self.ttytext.clipboard_append(self.ttytext.get('sel.first', 'sel.last'))        # I created this function because I was going to make a custom textbox    def on_paste(self, event):        """<Paste> event handler"""        self.ttytext.insert(tk.INSERT, self.ttytext.clipboard_get())        # I created this function because I was going to make a custom textbox    def readfromproccessout(self):        """To be executed in a separate thread to make read non-blocking"""        while self.alive:            data = self.p.stdout.raw.read(1024).decode()            self.outQueue.put(data)    def readfromproccesserr(self):        """To be executed in a separate thread to make read non-blocking"""        while self.alive:            data = self.p.stderr.raw.read(1024).decode()            self.errQueue.put(data)    def writeloop(self):        """Used to write data from stdout and stderr to the Text widget"""        # if there is anything to write from stdout or stderr, then write it        if not self.errQueue.empty():            self.write(self.errQueue.get())        if not self.outQueue.empty():            self.write(self.outQueue.get())        # run this method again after 10ms        if self.alive:            self.after(10, self.writeloop)    def write(self, string):        self.ttytext.insert(tk.END, string)        self.ttytext.see(tk.END)        self.line_start += len(string)        self.ttytext.inst_trigger()if __name__ == '__main__':    root = tk.Tk()    main_window = Console(root)    main_window.pack(fill=tk.BOTH, expand=True)    main_window.ttytext.focus_force()    root.mainloop()

The code above uses jupyter qtconsole (because it is very handy), otherwise simple python shell can also be used using the InteractiveShell() in code module.I have not completely made functions for Enter key, Up and Down arrow keys. These can be made by the user as per their choice.

This can also be found in Oli's answer here and it is customizable.