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