diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index ada3dea..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,14 +0,0 @@ -**Flags** - -**Tests** - - -**Note** - diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9cd259c..465bdaf 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -4,7 +4,9 @@ name: Deploy Jekyll with GitHub Pages dependencies preinstalled on: # Runs on pushes targeting the default branch push: - branches: ["main"] + branches: ["main"] # Only merge pull_request on main + # Don't create any single commit on main + # Get less github action use # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/README.md b/README.md index 2c5c3ea..52b4927 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@

TkTerminal

-[![PyPI](https://img.shields.io/pypi/v/tktermwidget)](https://pypi.org/project/tktermwidget) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Platform](https://img.shields.io/powershellgallery/p/Pester?color=blue) +[![PyPI](https://img.shields.io/pypi/v/tktermwidget)](https://pypi.org/project/tktermwidget) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) ### 🌏 [简体中文](README_CH.md) diff --git a/README_CH.md b/README_CH.md index 7b64c93..6c6eaa9 100644 --- a/README_CH.md +++ b/README_CH.md @@ -1,7 +1,9 @@

TkTerminal

-[![PyPI](https://img.shields.io/pypi/v/tktermwidget)](https://pypi.org/project/tktermwidget) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Platform](https://img.shields.io/powershellgallery/p/Pester?color=blue) +[![PyPI](https://img.shields.io/pypi/v/tktermwidget)](https://pypi.org/project/tktermwidget) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) ```TkTermianl``` 是一个使用 tkinter 用 Python 编写的终端模拟器 @@ -57,7 +59,7 @@ pip install tktermwidget ## 样例: ```python -# -*- coding: gbk -*- +"""An example for tktermwidget""" from tkinter import Tk from tkterm import Terminal diff --git a/tktermwidget/__init__.py b/tktermwidget/__init__.py index 2e3b19a..43eef8a 100644 --- a/tktermwidget/__init__.py +++ b/tktermwidget/__init__.py @@ -1,3 +1,8 @@ -"""Tktermwidget package""" -from .style import * # noqa: F401 -from .tkterm import Terminal # noqa: F401 +"""Import tktermwidget package""" +from .utils import check + +check() # Check the files + +# Import them after the check +from .style import * # noqa: F401, F403, E402 +from .widgets import Terminal # noqa: F401, E402 diff --git a/tktermwidget/style.py b/tktermwidget/style.py index ab36d49..93b4c29 100644 --- a/tktermwidget/style.py +++ b/tktermwidget/style.py @@ -1,4 +1,4 @@ -"""Styles for terminal widget""" +"""Write or config styles for terminal widget""" from __future__ import annotations from json import dump, load @@ -14,14 +14,16 @@ STYLE_PATH = Path(user_cache_dir("tktermwidget")) JSON_FILE = STYLE_PATH / "styles.json" -# Styles format: -# {yourstylename}: dict[str] = { -# "background": "{yourhexcolor}", -# "insertbackground": "{yourhexcolor}", -# "selectbackground": "{yourhexcolor}", -# "selectforeground": "{yourhexcolor}", -# "foreground": "{yourhexcolor}", -# } +""" +# A style example +{stylename}: dict[str] = { # Style for {...} + "background": "{hexcolor}", + "insertbackground": "{hexcolor}", + "selectbackground": "{hexcolor}", + "selectforeground": "{hexcolor}", + "foreground": "{hexcolor}", +} +""" # Built-in styles DEFAULT: dict[str] = { # Style for normal tkterminalwidget @@ -56,17 +58,7 @@ "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: """Write the style into the json file""" # User can call this function to write the style without gui @@ -82,37 +74,51 @@ def load_style() -> dict: return load(f) -# Class class Config(Tk): - """ "A config gui for user to edit their custom styles""" + """A config gui for user to edit their custom styles - def __init__(self, usetheme: bool = False, basedon: dict[str] = DEFAULT): + Args: + edit (bool): Enable the edit the text in the render + basedon (dict): Create a style based on the style you choose + usetheme (bool): Enable the apply sv_ttk theme to the window + """ + + def __init__(self, edit: bool = False, basedon: dict[str] = DEFAULT, usetheme: bool = False): super().__init__() + + # Setup window self.geometry("855x525") self.title("Config your custom style") self.resizable(False, False) - self.iconbitmap("") # Must call this function or we can't get the hwnd + self.iconbitmap("") + # Apply sv_ttk theme to the window if usetheme: - from darkdetect import isDark - from sv_ttk import set_theme - - set_theme("dark" if isDark() else "light") - self.option_add("*font", ("Cascadia Mono", 9)) - - if isDark(): - from ctypes import byref, c_int, sizeof, windll - - windll.dwmapi.DwmSetWindowAttribute( - windll.user32.GetParent(self.winfo_id()), 20, byref(c_int(2)), sizeof(c_int(2)) - ) - self.withdraw() - self.deiconify() - + try: + from darkdetect import isDark + from sv_ttk import set_theme + except ImportError: + usetheme = False + else: + set_theme("dark" if isDark() else "light") + self.option_add("*font", ("Cascadia Mono", 9)) + + # Enable window's darkmode + if isDark(): + from ctypes import byref, c_int, sizeof, windll + + windll.dwmapi.DwmSetWindowAttribute( + windll.user32.GetParent(self.winfo_id()), 20, byref(c_int(2)), sizeof(c_int(2)) + ) + self.withdraw() + self.deiconify() + + # If basedon is not DEFAULT then use basedon + # If basedon is DEFAULT then use load_style() + # If load_style() return a empty dict then use DEFAULT self.style: dict[str] = basedon if basedon != DEFAULT else load_style() if load_style() != {} else DEFAULT # Color choose or input widgets - # TODO: check the hex color is it vaild buttonframe = Frame(self) save = Button(buttonframe, text="Save", width=6, command=self.savestyle) cancel = Button(buttonframe, text="Cancel", width=6, command=self.destroy) @@ -123,6 +129,7 @@ def __init__(self, usetheme: bool = False, basedon: dict[str] = DEFAULT): backgroundentry = Entry(backgroundframe) backgroundbutton = Button(backgroundframe, command=lambda: self.selectcolor(backgroundentry, "background")) + # TODO: improve all the labels' text insertbackgroundframe = Frame(self) insertbackground = Label(insertbackgroundframe, text="Choose or input your insertbackground hex color") insertbackgroundentry = Entry(insertbackgroundframe) @@ -152,27 +159,28 @@ def __init__(self, usetheme: bool = False, basedon: dict[str] = DEFAULT): foregroundentry = Entry(foregroundframe) foregroundbutton = Button(foregroundframe, command=lambda: self.selectcolor(foregroundentry, "foreground")) - # Style render configs + # Config style render self.render = Text( self, width=40, + relief="flat", + font=("Cascadia Mono", 9, "normal"), + foreground=self.style["foreground"], background=self.style["background"], insertbackground=self.style["insertbackground"], selectbackground=self.style["selectbackground"], selectforeground=self.style["selectforeground"], - foreground=self.style["foreground"], - font=("Cascadia Mono", 9, "normal"), - relief="flat", ) + # Write down some example text self.render.insert("insert", "This is a normal text for test style.") - self.render.tag_add("select", "1.31", "1.36") + self.render.tag_add("select", "1.28", "1.39") self.render.tag_config( "select", background=self.style["selectbackground"], foreground=self.style["selectforeground"] ) - self.render["state"] = "disable" + self.render["state"] = "normal" if edit else "disable" - # add the theme to the button widgets if usetheme == True + # Apply the theme to the button widgets if usetheme is True if usetheme: for widget in ( backgroundbutton, @@ -184,13 +192,20 @@ def __init__(self, usetheme: bool = False, basedon: dict[str] = DEFAULT): widget.config(style="Accent.TButton", width=2, text="🎨") save.config(style="Accent.TButton") - # fill the entry with hexcolor before pack + # Fill the entry with hexcolor for widget, hexcolor in zip( (backgroundentry, insertbackgroundentry, selectbackgroundentry, selectforegroundentry, foregroundentry), self.style.values(), ): widget.insert("insert", hexcolor) + # Bind some events + backgroundentry.bind("", lambda event: self.checkhexcolor(event, "background")) + insertbackgroundentry.bind("", lambda event: self.checkhexcolor(event, "insertbackground")) + selectbackgroundentry.bind("", lambda event: self.checkhexcolor(event, "selectbackground")) + selectforegroundentry.bind("", lambda event: self.checkhexcolor(event, "selectforeground")) + foregroundentry.bind("", lambda event: self.checkhexcolor(event, "foreground")) + # Pack the widgets cancel.pack(side="right", padx=1) save.pack(side="right", padx=3) @@ -218,12 +233,6 @@ def __init__(self, usetheme: bool = False, basedon: dict[str] = DEFAULT): ): widget.pack(side="left", padx=3) - backgroundentry.bind("", lambda event: self.checkhexcolor(event, "background")) - insertbackgroundentry.bind("", lambda event: self.checkhexcolor(event, "insertbackground")) - selectbackgroundentry.bind("", lambda event: self.checkhexcolor(event, "selectbackground")) - selectforegroundentry.bind("", lambda event: self.checkhexcolor(event, "selectforeground")) - foregroundentry.bind("", lambda event: self.checkhexcolor(event, "foreground")) - for widget in ( backgroundframe, insertbackgroundframe, @@ -243,7 +252,7 @@ def selectcolor(self, entry: Entry, name: str) -> None: self.updaterender() # update the render to show the latest style def updaterender(self) -> None: - """Let the render show with the latest style""" + """Let the render widget render with the latest style""" self.render.config( background=self.style["background"], insertbackground=self.style["insertbackground"], @@ -257,7 +266,7 @@ def updaterender(self) -> None: self.update() def savestyle(self) -> None: - """Save the style""" + """Save the style to the json file""" write_style( background=self.style["background"], insertbackground=self.style["insertbackground"], @@ -268,7 +277,7 @@ def savestyle(self) -> None: self.destroy() def checkhexcolor(self, event: Event, name: str) -> None: - """Check the hex color""" + """Check the hex color is vaild""" if match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", event.widget.get()): event.widget.state(["invalid"]) self.style[name] = event.widget.get() @@ -277,8 +286,10 @@ def checkhexcolor(self, event: Event, name: str) -> None: event.widget.state(["!invalid"]) -CUSTOM: dict[str] = load_style() +# Load the custom style +CUSTOM: dict[str] = load_style() if load_style() != {} else DEFAULT if __name__ == "__main__": - configstyle = Config(True, basedon=POWERSHELL) + # An example basedon "powershell" style and also use sv_ttk theme + configstyle = Config(edit=True, basedon=POWERSHELL, usetheme=True) configstyle.mainloop() diff --git a/tktermwidget/utils.py b/tktermwidget/utils.py new file mode 100644 index 0000000..067e430 --- /dev/null +++ b/tktermwidget/utils.py @@ -0,0 +1,33 @@ +"""Some useful tools""" +from pathlib import Path + +from platformdirs import user_cache_dir + +# 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" + + +def check(): + """Check files and create them if they don't exsit""" + from json import dump + + # Check the "tktermwidget" is exsit + if not PACKAGE_PATH.exists(): + PACKAGE_PATH.mkdir(parents=True) + + # 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) + + +check() diff --git a/tktermwidget/tkterm.py b/tktermwidget/widgets.py similarity index 73% rename from tktermwidget/tkterm.py rename to tktermwidget/widgets.py index 95732ac..34b36f8 100644 --- a/tktermwidget/tkterm.py +++ b/tktermwidget/widgets.py @@ -1,7 +1,7 @@ -"""Terminal widget for tkinter""" +"""Tkinter Terminal widget""" from __future__ import annotations -from os import getcwd +from os import chdir, getcwd, path from pathlib import Path from platform import system from subprocess import PIPE, Popen @@ -10,41 +10,27 @@ from platformdirs import user_cache_dir -dev: bool = False -if dev: +if __name__ == "__main__": # For develop from style import DEFAULT else: - from .style import DEFAULT # noqa: F401 + from .style import DEFAULT -# Set constants HISTORY_PATH = Path(user_cache_dir("tktermwidget")) HISTORY_FILE = HISTORY_PATH / "history.txt" SYSTEM = system() -if SYSTEM == "Windows": +CREATE_NEWCONSOLE = 0 +SIGN = "$ " + +if SYSTEM == "Windows": # Check if platform is windows from subprocess import CREATE_NEW_CONSOLE SIGN = ">" -else: - CREATE_NEW_CONSOLE = 0 - SIGN = "$ " - -# Check that the history directory exists -if not HISTORY_PATH.exists(): - HISTORY_PATH.mkdir(parents=True) - # Also create the history file - with open(HISTORY_FILE, "w", encoding="utf-8") as f: - f.close() - -# Check that the history file exists -if not (HISTORY_FILE).exists(): - with open(HISTORY_FILE, "w", encoding="utf-8") as f: - f.close() class AutoHideScrollbar(Scrollbar): """Scrollbar that automatically hides when not needed""" - def __init__(self, master=None, **kwargs): + def __init__(self, master: Misc = None, **kwargs): Scrollbar.__init__(self, master=master, **kwargs) def set(self, first: int, last: int): @@ -61,8 +47,9 @@ class Terminal(Frame): Args: master (Misc): The parent widget - autohide (bool, optional): Whether to autohide the scrollbars. - (Set true to enable it.) + 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 @@ -76,16 +63,11 @@ class Terminal(Frame): 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 - loop (Event) -> str: Runs the command typed""" + 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, + self, master: Misc, style: dict = DEFAULT, filehistory: str = None, autohide: bool = False, *args, **kwargs ): Frame.__init__(self, master) @@ -93,30 +75,30 @@ def __init__( self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - # Create text widget and scrollbars - self.style = style - - scrollbars = Scrollbar if not autohide else AutoHideScrollbar + # 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 = scrollbars(self, orient="horizontal") - self.xscroll.grid(row=1, column=0, sticky="ew") + self.xscroll = scrollbar(self, orient="horizontal") horizontal = True - self.yscroll = scrollbars(self) + 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"]), - relief=kwargs.get("relief", "flat"), - foreground=kwargs.get("foreground", self.style["foreground"]), - yscrollcommand=self.yscroll.set, - wrap=kwargs.get("wrap", "char"), - font=kwargs.get("font", ("Cascadia Code", 9, "normal")), ) + if horizontal: self.text.config(xscrollcommand=self.xscroll.set) self.xscroll.config(command=self.text.xview) @@ -124,131 +106,89 @@ def __init__( # 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") - # self.yscroll.grid(row=1 if horizontal else 0, column=0 if horizontal else 1, sticky="ns") - # Create command prompt + # 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.index: int = 1 self.cursor: int = self.text.index("insert") - self.longsymbol: str = "\\" if not SYSTEM == "Windows" else "&&" - self.longcmd: str = "" - self.filehistory: str = HISTORY_FILE if not filehistory else filehistory 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("", self.up, add=True) self.text.bind("", self.down, add=True) - self.text.bind("", self.loop, add=True) + self.text.bind("", self.execute, add=True) for bind_str in ("", ""): self.text.bind(bind_str, self.left, add=True) - for bind_str in ("", ""): + for bind_str in ("", "", "", ""): self.text.bind(bind_str, self.check, add=True) - self.text.bind("", self.kill, add=True) # Isn't working - # History recorder - self.history = open( - self.filehistory, - "r+", - encoding="utf-8", - ) + self.text.bind("", self.kill, add=True) # Isn't working - self.historys = [i.strip() for i in self.history.readlines() if i.strip()] - self.historyindex = len(self.historys) - 1 + del horizontal def check(self, _: Event) -> None: - """Update cursor""" - self.cursor = self.text.index("insert") + """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): - self.text.bind("", self.ignore, True) - self.text.bind("", self.ignore, True) - elif float(self.cursor) >= float(self.latest): - self.text.unbind("") - self.text.unbind("") + for bind_str in ("", ""): + self.text.bind(bind_str, lambda _: "break", add=True) + else: + for unbind_str in ("", ""): + self.text.unbind(unbind_str) def directory(self) -> None: """Insert the directory""" self.text.insert("insert", getcwd() + SIGN) - def newline(self) -> None: - """Insert a newline""" - self.text.insert("insert", "\n") - self.index += 1 - - def ignore(self, _: Event) -> str: - """Ignore the event""" - 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].strip()) - self.historyindex -= 1 - return "break" - - 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].strip()) - 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: - """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: - return "break" - def kill(self, _: Event) -> str: """Kill the current process""" if self.current_process: - self.current_process.kill() + self.current_process.terminate() self.current_process = None return "break" - def update(self) -> str: - """Update or the command has no output""" - self.directory() - self.check(None) - self.latest = self.text.index("insert") - self.text.see("end") - return "break" - - def loop(self, _: Event) -> str: - """Create an input loop""" - # Get the command from the text - cmd = self.text.get(f"{self.index}.0", "end-1c") + def execute(self, _: Event) -> str: + """Execute the command""" + # Get the line from the text + cmd: str = self.text.get(f"{self.index}.0", "end-1c") + # Split the command from the line also strip cmd = cmd.split(SIGN)[-1].strip() - if self.longflag: - self.longcmd += cmd - cmd = self.longcmd - self.longcmd = "" - self.longflag = False - + # Special check 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) @@ -263,14 +203,24 @@ def loop(self, _: Event) -> str: if cmd in ["clear", "cls"]: self.text.delete("1.0", "end") self.directory() + self.index = 1 return "break" elif cmd == "exit": self.master.quit() + elif cmd.startswith("cd"): # TAG: is all platform use cd...? + # It will raise OSError instead of output a normal error + # TODO: fix it + if cmd == "cd..": + chdir(path.abspath(path.join(getcwd(), ".."))) + else: + chdir(cmd.split()[-1]) + self.newline() + self.directory() + return "break" - # Check that the insert position is at the end - if self.text.index("insert") != f"{self.index}.end": - self.text.mark_set("insert", f"{self.index}.end") - self.text.see("insert") + # 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 @@ -281,68 +231,105 @@ def loop(self, _: Event) -> str: stderr=PIPE, stdin=PIPE, text=True, - bufsize=1, - universal_newlines=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 - output: tuple = self.current_process.communicate() - returnlines: str = output[0] - errors: str = output[1] + 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) - if line == "\n": - self.index += 1 # Update the text and the index + self.index = int(self.text.index("insert").split(".")[0]) self.update() 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""" + # Make a newline + self.newline() + # 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 - # Create root window root = Tk() - - # Hide root window during initialization root.withdraw() - - # Set title root.title("Terminal") - # Create terminal term = Terminal(root) term.pack(expand=True, fill="both") - # Set minimum size and center app - - # Update widgets so minimum size is accurate root.update_idletasks() - # Set the minimum size minimum_width: int = root.winfo_reqwidth() minimum_height: int = root.winfo_reqheight() - # Get center of screen based on minimum size x_coords = int(root.winfo_screenwidth() / 2 - minimum_width / 2) y_coords = int(root.wm_maxsize()[1] / 2 - minimum_height / 2) - # Place app and make the minimum size the actual minimum size (non-infringable) + root.geometry(f"{minimum_width}x{minimum_height}+{x_coords}+{y_coords}") root.wm_minsize(minimum_width, minimum_height) - # Show root window root.deiconify() - - # Start mainloop root.mainloop()