Skip to content

Commit

Permalink
some small tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
littlewhitecloud authored Jul 12, 2023
1 parent 152851a commit 81f4568
Show file tree
Hide file tree
Showing 3 changed files with 370 additions and 12 deletions.
37 changes: 34 additions & 3 deletions tktermwidget/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
"""Tktermwidget package"""
from .style import * # noqa: F401
from .tkterm import Terminal # noqa: F401
"""__init__ of the tktermwidget package"""
from json import dump
from pathlib import Path

from platformdirs import user_cache_dir

from .style import * # noqa: F401, F403
from .widgets import Terminal # noqa: F401

# Get the package path
PACKAGE_PATH = Path(user_cache_dir("tktermwidget"))
# Get the history file
HISTORY_FILE = PACKAGE_PATH / "history.txt"
# Get the json file (style)
JSON_FILE = PACKAGE_PATH / "styles.json"

if not PACKAGE_PATH.exists(): # Check the "tktermwidget" is exsit
PACKAGE_PATH.mkdir(parents=True)
# Also create the history file
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
f.close()
# Also create the json file
with open(JSON_FILE, "w", encoding="utf-8") as f:
dump("{}", f)

# Check that the history file exists
if not (HISTORY_FILE).exists():
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
f.close()

# Check that the json file exists
if not (JSON_FILE).exists():
with open(JSON_FILE, "w", encoding="utf-8") as f:
dump("{}", f)
10 changes: 1 addition & 9 deletions tktermwidget/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,6 @@
"foreground": "#efefef",
}

# Check the style file
if not STYLE_PATH.exists():
STYLE_PATH.mkdir(parents=True)
with open(JSON_FILE, "w", encoding="utf-8") as f:
dump("{}", f)
if not (JSON_FILE).exists():
with open(JSON_FILE, "w", encoding="utf-8") as f:
dump("{}", f)


# Functions
def write_style(**styles) -> None:
Expand Down Expand Up @@ -280,5 +271,6 @@ def checkhexcolor(self, event: Event, name: str) -> None:
CUSTOM: dict[str] = load_style()

if __name__ == "__main__":
# An example or a test
configstyle = Config(True, basedon=POWERSHELL)
configstyle.mainloop()
335 changes: 335 additions & 0 deletions tktermwidget/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
"""Tkinter Terminal widget"""
from __future__ import annotations

from os import chdir, getcwd, path
from pathlib import Path
from platform import system
from subprocess import PIPE, Popen
from tkinter import Event, Misc, Text
from tkinter.ttk import Frame, Scrollbar

from platformdirs import user_cache_dir

if __name__ == "__main__": # For develop
from style import DEFAULT
else:
from .style import DEFAULT

HISTORY_PATH = Path(user_cache_dir("tktermwidget"))
HISTORY_FILE = HISTORY_PATH / "history.txt"
SYSTEM = system()
CREATE_NEWCONSOLE = 0
SIGN = "$ "

if SYSTEM == "Windows": # Check if platform is windows
from subprocess import CREATE_NEW_CONSOLE

SIGN = ">"


class AutoHideScrollbar(Scrollbar):
"""Scrollbar that automatically hides when not needed"""

def __init__(self, master=None, **kwargs):
Scrollbar.__init__(self, master=master, **kwargs)

def set(self, first: int, last: int):
"""Set the Scrollbar"""
if float(first) <= 0.0 and float(last) >= 1.0:
self.grid_remove()
else:
self.grid()
Scrollbar.set(self, first, last)


class Terminal(Frame):
"""A terminal widget for tkinter applications
Args:
master (Misc): The parent widget
style (dict, optional): Set the style for the Terminal widget
filehistory (str, optional): Set your own file history instead of the normal
autohide (bool, optional): Whether to autohide the scrollbars. Set true to enable.
*args: Arguments for the text widget
**kwargs: Keyword arguments for the text widget
Methods for outside use:
None
Methods for internal use:
up (Event) -> str: Goes up in the history
down (Event) -> str: Goes down in the history
(If the user is at the bottom of the history, it clears the command)
left (Event) -> str: Goes left in the command if the index is greater than the directory
(So the user can't delete the directory or go left of it)
kill (Event) -> str: Kills the current command
check (Event) -> None: Update cursor and check it if is out of the edit range
execute (Event) -> str: Execute the command"""

def __init__(
self, master: Misc, style: dict = DEFAULT, filehistory: str = None, autohide: bool = False, *args, **kwargs
):
Frame.__init__(self, master)

# Set row and column weights
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)

# Create text widget and x, y scrollbar
self.style: dict = style
horizontal: bool = False
scrollbar = Scrollbar if not autohide else AutoHideScrollbar

if kwargs.get("wrap", "char") == "none":
self.xscroll = scrollbar(self, orient="horizontal")
horizontal = True

self.yscroll = scrollbar(self)
self.text = Text(
self,
*args,
wrap=kwargs.get("wrap", "char"),
yscrollcommand=self.yscroll.set,
relief=kwargs.get("relief", "flat"),
font=kwargs.get("font", ("Cascadia Code", 9, "normal")),
foreground=kwargs.get("foreground", self.style["foreground"]),
background=kwargs.get("background", self.style["background"]),
insertbackground=kwargs.get("insertbackground", self.style["insertbackground"]),
selectbackground=kwargs.get("selectbackground", self.style["selectbackground"]),
selectforeground=kwargs.get("selectforeground", self.style["selectforeground"]),
)

if horizontal:
self.text.config(xscrollcommand=self.xscroll.set)
self.xscroll.config(command=self.text.xview)
self.yscroll.config(command=self.text.yview)

# Grid widgets
self.text.grid(row=0, column=0, sticky="nsew")
if horizontal:
self.xscroll.grid(row=1, column=0, sticky="ew")
self.yscroll.grid(row=0, column=1, sticky="ns")

# Init command prompt
self.directory()

# Set constants
self.longsymbol: str = "\\" if not SYSTEM == "Windows" else "&&"
self.filehistory: str = HISTORY_FILE if not filehistory else filehistory

# Set variables
self.index: int = 1
self.longcmd: str = ""
self.longflag: bool = False
self.current_process: Popen | None = None
self.cursor: int = self.text.index("insert")

self.latest: int = self.cursor

# History recorder
self.history = open(
self.filehistory,
"r+", # Both read and write
encoding="utf-8",
)

self.historys = [i.strip() for i in self.history.readlines()]
self.historyindex = len(self.historys) - 1

# Bind events
self.text.bind("<Up>", self.up, add=True)
self.text.bind("<Down>", self.down, add=True)
self.text.bind("<Return>", self.execute, add=True)
for bind_str in ("<Left>", "<BackSpace>"):
self.text.bind(bind_str, self.left, add=True)
for bind_str in ("<Return>", "<ButtonRelease-1>", "<Left>", "<Right>"):
self.text.bind(bind_str, self.check, add=True)

self.text.bind("<Control-KeyPress-c>", self.kill, add=True) # Isn't working

del horizontal

def check(self, _: Event) -> None:
"""Update cursor and check if it is out of the edit range"""
self.cursor = self.text.index("insert") # Update cursor
if float(self.cursor) < float(self.latest):
for bind_str in ("<KeyPress>", "<KeyPress-BackSpace>"):
self.text.bind(bind_str, lambda _: "break", add=True)
else:
for unbind_str in ("<KeyPress>", "<KeyPress-BackSpace>"):
self.text.unbind(unbind_str)

def directory(self) -> None:
"""Insert the directory"""
self.text.insert("insert", getcwd() + SIGN)

def kill(self, _: Event) -> str:
"""Kill the current process"""
if self.current_process:
self.current_process.terminate()
self.current_process = None
return "break"

def execute(self, _: Event) -> str:
"""Execute the command"""
# Get the line from the text
cmd = self.text.get(f"{self.index}.0", "end-1c")
# Split the command from the line also strip
cmd = cmd.split(SIGN)[-1].strip()

# TODO: get rid off all return "break" in if statements
# use the flag leave: bool instead of

if cmd.endswith(self.longsymbol):
self.longcmd += cmd.split(self.longsymbol)[0]
self.longflag = True
self.newline()
return "break"

if self.longflag:
cmd = self.longcmd + cmd
self.longcmd = ""
self.longflag = False

if cmd: # Record the command if it isn't empty
self.history.write(cmd + "\n")
self.historys.append(cmd)
self.historyindex = len(self.historys) - 1
else: # Leave the loop
self.newline()
self.directory()
self.text.see("end")
return "break"

# Check the command if it is a special command
if cmd in ["clear", "cls"]:
self.text.delete("1.0", "end")
self.directory()
return "break"
elif cmd == "exit":
self.master.quit()
elif cmd.startswith("cd"): # TAG: is all platform use cd...?
if cmd == "cd..":
chdir(path.abspath(path.join(getcwd(), "..")))
else:
chdir(cmd.split()[-1])
self.newline()
self.directory()
return "break"

# Set the insert position is at the end
self.text.mark_set("insert", f"{self.index}.end")
self.text.see("insert")

# TODO: Refactor the way we get output from subprocess
# Run the command
self.current_process = Popen(
cmd,
shell=True,
stdout=PIPE,
stderr=PIPE,
stdin=PIPE,
text=True,
cwd=getcwd(), # TODO: use dynamtic path instead (see #35)
creationflags=CREATE_NEW_CONSOLE,
)
# The following needs to be put in an after so the kill command works

# Check if the command was successful
returnlines: str = ""
errors: str = ""
returnlines, errors = self.current_process.communicate()
returncode = self.current_process.returncode
self.current_process = None

if returncode != 0:
returnlines += errors # If the command was unsuccessful, it doesn't give stdout
# TODO: Get the success message from the command (see #16)

# Output to the text
self.newline()
for line in returnlines:
self.text.insert("insert", line)

# Update the text and the index
self.index = int(self.text.index("insert").split(".")[0])
self.update()
print(self.index)

del returnlines, errors, returncode, cmd
return "break" # Prevent the default newline character insertion

def newline(self) -> None:
"""Insert a newline"""
self.text.insert("insert", "\n")
self.index += 1

def update(self) -> str:
"""Update the text widget or the command has no output"""
# Insert the directory
self.directory()
# Update cursor and check if it is out of the edit range
self.check(None)
# Update latest index
self.latest = self.text.index("insert")
# Warp to the end
self.text.see("end")
return "break"

# Keypress
def down(self, _: Event) -> str:
"""Go down in the history"""
if self.historyindex < len(self.historys) - 1:
self.text.delete(f"{self.index}.0", "end-1c")
self.directory()
# Insert the command
self.text.insert("insert", self.historys[self.historyindex])
self.historyindex += 1
else:
# Clear the command
self.text.delete(f"{self.index}.0", "end-1c")
self.directory()
return "break"

def left(self, _: Event) -> str | None:
"""Go left in the command if the command is greater than the path"""
insert_index = self.text.index("insert")
dir_index = f"{insert_index.split('.')[0]}.{len(getcwd() + SIGN)}"
if insert_index == dir_index:
del insert_index, dir_index
return "break"

def up(self, _: Event) -> str:
"""Go up in the history"""
if self.historyindex >= 0:
self.text.delete(f"{self.index}.0", "end-1c")
self.directory()
# Insert the command
self.text.insert("insert", self.historys[self.historyindex])
self.historyindex -= 1
return "break"


if __name__ == "__main__":
from tkinter import Tk

root = Tk()
root.withdraw()
root.title("Terminal")

term = Terminal(root)
term.pack(expand=True, fill="both")

root.update_idletasks()

minimum_width: int = root.winfo_reqwidth()
minimum_height: int = root.winfo_reqheight()

x_coords = int(root.winfo_screenwidth() / 2 - minimum_width / 2)
y_coords = int(root.wm_maxsize()[1] / 2 - minimum_height / 2)

root.geometry(f"{minimum_width}x{minimum_height}+{x_coords}+{y_coords}")
root.wm_minsize(minimum_width, minimum_height)

root.deiconify()
root.mainloop()

0 comments on commit 81f4568

Please sign in to comment.