Skip to content Skip to sidebar Skip to footer

How Can I Create A Small Idle-like Python Shell In Tkinter?

I'm trying to make a thing controlled by a Python Shell GUI. The only thing is, I don't know how to make that whole input/output thing. I just want to be able to type an input, exe

Solution 1:

Simple Python Shell / Terminal / Command-Prompt


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

import os
from tkinter import *
from subprocess import *


classPythonShell:

    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    @staticmethoddefwelcome_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()

    defget_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)

    defstore_commands(self, command=None):

        try:
            self.mem_cache.write(command + ';')
            self.mem_cache.close()

        except Exception as e:
            print(e)

    defget_stored_commands(self):
        try:
            withopen("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)

    @staticmethoddefcheck_if_file_empty():
        size = os.stat("idle.txt").st_size

        if size != 0:
            returnTrueelse:
            returnFalsedefrun_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...'passelse:
                    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

    defstart_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 ...      
    |classPythonShell:
        |def__init__(self):...

        @staticmethod
        |defwelcome_note():...
        |defget_text(self):...
        |defstore_commands(self, commmand):...
        |defget_stored_commands(self):...

        @staticmethod
        |defcheck_if_file_empty():
        |defrun_commands(self, command):...
        |defstart_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 platform
    value=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.

Solution 2:

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

import tkinter as tk
import sys, io
import subprocess as subp
from contextlib import redirect_stdout

classShell(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 boxdefinsert_text(self, txt='', end='\n'):
    self.insert(tk.END, txt+end)
    self.see(tk.END) # make sure it is visibledefshow_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 inputdefon_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 keyif event.keysym == 'Down':
      return"break"# disable the default handling of down keyif event.keysym in ('Left', 'BackSpace'):
      current = self.index(tk.INSERT) # get the current position of the input cursorif self.compare(current, '==', self.cursor):
        # if input cursor is at the beginning of input (after the prompt), do nothingreturn"break"if event.keysym == 'Return':
      # extract the command input
      cmd = self.get(self.cursor, tk.END).strip()
      self.insert_text() # advance to next lineif 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 keyif event.keysym == 'Escape':
      self.master.destroy() # quit the shell# function to handle python statement inputdefexecute(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 inputdefsystem(self, cmd):
    self.cmd = cmd  # save the commandtry:
      # 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.

Solution 3:

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 tk
import sys
import re
from code import InteractiveConsole
from contextlib import redirect_stderr, redirect_stdout
from io import StringIO


classHistory(list):
    def__getitem__(self, index):
        try:
            returnlist.__getitem__(self, index)
        except IndexError:
            returnclassTextConsole(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)

    defon_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 inenumerate(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'defon_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'defprompt(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')

    defon_key_press(self, event):
        """Prevent text insertion in command history"""if self.compare('insert', '<', 'input') and event.keysym notin ['Left', 'Right']:
            self._hist_item = len(self.history)
            self.mark_set('insert', 'input lineend')
            ifnot event.char.isalnum():
                return'break'defon_key_release(self, event):
        """Reset history scrolling"""if self.compare('insert', '<', 'input') and event.keysym notin ['Left', 'Right']:
            self._hist_item = len(self.history)
            return'break'defon_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 >= 0andnot 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'defon_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 isnotNoneandnot item.startswith(line):
                self._hist_item += 1
                item = self.history[self._hist_item]
            if item isnotNone:
                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'defon_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]) + 1for line inrange(start_line, end_line):
                self.insert('%i.0' % line, '    ')
        else:
            txt = self.get('insert-1c')
            ifnot txt.isalnum() and txt != '.':
                self.insert('insert', '    ')
        return"break"defon_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)

    defon_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'defon_ctrl_return(self, event=None):
        """Handle Ctrl+Return key press"""
        self.insert('insert', '\n' + self._prompt2, 'prompt')
        return'break'defon_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'definsert_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')

    defeval_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 inenumerate(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 tracebackwith redirect_stderr(err):     # redirect error traceback to errwith 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 promptelse:
                output = out.getvalue()  # get outputif output:
                    self.insert('end', output, 'output')
                self.mark_set('input', 'end')
                self.see('end')
                ifnot 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 widgetelif 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()

Post a Comment for "How Can I Create A Small Idle-like Python Shell In Tkinter?"