How can I create a small IDLE-like Python Shell in Tkinter? How can I create a small IDLE-like Python Shell in Tkinter? shell shell

How can I create a small IDLE-like Python Shell in Tkinter?


Simple Python Shell / Terminal / Command-Prompt


  • ********************* It's literally just a "type input, show output" thing. ************************

import osfrom tkinter import *from subprocess import *class PythonShell:    def __init__(self):        self.master = Tk()        self.mem_cache = open("idle.txt", "w+")        self.body = None        self.entry = None        self.button = None        self.entry_content = None    @staticmethod    def welcome_note():        """        To show welcome note on tkinter window        :return:        """        Label(text="Welcome To My Python Program [Version 1.0]", font='Arial 12', background="#272626",              foreground="white").pack()        Label(text=">> Insert Python Commands <<", font='Arial 12', background="#272626",              foreground="white").pack()    def get_text(self):        """        This method will perform following operations;        1- Get text from body        2- Implies python compilation (treat text as command)        3- Set Output in Output-Entry        :return: get and set text in body of text box        """        content = self.body.get(1.0, "end-1c")        out_put = self.run_commands(content)        self.entry_content.set(out_put)    def store_commands(self, command=None):        try:            self.mem_cache.write(command + ';')            self.mem_cache.close()        except Exception as e:            print(e)    def get_stored_commands(self):        try:            with open("idle.txt", "r") as self.mem_cache:                self.mem_cache.seek(0)                val = self.mem_cache.read()                self.mem_cache.close()                return val        except Exception as e:            print(e)    @staticmethod    def check_if_file_empty():        size = os.stat("idle.txt").st_size        if size != 0:            return True        else:            return False    def run_commands(self, command):        """        This method would return output of every command place in text box        :param command: python command from text box        :return: output of command        """        print("Running command: {}".format(command))        value = None        new_line_char = command.find('\n')        semi_colons_char = command.find(';')        double_quote = command.find('"')        try:            if new_line_char != -1:                if semi_colons_char != -1 & double_quote == -1:                    new_cmd = command.replace("\n", "")                    cmd_value = '"' + new_cmd + '"'                    self.store_commands(command)                    value = check_output("python -c " + cmd_value, shell=True).decode()                elif semi_colons_char == -1 & double_quote == -1:                    new_cmd = command.replace("\n", ";")                    cmd_value = '"' + new_cmd + '"'                    self.store_commands(command)                    value = check_output("python -c " + cmd_value, shell=True).decode()                elif double_quote != -1:                    cmd_1 = command.replace('"', "'")                    new_cmd = cmd_1.replace('\n', ';')                    cmd_value = '"' + new_cmd + '"'                    self.store_commands(command)                    value = check_output("python -c " + cmd_value, shell=True).decode()                elif self.body.compare("end-1c", "==", "1.0"):                    self.entry_content.set("the widget is empty")            elif self.body.compare("end-1c", "==", "1.0"):                value = "The widget is empty. Please Enter Something."            else:                variable_analyzer = command.find('=')                file_size = PythonShell.check_if_file_empty()                if file_size:                    new_cmd = command.replace('"', "'")                    cmd_value = '"' + new_cmd + '"'                    stored_value = self.get_stored_commands()                    cmd = stored_value + cmd_value                    cmd.replace('"', '')                    value = check_output("python -c " + cmd, shell=True).decode()                elif variable_analyzer != -1:                    new_cmd = command.replace('"', "'")                    cmd_value = '"' + new_cmd + '"'                    self.store_commands(cmd_value)                    value = 'Waiting for input...'                    pass                else:                    new_cmd = command.replace('"', "'")                    cmd_value = '"' + new_cmd + '"'                    value = check_output("python -c " + cmd_value, shell=True).decode()        except Exception as ex:            print('>>>', ex)            self.entry_content.set('Invalid Command. Try again!!!')        print('>>', value)        # To Clear Text body After Button Click        # self.body.delete('1.0', END)        return value    def start_terminal(self):        """        Initiate tkinter session to place and run commands        :return:        """        self.master.propagate(0)        self.master.geometry('750x350')        self.master.title('Python IDLE')        self.master.configure(background='#272626')        terminal.welcome_note()        self.body = Text(self.master, height='10', width='75', font='Consolas 12', background="#272626",                         foreground="white",                         insertbackground='white')        # self.body.propagate(0)        self.body.pack(expand=True)        Label(text=">> Command Output <<", font='Arial 12', background="#272626",              foreground="white").pack()        self.entry_content = StringVar()        self.entry = Entry(self.master, textvariable=self.entry_content, width=50, font='Consolas 16',                           background="white",                           foreground="black")        self.entry.pack()        # self.entry.propagate(0)        self.button = Button(self.master, text="Run Command", command=self.get_text, background="white",                             foreground="black",                             font='Helvetica 12').pack()        self.master.mainloop()if __name__ == '__main__':    terminal = PythonShell()    terminal.start_terminal()

The above given python script has following hierarchy as given;

    |import ...          |class PythonShell:        |def __init__(self):...        @staticmethod        |def welcome_note():...        |def get_text(self):...        |def store_commands(self, commmand):...        |def get_stored_commands(self):...        @staticmethod        |def check_if_file_empty():        |def run_commands(self, command):...        |def start_terminal(self):...    |if __name__ == '__main__':...

Workflow:

The basic workflow for the above code is given as follows;

  • def welcome_note():... Includes the Label that will display outside the text body.

  • def get_text(self):... Performs two operations; ** Get text from text body ** & ** Set Output in the Entry Box **.

  • def store_commands(self, command):... Use to store variable into file.

  • def get_stored_commands(self):... Get variable stored in file.

  • def check_if_file_empty():... Check Size of file.

  • def run_commands(self, command):... This method act as python compiler that take commands, do processing and yield output for the given command. To run commands, i would recommend to use subprocess-module because it provides more powerful facilities for spawning new processes and retrieving their results; To run window-commands using python includes various builtin libraries such as;

    1. os (in detail), 2. subprocess (in detail) etc.

    To checkout which is better to use, visit reference: subprocess- module is preferable than os-module.

  • def start_terminal(self):... This method simply involves the functionality to initiate tkinter session window and show basic layout for input and output window.

    You can further modify and optimize this code according to your requirement.


Workaroud:

This simple tkinter GUI based python shell perform simple functionality as windows-command-prompt. To run python commands directly in command-prompt without moving into python terminal, we do simple as;

python -c "print('Hey Eleeza!!!')"

Its result would be simple as;

Hey Eleeza!!!

Similarly, to run more than one lines directly at a time as given;

python -c "import platform;sys_info=platform.uname();print(sys_info)"

Its output would be as;

My System Info: uname_result(system='Windows', node='DESKTOP-J75UTG5', release='10', version='10.0.18362', machine='AMD64', processor='Intel64 Family 6 Model 142 Stepping 10, GenuineIntel')

So to use this tkinter python shell;

  • Either you can place command as;

    import platformvalue=platform.uname()print('Value:', value)
  • or like this way;

    import platform;value=platform.uname();print('Value:', value)
  • or simply inline command as

    import platform;value=platform.uname();print('Value:', value)

You will get the same result.


This is a simple shell mainly using exec() to execute the python statements and subprocess.Popen() to execute external command:

import tkinter as tkimport sys, ioimport subprocess as subpfrom contextlib import redirect_stdoutclass Shell(tk.Text):  def __init__(self, parent, **kwargs):    tk.Text.__init__(self, parent, **kwargs)    self.bind('<Key>', self.on_key) # setup handler to process pressed keys    self.cmd = None        # hold the last command issued    self.show_prompt()  # to append given text at the end of Text box  def insert_text(self, txt='', end='\n'):    self.insert(tk.END, txt+end)    self.see(tk.END) # make sure it is visible  def show_prompt(self):    self.insert_text('>> ', end='')    self.mark_set(tk.INSERT, tk.END) # make sure the input cursor is at the end    self.cursor = self.index(tk.INSERT) # save the input position  # handler to process keyboard input  def on_key(self, event):    #print(event)    if event.keysym == 'Up':      if self.cmd:        # show the last command        self.delete(self.cursor, tk.END)        self.insert(self.cursor, self.cmd)      return "break" # disable the default handling of up key    if event.keysym == 'Down':      return "break" # disable the default handling of down key    if event.keysym in ('Left', 'BackSpace'):      current = self.index(tk.INSERT) # get the current position of the input cursor      if self.compare(current, '==', self.cursor):        # if input cursor is at the beginning of input (after the prompt), do nothing        return "break"    if event.keysym == 'Return':      # extract the command input      cmd = self.get(self.cursor, tk.END).strip()      self.insert_text() # advance to next line      if cmd.startswith('`'):        # it is an external command        self.system(cmd)      else:        # it is python statement        self.execute(cmd)      self.show_prompt()      return "break" # disable the default handling of Enter key    if event.keysym == 'Escape':      self.master.destroy() # quit the shell  # function to handle python statement input  def execute(self, cmd):    self.cmd = cmd  # save the command    # use redirect_stdout() to capture the output of exec() to a string    f = io.StringIO()    with redirect_stdout(f):      try:        exec(self.cmd, globals())      except Exception as e:        print(e)    # then append the output of exec() in the Text box    self.insert_text(f.getvalue(), end='')  # function to handle external command input  def system(self, cmd):    self.cmd = cmd  # save the command    try:      # extract the actual command      cmd = cmd[cmd.index('`')+1:cmd.rindex('`')]      proc = subp.Popen(cmd, stdout=subp.PIPE, stderr=subp.PIPE, text=True)      stdout, stderr = proc.communicate(5) # get the command output      # append the command output to Text box      self.insert_text(stdout)    except Exception as e:      self.insert_text(str(e))root = tk.Tk()root.title('Simple Python Shell')shell = Shell(root, width=100, height=50, font=('Consolas', 10))shell.pack(fill=tk.BOTH, expand=1)shell.focus_set()root.mainloop()

Just input normal python statement:

>> x = 1>> print(x)1

Or input a shell command:

>> `cmd /c date /t`2019-12-09

You can also use Up key to recall the last command.

Please note that if you execute a system command requiring user input, the shell will be freeze for 5 seconds (timeout period used in communicate()).

You can modify on_key() function to suit your need.

Please also be reminded that using exec() is not a good practice.


I had implemented python shell using code.InteractiveConsole to execute the commands for a project. Below is a simplified version, though still quite long because I had written bindings for special keys (like Return, Tab ...) to behave like in the python console. It is possible to add more features such as autocompletion with jedi and syntax highighting with pygments.

The main idea is that I use the push() method of the code.InteractiveConsole to execute the commands. This method returns True if it is a partial command, e.g. def test(x):, and I use this feedback to insert a ... prompt, otherwise, the output is displayed and a new >>> prompt is displayed. I capture the output using contextlib.redirect_stdout.

Also there is a lot of code involving marks and comparing indexes because I prevent the user from inserting text inside previously executed commands. The idea is that I created a mark 'input' which tells me where the start of the active prompt is and with self.compare('insert', '<', 'input') I can know when the user is trying to insert text above the active prompt.

import tkinter as tkimport sysimport refrom code import InteractiveConsolefrom contextlib import redirect_stderr, redirect_stdoutfrom io import StringIOclass History(list):    def __getitem__(self, index):        try:            return list.__getitem__(self, index)        except IndexError:            returnclass TextConsole(tk.Text):    def __init__(self, master, **kw):        kw.setdefault('width', 50)        kw.setdefault('wrap', 'word')        kw.setdefault('prompt1', '>>> ')        kw.setdefault('prompt2', '... ')        banner = kw.pop('banner', 'Python %s\n' % sys.version)        self._prompt1 = kw.pop('prompt1')        self._prompt2 = kw.pop('prompt2')        tk.Text.__init__(self, master, **kw)        # --- history        self.history = History()        self._hist_item = 0        self._hist_match = ''        # --- initialization        self._console = InteractiveConsole() # python console to execute commands        self.insert('end', banner, 'banner')        self.prompt()        self.mark_set('input', 'insert')        self.mark_gravity('input', 'left')        # --- bindings        self.bind('<Control-Return>', self.on_ctrl_return)        self.bind('<Shift-Return>', self.on_shift_return)        self.bind('<KeyPress>', self.on_key_press)        self.bind('<KeyRelease>', self.on_key_release)        self.bind('<Tab>', self.on_tab)        self.bind('<Down>', self.on_down)        self.bind('<Up>', self.on_up)        self.bind('<Return>', self.on_return)        self.bind('<BackSpace>', self.on_backspace)        self.bind('<Control-c>', self.on_ctrl_c)        self.bind('<<Paste>>', self.on_paste)    def on_ctrl_c(self, event):        """Copy selected code, removing prompts first"""        sel = self.tag_ranges('sel')        if sel:            txt = self.get('sel.first', 'sel.last').splitlines()            lines = []            for i, line in enumerate(txt):                if line.startswith(self._prompt1):                    lines.append(line[len(self._prompt1):])                elif line.startswith(self._prompt2):                    lines.append(line[len(self._prompt2):])                else:                    lines.append(line)            self.clipboard_clear()            self.clipboard_append('\n'.join(lines))        return 'break'    def on_paste(self, event):        """Paste commands"""        if self.compare('insert', '<', 'input'):            return "break"        sel = self.tag_ranges('sel')        if sel:            self.delete('sel.first', 'sel.last')        txt = self.clipboard_get()        self.insert("insert", txt)        self.insert_cmd(self.get("input", "end"))        return 'break'    def prompt(self, result=False):        """Insert a prompt"""        if result:            self.insert('end', self._prompt2, 'prompt')        else:            self.insert('end', self._prompt1, 'prompt')        self.mark_set('input', 'end-1c')    def on_key_press(self, event):        """Prevent text insertion in command history"""        if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']:            self._hist_item = len(self.history)            self.mark_set('insert', 'input lineend')            if not event.char.isalnum():                return 'break'    def on_key_release(self, event):        """Reset history scrolling"""        if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']:            self._hist_item = len(self.history)            return 'break'    def on_up(self, event):        """Handle up arrow key press"""        if self.compare('insert', '<', 'input'):            self.mark_set('insert', 'end')            return 'break'        elif self.index('input linestart') == self.index('insert linestart'):            # navigate history            line = self.get('input', 'insert')            self._hist_match = line            hist_item = self._hist_item            self._hist_item -= 1            item = self.history[self._hist_item]            while self._hist_item >= 0 and not item.startswith(line):                self._hist_item -= 1                item = self.history[self._hist_item]            if self._hist_item >= 0:                index = self.index('insert')                self.insert_cmd(item)                self.mark_set('insert', index)            else:                self._hist_item = hist_item            return 'break'    def on_down(self, event):        """Handle down arrow key press"""        if self.compare('insert', '<', 'input'):            self.mark_set('insert', 'end')            return 'break'        elif self.compare('insert lineend', '==', 'end-1c'):            # navigate history            line = self._hist_match            self._hist_item += 1            item = self.history[self._hist_item]            while item is not None and not item.startswith(line):                self._hist_item += 1                item = self.history[self._hist_item]            if item is not None:                self.insert_cmd(item)                self.mark_set('insert', 'input+%ic' % len(self._hist_match))            else:                self._hist_item = len(self.history)                self.delete('input', 'end')                self.insert('insert', line)            return 'break'    def on_tab(self, event):        """Handle tab key press"""        if self.compare('insert', '<', 'input'):            self.mark_set('insert', 'input lineend')            return "break"        # indent code        sel = self.tag_ranges('sel')        if sel:            start = str(self.index('sel.first'))            end = str(self.index('sel.last'))            start_line = int(start.split('.')[0])            end_line = int(end.split('.')[0]) + 1            for line in range(start_line, end_line):                self.insert('%i.0' % line, '    ')        else:            txt = self.get('insert-1c')            if not txt.isalnum() and txt != '.':                self.insert('insert', '    ')        return "break"    def on_shift_return(self, event):        """Handle Shift+Return key press"""        if self.compare('insert', '<', 'input'):            self.mark_set('insert', 'input lineend')            return 'break'        else: # execute commands            self.mark_set('insert', 'end')            self.insert('insert', '\n')            self.insert('insert', self._prompt2, 'prompt')            self.eval_current(True)    def on_return(self, event=None):        """Handle Return key press"""        if self.compare('insert', '<', 'input'):            self.mark_set('insert', 'input lineend')            return 'break'        else:            self.eval_current(True)            self.see('end')        return 'break'    def on_ctrl_return(self, event=None):        """Handle Ctrl+Return key press"""        self.insert('insert', '\n' + self._prompt2, 'prompt')        return 'break'    def on_backspace(self, event):        """Handle delete key press"""        if self.compare('insert', '<=', 'input'):            self.mark_set('insert', 'input lineend')            return 'break'        sel = self.tag_ranges('sel')        if sel:            self.delete('sel.first', 'sel.last')        else:            linestart = self.get('insert linestart', 'insert')            if re.search(r'    $', linestart):                self.delete('insert-4c', 'insert')            else:                self.delete('insert-1c')        return 'break'    def insert_cmd(self, cmd):        """Insert lines of code, adding prompts"""        input_index = self.index('input')        self.delete('input', 'end')        lines = cmd.splitlines()        if lines:            indent = len(re.search(r'^( )*', lines[0]).group())            self.insert('insert', lines[0][indent:])            for line in lines[1:]:                line = line[indent:]                self.insert('insert', '\n')                self.prompt(True)                self.insert('insert', line)                self.mark_set('input', input_index)        self.see('end')    def eval_current(self, auto_indent=False):        """Evaluate code"""        index = self.index('input')        lines = self.get('input', 'insert lineend').splitlines() # commands to execute        self.mark_set('insert', 'insert lineend')        if lines:  # there is code to execute            # remove prompts            lines = [lines[0].rstrip()] + [line[len(self._prompt2):].rstrip() for line in lines[1:]]            for i, l in enumerate(lines):                if l.endswith('?'):                    lines[i] = 'help(%s)' % l[:-1]            cmds = '\n'.join(lines)            self.insert('insert', '\n')            out = StringIO()  # command output            err = StringIO()  # command error traceback            with redirect_stderr(err):     # redirect error traceback to err                with redirect_stdout(out): # redirect command output                    # execute commands in interactive console                    res = self._console.push(cmds)                    # if res is True, this is a partial command, e.g. 'def test():' and we need to wait for the rest of the code            errors = err.getvalue()            if errors:  # there were errors during the execution                self.insert('end', errors)  # display the traceback                self.mark_set('input', 'end')                self.see('end')                self.prompt() # insert new prompt            else:                output = out.getvalue()  # get output                if output:                    self.insert('end', output, 'output')                self.mark_set('input', 'end')                self.see('end')                if not res and self.compare('insert linestart', '>', 'insert'):                    self.insert('insert', '\n')                self.prompt(res)                if auto_indent and lines:                    # insert indentation similar to previous lines                    indent = re.search(r'^( )*', lines[-1]).group()                    line = lines[-1].strip()                    if line and line[-1] == ':':                        indent = indent + '    '                    self.insert('insert', indent)                self.see('end')                if res:                    self.mark_set('input', index)                    self._console.resetbuffer()  # clear buffer since the whole command will be retrieved from the text widget                elif lines:                    self.history.append(lines)  # add commands to history                    self._hist_item = len(self.history)            out.close()            err.close()        else:            self.insert('insert', '\n')            self.prompt()if __name__ == '__main__':    root = tk.Tk()    console = TextConsole(root)    console.pack(fill='both', expand=True)    root.mainloop()