Skip to content

API Reference

Terminals

Bases: Frame

Mono's tabbed terminal manager

This widget is a container for multiple terminal instances. It provides methods to create, delete, and manage terminal instances. It also provides methods to run commands in the active terminal and switch between terminals.

Parameters:

Name Type Description Default
master Tk

Main window.

required
cwd str

Working directory.

None
theme Theme

Custom theme instance.

None
Source code in mono\__init__.py
class Terminals(tk.Frame):
    """Mono's tabbed terminal manager

    This widget is a container for multiple terminal instances. It provides
    methods to create, delete, and manage terminal instances. It also provides
    methods to run commands in the active terminal and switch between terminals.

    Args:
        master (tk.Tk): Main window.
        cwd (str): Working directory.
        theme (Theme): Custom theme instance."""

    def __init__(
        self, master, cwd: str = None, theme: Theme = None, *args, **kwargs
    ) -> None:
        super().__init__(master, *args, **kwargs)
        self.master = master
        self.base = self

        self.theme = theme or Theme()
        self.styles = Styles(self, self.theme)

        self.config(bg=self.theme.border)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        self.grid_propagate(False)

        self.cwd = cwd

        self.tabs = Tabs(self)
        self.tabs.grid(row=0, column=1, padx=(1, 0), sticky=tk.NS)

        self.active_terminals = []

    def add_default_terminal(self, *_) -> Default:
        """Add a default terminal to the list. Create a tab for it.

        Returns:
            Default: Default terminal instance."""

        default_terminal = Default(
            self, cwd=self.cwd or get_home_directory(), standalone=False
        )
        self.add_terminal(default_terminal)
        return default_terminal

    def add_terminals(self, terminals) -> None:
        """Append multiple terminals to list. Create tabs for them.

        Args:
            terminals (list): List of Shell types to append."""

        for terminal in terminals:
            self.add_terminal(terminal)

    def add_terminal(self, terminal: Terminal) -> None:
        """Append terminal to list. Create tab for it.

        Args:
            terminal (Terminal): Shell type to append."""

        self.active_terminals.append(terminal)
        self.tabs.add_tab(terminal)

    def set_cwd(self, cwd: str) -> None:
        """Set current working directory for all terminals.

        Args:
            cwd (str): Directory path."""

        self.cwd = cwd

    def open_shell(self, shell: Terminal) -> None:
        """Creates an instance and opens a shell.

        Args:
            shell (Terminal): Shell type to open (not instance)
                use add_terminal() to add existing instance."""

        self.add_terminal(
            shell(self, cwd=self.cwd or get_home_directory(), standalone=False)
        )

    def open_another_terminal(self, cwd: str = None) -> None:
        """Opens another instance of the active terminal.

        Args:
            cwd (str): Directory path."""

        self.add_terminal(
            self.active_terminal_type(
                self, cwd=cwd or self.cwd or get_home_directory(), standalone=False
            )
        )

    def delete_all_terminals(self, *_) -> None:
        """Permanently delete all terminal instances."""

        for terminal in self.active_terminals:
            terminal.destroy()

        self.tabs.clear_all_tabs()
        self.active_terminals.clear()
        self.refresh()

    def delete_terminal(self, terminal: Terminal) -> None:
        """Permanently delete a terminal instance.

        Args:
            terminal (Terminal): Terminal instance to delete."""

        terminal.destroy()
        self.active_terminals.remove(terminal)

    def delete_active_terminal(self, *_) -> None:
        """Permanently delete the active terminal."""

        try:
            self.tabs.close_active_tab()
        except IndexError:
            pass

    def set_active_terminal(self, terminal: Terminal) -> None:
        """Switch tabs to the terminal.

        Args:
            terminal (Terminal): Terminal instance to switch to."""

        for tab in self.tabs.tabs:
            if tab.terminal == terminal:
                self.tabs.set_active_tab(tab)

    def set_active_terminal_by_name(self, name: str) -> None:
        """Switch tabs to the terminal by name.

        Args:
            name (str): Name of the terminal to switch to."""

        for tab in self.tabs.tabs:
            if tab.terminal.name == name:
                self.tabs.set_active_tab(tab)
                break

    def clear_terminal(self, *_) -> None:
        """Clear text in the active terminal."""

        if active := self.active_terminal:
            active.clear()

    def run_command(self, command: str) -> None:
        """Run a command in the active terminal. If there is no active terminal,
        create a default terminal and run the command.

        Args:
            command (str): Command to run."""

        if not self.active_terminal:
            default = self.add_default_terminal()
            default.run_command(command)
            # this won't work, TODO: implement a queue for commands
        else:
            self.active_terminal.run_command(command)

    @staticmethod
    def run_in_external_console(self, command: str) -> None:
        """Run a command in an external console.

        Args:
            command (str): Command to run."""

        match platform.system():
            case "Windows":
                subprocess.Popen(["start", "cmd", "/K", command], shell=True)
            case "Linux":
                subprocess.Popen(["x-terminal-emulator", "-e", command])
            case "Darwin":
                subprocess.Popen(["open", "-a", "Terminal", command])
            case _:
                print("No terminal emulator detected.")

    def open_pwsh(self, *_):
        """Create a Powershell terminal instance and open it"""

        self.open_shell(get_shell_from_name("Powershell"))

    def open_cmd(self, *_):
        """Create a Command Prompt terminal instance and open it"""

        self.open_shell(get_shell_from_name("Command Prompt"))

    def open_bash(self, *_):
        """Create a Bash terminal instance and open it"""

        self.open_shell(get_shell_from_name("Bash"))

    def open_python(self, *_):
        """Create a Python terminal instance and open it"""

        self.open_shell(get_shell_from_name("Python"))

    @property
    def active_terminal_type(self) -> Terminal:
        """Get the type of the active terminal. If there is no active
        terminal, return Default type."""

        if active := self.active_terminal:
            return type(active)

        return Default

    @property
    def active_terminal(self) -> Terminal:
        """Get the active terminal instance."""

        if not self.tabs.active_tab:
            return

        return self.tabs.active_tab.terminal

    def refresh(self, *_) -> None:
        """Generates <<Empty>> event that can be bound to hide the terminal
        if there are no active terminals."""

        if not self.active_terminals:
            self.event_generate("<<Empty>>", when="tail")

active_terminal: Terminal property

Get the active terminal instance.

active_terminal_type: Terminal property

Get the type of the active terminal. If there is no active terminal, return Default type.

add_default_terminal(*_)

Add a default terminal to the list. Create a tab for it.

Returns:

Name Type Description
Default Default

Default terminal instance.

Source code in mono\__init__.py
def add_default_terminal(self, *_) -> Default:
    """Add a default terminal to the list. Create a tab for it.

    Returns:
        Default: Default terminal instance."""

    default_terminal = Default(
        self, cwd=self.cwd or get_home_directory(), standalone=False
    )
    self.add_terminal(default_terminal)
    return default_terminal

add_terminal(terminal)

Append terminal to list. Create tab for it.

Parameters:

Name Type Description Default
terminal Terminal

Shell type to append.

required
Source code in mono\__init__.py
def add_terminal(self, terminal: Terminal) -> None:
    """Append terminal to list. Create tab for it.

    Args:
        terminal (Terminal): Shell type to append."""

    self.active_terminals.append(terminal)
    self.tabs.add_tab(terminal)

add_terminals(terminals)

Append multiple terminals to list. Create tabs for them.

Parameters:

Name Type Description Default
terminals list

List of Shell types to append.

required
Source code in mono\__init__.py
def add_terminals(self, terminals) -> None:
    """Append multiple terminals to list. Create tabs for them.

    Args:
        terminals (list): List of Shell types to append."""

    for terminal in terminals:
        self.add_terminal(terminal)

clear_terminal(*_)

Clear text in the active terminal.

Source code in mono\__init__.py
def clear_terminal(self, *_) -> None:
    """Clear text in the active terminal."""

    if active := self.active_terminal:
        active.clear()

delete_active_terminal(*_)

Permanently delete the active terminal.

Source code in mono\__init__.py
def delete_active_terminal(self, *_) -> None:
    """Permanently delete the active terminal."""

    try:
        self.tabs.close_active_tab()
    except IndexError:
        pass

delete_all_terminals(*_)

Permanently delete all terminal instances.

Source code in mono\__init__.py
def delete_all_terminals(self, *_) -> None:
    """Permanently delete all terminal instances."""

    for terminal in self.active_terminals:
        terminal.destroy()

    self.tabs.clear_all_tabs()
    self.active_terminals.clear()
    self.refresh()

delete_terminal(terminal)

Permanently delete a terminal instance.

Parameters:

Name Type Description Default
terminal Terminal

Terminal instance to delete.

required
Source code in mono\__init__.py
def delete_terminal(self, terminal: Terminal) -> None:
    """Permanently delete a terminal instance.

    Args:
        terminal (Terminal): Terminal instance to delete."""

    terminal.destroy()
    self.active_terminals.remove(terminal)

open_another_terminal(cwd=None)

Opens another instance of the active terminal.

Parameters:

Name Type Description Default
cwd str

Directory path.

None
Source code in mono\__init__.py
def open_another_terminal(self, cwd: str = None) -> None:
    """Opens another instance of the active terminal.

    Args:
        cwd (str): Directory path."""

    self.add_terminal(
        self.active_terminal_type(
            self, cwd=cwd or self.cwd or get_home_directory(), standalone=False
        )
    )

open_bash(*_)

Create a Bash terminal instance and open it

Source code in mono\__init__.py
def open_bash(self, *_):
    """Create a Bash terminal instance and open it"""

    self.open_shell(get_shell_from_name("Bash"))

open_cmd(*_)

Create a Command Prompt terminal instance and open it

Source code in mono\__init__.py
def open_cmd(self, *_):
    """Create a Command Prompt terminal instance and open it"""

    self.open_shell(get_shell_from_name("Command Prompt"))

open_pwsh(*_)

Create a Powershell terminal instance and open it

Source code in mono\__init__.py
def open_pwsh(self, *_):
    """Create a Powershell terminal instance and open it"""

    self.open_shell(get_shell_from_name("Powershell"))

open_python(*_)

Create a Python terminal instance and open it

Source code in mono\__init__.py
def open_python(self, *_):
    """Create a Python terminal instance and open it"""

    self.open_shell(get_shell_from_name("Python"))

open_shell(shell)

Creates an instance and opens a shell.

Parameters:

Name Type Description Default
shell Terminal

Shell type to open (not instance) use add_terminal() to add existing instance.

required
Source code in mono\__init__.py
def open_shell(self, shell: Terminal) -> None:
    """Creates an instance and opens a shell.

    Args:
        shell (Terminal): Shell type to open (not instance)
            use add_terminal() to add existing instance."""

    self.add_terminal(
        shell(self, cwd=self.cwd or get_home_directory(), standalone=False)
    )

refresh(*_)

Generates <> event that can be bound to hide the terminal if there are no active terminals.

Source code in mono\__init__.py
def refresh(self, *_) -> None:
    """Generates <<Empty>> event that can be bound to hide the terminal
    if there are no active terminals."""

    if not self.active_terminals:
        self.event_generate("<<Empty>>", when="tail")

run_command(command)

Run a command in the active terminal. If there is no active terminal, create a default terminal and run the command.

Parameters:

Name Type Description Default
command str

Command to run.

required
Source code in mono\__init__.py
def run_command(self, command: str) -> None:
    """Run a command in the active terminal. If there is no active terminal,
    create a default terminal and run the command.

    Args:
        command (str): Command to run."""

    if not self.active_terminal:
        default = self.add_default_terminal()
        default.run_command(command)
        # this won't work, TODO: implement a queue for commands
    else:
        self.active_terminal.run_command(command)

run_in_external_console(command) staticmethod

Run a command in an external console.

Parameters:

Name Type Description Default
command str

Command to run.

required
Source code in mono\__init__.py
@staticmethod
def run_in_external_console(self, command: str) -> None:
    """Run a command in an external console.

    Args:
        command (str): Command to run."""

    match platform.system():
        case "Windows":
            subprocess.Popen(["start", "cmd", "/K", command], shell=True)
        case "Linux":
            subprocess.Popen(["x-terminal-emulator", "-e", command])
        case "Darwin":
            subprocess.Popen(["open", "-a", "Terminal", command])
        case _:
            print("No terminal emulator detected.")

set_active_terminal(terminal)

Switch tabs to the terminal.

Parameters:

Name Type Description Default
terminal Terminal

Terminal instance to switch to.

required
Source code in mono\__init__.py
def set_active_terminal(self, terminal: Terminal) -> None:
    """Switch tabs to the terminal.

    Args:
        terminal (Terminal): Terminal instance to switch to."""

    for tab in self.tabs.tabs:
        if tab.terminal == terminal:
            self.tabs.set_active_tab(tab)

set_active_terminal_by_name(name)

Switch tabs to the terminal by name.

Parameters:

Name Type Description Default
name str

Name of the terminal to switch to.

required
Source code in mono\__init__.py
def set_active_terminal_by_name(self, name: str) -> None:
    """Switch tabs to the terminal by name.

    Args:
        name (str): Name of the terminal to switch to."""

    for tab in self.tabs.tabs:
        if tab.terminal.name == name:
            self.tabs.set_active_tab(tab)
            break

set_cwd(cwd)

Set current working directory for all terminals.

Parameters:

Name Type Description Default
cwd str

Directory path.

required
Source code in mono\__init__.py
def set_cwd(self, cwd: str) -> None:
    """Set current working directory for all terminals.

    Args:
        cwd (str): Directory path."""

    self.cwd = cwd

Shells

get_available_shells()

Return a list of available shells.

Source code in mono\shells\__init__.py
def get_available_shells() -> dict[str, Terminal]:
    """Return a list of available shells."""

    return SHELLS

get_shell_from_name(name)

Return the shell class from the name.

If the shell is not found, return the default shell for the platform.

Parameters:

Name Type Description Default
name str

The name of the shell to get.

required

Returns:

Name Type Description
Terminal Terminal | Default

The shell class.

Source code in mono\shells\__init__.py
def get_shell_from_name(name: str) -> Terminal | Default:
    """Return the shell class from the name.

    If the shell is not found, return the default shell for the platform.

    Args:
        name (str): The name of the shell to get.

    Returns:
        Terminal: The shell class.
    """

    return SHELLS.get(name, Default)

is_shell_registered(name)

Check if a shell is registered.

Parameters:

Name Type Description Default
name str

The name of the shell.

required

Returns:

Name Type Description
bool bool

True if the shell is registered, False otherwise.

Source code in mono\shells\__init__.py
def is_shell_registered(name: str) -> bool:
    """Check if a shell is registered.

    Args:
        name (str): The name of the shell.

    Returns:
        bool: True if the shell is registered, False otherwise.
    """

    return name in SHELLS

register_shell(name, shell)

Register a new shell.

Parameters:

Name Type Description Default
name str

The name of the shell.

required
shell Terminal

The shell class.

required
Source code in mono\shells\__init__.py
def register_shell(name: str, shell: Terminal) -> None:
    """Register a new shell.

    Args:
        name (str): The name of the shell.
        shell (Terminal): The shell class.
    """

    SHELLS[name] = shell

Terminal

Bases: Frame

Terminal abstract class. All shell types should inherit from this class.

The inherited class should implement following attributes

name (str): Name of the terminal. shell (str): command / path to shell executable.

Parameters:

Name Type Description Default
master Tk

Main window.

required
cwd str

Working directory.

'.'
Source code in mono\terminal.py
class Terminal(ttk.Frame):
    """Terminal abstract class. All shell types should inherit from this class.

    The inherited class should implement following attributes:
        name (str): Name of the terminal.
        shell (str): command / path to shell executable.

    Args:
        master (tk.Tk): Main window.
        cwd (str): Working directory."""

    name: str
    shell: str

    def __init__(
        self, master, cwd=".", theme: Theme = None, standalone=True, *args, **kwargs
    ) -> None:
        super().__init__(master, *args, **kwargs)
        self.master = master
        self.standalone = standalone
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        self.alive = False
        self.cwd = cwd
        self.p = None

        if self.standalone:
            self.base = self

            from .styles import Styles
            from .theme import Theme

            self.theme = theme or Theme()
            self.style = Styles(self, self.theme)
        else:
            self.base = master.base
            self.theme = self.base.theme

        self.text = TerminalText(
            self, relief=tk.FLAT, padx=10, pady=10, font=("Consolas", 11)
        )
        self.text.config(
            bg=self.theme.terminal[0],
            fg=self.theme.terminal[1],
            insertbackground=self.theme.terminal[1],
        )
        self.text.grid(row=0, column=0, sticky=tk.NSEW)
        self.text.bind("<Return>", self.enter)

        self.terminal_scrollbar = Scrollbar(self, style="MonoScrollbar")
        self.terminal_scrollbar.grid(row=0, column=1, sticky="NSW")

        self.text.config(yscrollcommand=self.terminal_scrollbar.set)
        self.terminal_scrollbar.config(command=self.text.yview, orient=tk.VERTICAL)

        self.text.tag_config("prompt", foreground="orange")
        self.text.tag_config("command", foreground="yellow")

        self.bind("<Destroy>", self.stop_service)

    def check_shell(self):
        """Check if the shell is available in the system path."""

        import shutil

        self.shell = shutil.which(self.shell)
        return self.shell

    def start_service(self, *_) -> None:
        """Start the terminal service."""

        self.alive = True
        self.last_command = None

        self.p = PTY.spawn([self.shell])
        Thread(target=self._write_loop, daemon=True).start()

    def stop_service(self, *_) -> None:
        """Stop the terminal service."""

        self.alive = False

    def run_command(self, command: str) -> None:
        """Run a command in the terminal.
        TODO: Implement a queue for running multiple commands."""

        self.text.insert("end", command, "command")
        self.enter()

    def enter(self, *_) -> None:
        """Enter key event handler for running commands."""

        command = self.text.get("input", "end")
        self.last_command = command
        self.text.register_history(command)
        if command.strip():
            self.text.delete("input", "end")

        self.p.write(command + "\r\n")
        return "break"

    def _write_loop(self) -> None:
        while self.alive:
            if buf := self.p.read():
                p = buf.find("\x1b]0;")

                if p != -1:
                    buf = buf[:p]
                buf = [
                    strip_ansi_escape_sequences(i)
                    for i in replace_newline(buf).splitlines()
                ]
                self._insert("\n".join(buf))

    def _insert(self, output: str, tag="") -> None:
        self.text.insert(tk.END, output, tag)
        # self.terminal.tag_add("prompt", "insert linestart", "insert")
        self.text.see(tk.END)
        self.text.mark_set("input", "insert")

    def _newline(self):
        self._insert("\n")

    def clear(self) -> None:
        """Clear the terminal."""

        self.text.clear()

    # TODO: Implement a better way to handle key events.
    def _ctrl_key(self, key: str) -> None:
        if key == "c":
            self.run_command("\x03")

    def __str__(self) -> str:
        return self.name

__init__(master, cwd='.', theme=None, standalone=True, *args, **kwargs)

Source code in mono\terminal.py
def __init__(
    self, master, cwd=".", theme: Theme = None, standalone=True, *args, **kwargs
) -> None:
    super().__init__(master, *args, **kwargs)
    self.master = master
    self.standalone = standalone
    self.grid_columnconfigure(0, weight=1)
    self.grid_rowconfigure(0, weight=1)

    self.alive = False
    self.cwd = cwd
    self.p = None

    if self.standalone:
        self.base = self

        from .styles import Styles
        from .theme import Theme

        self.theme = theme or Theme()
        self.style = Styles(self, self.theme)
    else:
        self.base = master.base
        self.theme = self.base.theme

    self.text = TerminalText(
        self, relief=tk.FLAT, padx=10, pady=10, font=("Consolas", 11)
    )
    self.text.config(
        bg=self.theme.terminal[0],
        fg=self.theme.terminal[1],
        insertbackground=self.theme.terminal[1],
    )
    self.text.grid(row=0, column=0, sticky=tk.NSEW)
    self.text.bind("<Return>", self.enter)

    self.terminal_scrollbar = Scrollbar(self, style="MonoScrollbar")
    self.terminal_scrollbar.grid(row=0, column=1, sticky="NSW")

    self.text.config(yscrollcommand=self.terminal_scrollbar.set)
    self.terminal_scrollbar.config(command=self.text.yview, orient=tk.VERTICAL)

    self.text.tag_config("prompt", foreground="orange")
    self.text.tag_config("command", foreground="yellow")

    self.bind("<Destroy>", self.stop_service)

check_shell()

Check if the shell is available in the system path.

Source code in mono\terminal.py
def check_shell(self):
    """Check if the shell is available in the system path."""

    import shutil

    self.shell = shutil.which(self.shell)
    return self.shell

clear()

Clear the terminal.

Source code in mono\terminal.py
def clear(self) -> None:
    """Clear the terminal."""

    self.text.clear()

enter(*_)

Enter key event handler for running commands.

Source code in mono\terminal.py
def enter(self, *_) -> None:
    """Enter key event handler for running commands."""

    command = self.text.get("input", "end")
    self.last_command = command
    self.text.register_history(command)
    if command.strip():
        self.text.delete("input", "end")

    self.p.write(command + "\r\n")
    return "break"

run_command(command)

Run a command in the terminal. TODO: Implement a queue for running multiple commands.

Source code in mono\terminal.py
def run_command(self, command: str) -> None:
    """Run a command in the terminal.
    TODO: Implement a queue for running multiple commands."""

    self.text.insert("end", command, "command")
    self.enter()

start_service(*_)

Start the terminal service.

Source code in mono\terminal.py
def start_service(self, *_) -> None:
    """Start the terminal service."""

    self.alive = True
    self.last_command = None

    self.p = PTY.spawn([self.shell])
    Thread(target=self._write_loop, daemon=True).start()

stop_service(*_)

Stop the terminal service.

Source code in mono\terminal.py
def stop_service(self, *_) -> None:
    """Stop the terminal service."""

    self.alive = False

Theme

Theme

Color theme for the terminal.

Attributes:

Name Type Description
bg str

Background color.

fg str

Foreground color.

abg str

Active background color.

afg str

Active foreground color.

border str

Border color.

tabbar str

Tab bar background color. This can be modified only after initialization.

tab tuple

Tab color scheme. This can be modified only after initialization.

tabs tuple

Tabs color scheme. This can be modified only after initialization.

tab_active tuple

Active tab color scheme. This can be modified only after initialization.

terminal tuple

Terminal color scheme. This can be modified only after initialization.

scrollbar tuple

Scrollbar color scheme. This can be modified only after initialization.

Source code in mono\theme.py
class Theme:
    """Color theme for the terminal.

    Attributes:
        bg (str): Background color.
        fg (str): Foreground color.
        abg (str): Active background color.
        afg (str): Active foreground color.
        border (str): Border color.
        tabbar (str): Tab bar background color. This can be modified only after initialization.
        tab (tuple): Tab color scheme. This can be modified only after initialization.
        tabs (tuple): Tabs color scheme. This can be modified only after initialization.
        tab_active (tuple): Active tab color scheme. This can be modified only after initialization.
        terminal (tuple): Terminal color scheme. This can be modified only after initialization.
        scrollbar (tuple): Scrollbar color scheme. This can be modified only after initialization.
    """

    bg = "#181818"
    fg = "#8B949E"
    abg = "#2C2D2D"
    afg = "#CCCCCC"
    border = "#2A2A2A"

    def __init__(self) -> None:
        self.tabbar = self.bg
        self.tab = (self.bg, self.fg, self.abg, self.afg)
        self.tabs = (self.bg, self.fg)
        self.tab_active = (self.abg, self.afg)
        self.terminal = (self.bg, self.fg)
        self.scrollbar = (self.bg, self.abg)