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, execute the python command and give the output of the python command. I know that IDLE is made in Tkinter, so it uses the widgets?
It's literally just a "type input, show output" thing.
I've tried searching it up but it seems like most of the results are to do with the command line, which isn't what I'm looking for. The only other question that was exactly like mine wasn't what I had in mind, either. I also tried loking up the source code for IDLE but couldn't find what I was looking for.
I've found some answers that work for Linux but I'm on Windows 10...
I need the "shell" to be in Tkinter because on one side of the screen will be something else which is connected to the command outputs.
Does anyone know the widgets used to make a very simple Python shell?
Simple Python Shell / Terminal / Command-Prompt
- ********************* It's literally just a "
type input
, show output
" thing. ************************
import os
from 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 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.
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
class 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 tk
import sys
import re
from code import InteractiveConsole
from contextlib import redirect_stderr, redirect_stdout
from io import StringIO
class History(list):
def __getitem__(self, index):
try:
return list.__getitem__(self, index)
except IndexError:
return
class 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()