How Can I Create A Small Idle-like Python Shell In Tkinter?
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 usesubprocess-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 initiatetkinter
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?"