2

I made two tkinter text boxes one of which takes your python script as input and the other one shows results of the execution of your script but when I used input() command I got an error. Below given is the class for stdout redirector and also the execute function which executes after reading the script, which works fine. I have not included Text, tkinter, etc because I use all the general methods that work with the code like Text.get(), Text.mark_set(), Text.replace(), etc and also some of the functions are not included here. Other than the script and the output boxes I also tried to embed whole of the console in a textbox with InteractiveConsole but the problem was same in the case of receiving input or stdin but in both the cases stdout and stderr works fine.

from code import InteractiveConsole, InteractiveInterpreter


class StdoutRedirector(object):
    def __init__(self, text_widget):
        self.text_space = text_widget

    def write(self, string):
        self.text_space.insert('end', string)
        self.text_space.see('end')


##class StdinRedirector(object):
##    def __init__(self, text_widget):
##        self.text_space = text_widget
##
##    def readline(self) -> str:
##        t = self.text_space.get(INSERT, f"{int(text.index(INSERT).split('.')[0])}.{int(text.index(INSERT).split('.')[1])}")
##        return t


def execute(event=None):
    save()
    code = text.get('1.0',END+'-1c')
    stdin = sys.stdin
    stdout = sys.stdout 
    stderr = sys.stderr

    output.delete('1.0',END)
##    def a():
##        sys.stdin = StdinRedirector(output)
##    output.bind('<Return>', lambda: a)
    
    sys.stdout = StdoutRedirector(output)
    sys.stderr = StdoutRedirector(output)
    
    interp = InteractiveInterpreter()
    interp.runcode(code)

    sys.stdout = stdout
    sys.stderr = stderr
##    sys.stdin = stdin

After which I tried Redirecting stdin, which obviously didn't work, and instead the application hung and the window stopped responding even after trying again and again. Please help me with this... I don't know if its impossible but PyCharm and others have I/O Streams inside them so maybe the console or the execution window CAN be wholly embedded in a text box.

4
  • Maybe you can get ideas from stackoverflow.com/questions/59164314/…
    – j_4321
    Commented Sep 28, 2020 at 11:22
  • @j_4321 I tried out all of them from the above mentioned link but all of them have the same problem of not being able to receive input. I suggest you to try all and you will also notice that all have same problem
    – prerakl123
    Commented Sep 29, 2020 at 5:57
  • Ok, sorry I did not read the question carefully enough, though usually command line input and GUI are not mixed together so I misunderstood input with input from the user through the text. Can't help you with that one.
    – j_4321
    Commented Sep 29, 2020 at 6:49
  • Can in anyway await be used or async def ? Because while making a discord bot it struck my mind if in discord it waits for user input then maybe till user inputs anything till then it could wait and then on hitting Enter key it sends data to terminal Just an idea, I don't know if it can be done ?
    – prerakl123
    Commented Oct 2, 2020 at 9:18

1 Answer 1

1

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 tk
import subprocess
import queue
import os
from threading import Thread


class 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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.