Skip to content

An expressive GUI framework for compatible terminals

License

Notifications You must be signed in to change notification settings

Kieran-Lock/XtermGUI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


Logo

An expressive GUI framework for compatible terminals
Explore the docs »

Getting Started · Usage · Documentation · API · License

About the Project

XtermGUI is a lightweight GUI framework for compatible Xterm terminals, allowing you to write complex terminal applications, with expressive, concise, and readable code.

With zero external dependencies, and features including keyboard and mouse input, customizable event listeners, and multiple layer management, you can create and manage complex terminal GUIs with minimal overhead, all with a convenient Python API.

Getting Started

XtermGUI is available on PyPI. To use it in your project, run:

pip install XtermGUI

To install specific previous versions, take a look at the version history, locate the version tag (vX.Y.Z), and run:

pip install Xterm==X.Y.Z

Trouble Shooting

XtermGUI uses Xterm-specific control sequences. If XtermGUI is running incorrectly once installed, ensure that the terminal you are using has sufficient mouse-reporting and Xterm support support.

This framework has been tested using the default setup for WSL 2 on Windows 10, which uses an Xterm-256color terminal by default.

Here is a useful resource for Xterm control sequences reference.

Basic Usage

Setting up the terminal

Use the console_inputs context manager to prepare the terminal for use with XtermGUI. This will handle the cleanup automatically.

from xtermgui import console_inputs


with console_inputs():
    ...

Please note that there may sometimes be control sequence leakage when exiting your application. This is unavoidable (I am yet to find a solution), but will only happen once the application is exited, and for only very short periods of time.

Reading console input

Use the read_console function to read both keyboard and mouse input from the console. View the API summary or documentation for the possible events you can receieve from this function.

from xtermgui import console_inputs, read_console


with console_inputs():
    read_key = read_console()
    ...

Read repeated console input by placing this function in a loop. This can be useful for reading a stream of user inputs.

from xtermgui import console_inputs, read_console


with console_inputs():
    while True:  # Or some other loop
        read_key = read_console()
        ...

Text Formatting

The Text class can be used to represent formatted text, which can be printed to the console. It provides all the same functionality as the built-in str string type, but can be used in conjunction with the Style and Colour classes to represent coloured and styled text.

from xtermgui import Colours, Styles, Text, Colour, RGBs


Colour.configure_default_background(RGBs.DEFAULT_BACKGROUND_WSL.value)  # Test written in WSL, so the background is configured like so


text = "This is styled text."
text_colour = Colours.F_BLUE.value.blend(Colours.F_GREY.value, foreground_bias=0.5, foreground_gamma=2.2)  # This blending just uses the default blending parameters
text_background_colour = Colours.B_BLACK.value
text_style = Styles.BOLD.value + Styles.UNDERLINED.value

text = Text(text, colour=text_colour, style=text_style)
text.add_colour(text_background_colour)

print(text)  # Prints the text string, with a silver-blue foreground, black background, underlined and in bold

Complex Usage

For more complex use cases, it is best to organise your application using the GUI or LayeredGUI classes. Some of the benefits of this approach are:

  • Easier & Customizable Event Listeners
  • Layer Management
  • Simplified GUI Manipulation

Creating and configuring a GUI

You can create a simple GUI by inheriting from GUI.

from xtermgui import GUI


class MyGUI(GUI):
    def __init__(self) -> None:
        super().__init__()

You can use this GUI by using the start method as a context manager.

from xtermgui import GUI


class MyGUI(GUI):
    def __init__(self) -> None:
        super().__init__()


def main() -> None:
    gui = MyGUI()

    with gui.start():
        ...


if __name__ == "__main__":
    main()

Interactions

Receiving input events, via interactions, is the default behaviour of GUI objects in XtermGUI. When enabled, it handles the input thread automatically, such that all events you receive will be called in a separate thread.

Opt-out of input events by passing inputs=False to the start method.

from xtermgui import GUI


class MyGUI(GUI):
    def __init__(self) -> None:
        super().__init__()


def main() -> None:
    gui = MyGUI()

    with gui.start():  # inputs=True is default: opt-out with inputs=False
        ...


if __name__ == "__main__":
    main()

Keyboard Interactions

Receive keyboard events with the KeyboardInteraction decorator.

from xtermgui import GUI, KeyboardInteraction, Events, KeyboardEvent


class MyGUI(GUI):
    def __init__(self) -> None:
        super().__init__()

    @KeyboardInteraction(Events.SPACE.value)
    def clicked_space(self, event: KeyboardEvent) -> None:
        ...

The clicked_space method runs when space is pressed. Reference the API for other keyboard events you can receive.

Mouse Interactions

Mouse events can be dealt with similarly, with the MouseInteraction decorator.

from xtermgui import GUI, MouseInteraction, Events, MouseEvent, Region, Coordinate


INTERACTION_REGION = Region(Coordinate(0, 0), Coordinate(20, 0), Coordinate(20, 10), Coordinate(0, 10))  # An example region - a 20x10 rectangle, which forms a square in the terminal


class MyGUI(GUI):
    def __init__(self) -> None:
        super().__init__()

    @MouseInteraction(Events.LEFT_MOUSE_DOWN.value, region=INTERACTION_REGION)  # Runs when left mouse button is pressed within the specified region
    def left_mouse_down(self, event: MouseEvent) -> None:
        ...

Mouse interactions require an Event, and take a Region as an optional argument. If region is omitted, the event will fire at any position.

GUI I/O Operations

The GUI class provides three key I/O methods - print, erase, and clear - each of which are show below.

from xtermgui import GUI, Coordinate


class MyGUI(GUI):
    def __init__(self) -> None:
        super().__init__()


def main() -> None:
    gui = MyGUI()
    text = "This text will be printed in the console."
    coordinates = Coordinate(10, 5)

    with gui.start():
        gui.print(text, at=coordinates)  # Print provides all the same functionality as the built-in print function
        gui.erase(at=coordinates)
        gui.clear()


if __name__ == "__main__":
    main()

Managing Layers

To manage GUI layers in your application, use the LayeredGUI class. This will provide all of the same I/O methods as the simple GUI class, but manages layers automatically.

from xtermgui import Colour, RGBs, LayeredGUI, Coordinate


Colour.configure_default_background(RGBs.DEFAULT_BACKGROUND_WSL.value)


class MyGUI(LayeredGUI):
    def __init__(self) -> None:
        super().__init__()  # self.base_layer is created automatically
        self.second_layer = self.add_layer("Layer Name", z=1)  # z-index is the same as that of the existing layer with the greatest z-index by default


def main() -> None:
    gui = MyGUI()
    text_base_layer = "This text will be printed in the console, on the base layer."
    text_1_second_layer = "This text will be printed in the console, on the second layer."
    text_2_second_layer = "This text will also be printed in the console, on the second layer."
    coordinates = Coordinate(10, 5)

    with gui.start():
        gui.print(text_base_layer, at=coordinates)  # Prints on the active layer by default - this is initially the base layer
        gui.print(text_1_second_layer, at=coordinates, layer=gui.second_layer)  # Prints over the text on the base layer
        with gui.as_active(gui.second_layer):  # Second layer is set as active within this scope only
            gui.print(text_2_second_layer, at=coordinates)  # Overwrites the existing text
        gui.clear(layer=gui.second_layer)  # Only the content printed to the base layer now shows


if __name__ == "__main__":
    main()

Methods on the Layer class should not be used directly - only interact with layered GUIs via the LayeredGUI class methods.

For more examples, functionality, and detail, please refer to the Documentation

API Summary

Details of the XtermGUI API are listed below for quick reference.

Input Events

Input events can be received via the GUI API.

from enum import Enum
from xtermgui import Event, KeyboardEvent, MouseEvent


class Events(Enum):
    SHIFT_BACKSPACE: Event = Event("SHIFT_BACKSPACE")
    TAB: Event = Event("TAB")
    ENTER: Event = Event("ENTER")
    BACKSPACE: Event = Event("BACKSPACE")
    POUND: Event = Event("POUND")

    UP_ARROW: Event = Event("UP_ARROW")
    DOWN_ARROW: Event = Event("DOWN_ARROW")
    RIGHT_ARROW: Event = Event("RIGHT_ARROW")
    LEFT_ARROW: Event = Event("LEFT_ARROW")
    END: Event = Event("END")
    HOME: Event = Event("HOME")
    SHIFT_TAB: Event = Event("SHIFT_TAB")

    LEFT_MOUSE_UP: Event = Event("LEFT_MOUSE_UP")
    LEFT_MOUSE_DOWN: Event = Event("LEFT_MOUSE_DOWN")
    MIDDLE_MOUSE_UP: Event = Event("MIDDLE_MOUSE_UP")
    MIDDLE_MOUSE_DOWN: Event = Event("MIDDLE_MOUSE_DOWN")
    RIGHT_MOUSE_UP: Event = Event("RIGHT_MOUSE_UP")
    RIGHT_MOUSE_DOWN: Event = Event("RIGHT_MOUSE_DOWN")
    LEFT_MOUSE_DRAG: Event = Event("LEFT_MOUSE_DRAG")
    MIDDLE_MOUSE_DRAG: Event = Event("MIDDLE_MOUSE_DRAG")
    RIGHT_MOUSE_DRAG: Event = Event("RIGHT_MOUSE_DRAG")
    MOVE: Event = Event("MOVE")
    SCROLL_UP: Event = Event("SCROLL_UP")
    SCROLL_DOWN: Event = Event("SCROLL_DOWN")

    INSERT: Event = Event("INSERT")
    DELETE: Event = Event("DELETE")
    PAGE_UP: Event = Event("PAGE_UP")
    PAGE_DOWN: Event = Event("PAGE_DOWN")
    F5: Event = Event("F5")
    F6: Event = Event("F6")
    F7: Event = Event("F7")
    F8: Event = Event("F8")
    F9: Event = Event("F9")
    F10: Event = Event("F10")
    F11: Event = Event("F11")
    F12: Event = Event("F12")

    F1: Event = Event("F1")
    F2: Event = Event("F2")
    F3: Event = Event("F3")
    F4: Event = Event("F4")
    SPACE: Event = Event(" ")
    FORWARD_SLASH: Event = Event("/")
    BACKSLASH: Event = Event("\\")
    VERTICAL_BAR: Event = Event("|")
    PIPE: Event = Event("|")
    EXCLAMATION_MARK: Event = Event("!")
    QUESTION_MARK: Event = Event("?")
    COMMA: Event = Event(",")
    FULL_STOP: Event = Event(".")
    LESS_THAN: Event = Event("<")
    LEFT_ANGLE_BRACKET: Event = Event("<")
    LEFT_CHEVRON: Event = Event("<")
    GREATER_THAN: Event = Event(">")
    RIGHT_ANGLE_BRACKET: Event = Event(">")
    RIGHT_CHEVRON: Event = Event(">")
    SEMI_COLON: Event = Event(";")
    COLON: Event = Event(":")
    APOSTROPHE: Event = Event("'")
    AT: Event = Event("@")
    HASHTAG: Event = Event("#")
    OCTOTHORPE: Event = Event("#")
    TILDE: Event = Event("~")
    LEFT_SQUARE_BRACKET: Event = Event("[")
    RIGHT_SQUARE_BRACKET: Event = Event("]")
    LEFT_CURLY_BRACKET: Event = Event("{")
    LEFT_BRACE: Event = Event("{")
    RIGHT_CURLY_BRACKET: Event = Event("}")
    RIGHT_BRACE: Event = Event("}")
    HYPHEN: Event = Event("-")
    MINUS: Event = Event("-")
    UNDERSCORE: Event = Event("_")
    EQUALS: Event = Event("=")
    PLUS: Event = Event("+")
    BACKTICK: Event = Event("`")
    LOGICAL_NEGATION: Event = Event("¬")
    BROKEN_BAR: Event = Event("¦")
    SPEECH_MARK: Event = Event("\")")
    QUOTATION_MARK: Event = Event("\")")
    DOLLAR: Event = Event("$")
    PERCENT: Event = Event("%")
    CARET: Event = Event("^")
    AMPERSAND: Event = Event("&")
    ASTERISK: Event = Event("*")
    LEFT_BRACKET: Event = Event("(")
    LEFT_PARENTHESIS: Event = Event("(")
    RIGHT_BRACKET: Event = Event(")")
    RIGHT_PARENTHESIS: Event = Event(")")

    ANY_KEYBOARD: Event = Event(KeyboardEvent.ANY, lambda _: True)
    ANY_MOUSE: Event = Event(MouseEvent.ANY, lambda _: True)
    UNRECOGNIZED_KEYBOARD: Event = Event(KeyboardEvent.UNRECOGNIZED)
    UNRECOGNIZED_MOUSE: Event = Event(MouseEvent.UNRECOGNIZED)

Events that do not appear in this enum (such as the letters of the alphabet) can be created with Event("<LETTER>"). For example, Event("A").

RGB Colours

RGB colours represent only information about a specific colour.

from enum import Enum
from xtermgui import RGB


class RGBs(Enum):
    DEFAULT_FOREGROUND = RGB(192, 192, 192)
    DEFAULT_BACKGROUND_PYCHARM = RGB(43, 43, 43)
    DEFAULT_BACKGROUND_REPLIT = RGB(28, 35, 51)
    DEFAULT_BACKGROUND_WSL = RGB(12, 12, 12)
    BLACK = RGB(0, 0, 0)
    WHITE = RGB(255, 255, 255)
    RED = RGB(255, 0, 0)
    GREEN = RGB(0, 255, 0)
    BLUE = RGB(0, 0, 255)
    YELLOW = RGB(255, 255, 0)
    CYAN = RGB(0, 255, 255)
    MAGENTA = RGB(255, 0, 255)
    ORANGE = RGB(255, 165, 0)
    PURPLE = RGB(230, 230, 250)
    GREY = RGB(142, 142, 142)
    BROWN = RGB(162, 162, 42)

Text Colours

Text colours represent the colour of text printed to the console.

from enum import Enum
from xtermgui import Colour, RGBs



class Colours(Enum):
    F_BLACK: Colour = Colour(foreground=RGBs.BLACK.value)
    F_WHITE: Colour = Colour(foreground=RGBs.WHITE.value)
    F_RED: Colour = Colour(foreground=RGBs.RED.value)
    F_GREEN: Colour = Colour(foreground=RGBs.GREEN.value)
    F_BLUE: Colour = Colour(foreground=RGBs.BLUE.value)
    F_YELLOW: Colour = Colour(foreground=RGBs.YELLOW.value)
    F_CYAN: Colour = Colour(foreground=RGBs.CYAN.value)
    F_MAGENTA: Colour = Colour(foreground=RGBs.MAGENTA.value)
    F_ORANGE: Colour = Colour(foreground=RGBs.ORANGE.value)
    F_PURPLE: Colour = Colour(foreground=RGBs.PURPLE.value)
    F_GREY: Colour = Colour(foreground=RGBs.GREY.value)
    F_BROWN: Colour = Colour(foreground=RGBs.BROWN.value)
    F_DEFAULT: Colour = Colour(foreground=RGBs.DEFAULT_FOREGROUND.value)

    B_BLACK: Colour = Colour(background=RGBs.BLACK.value)
    B_WHITE: Colour = Colour(background=RGBs.WHITE.value)
    B_RED: Colour = Colour(background=RGBs.RED.value)
    B_GREEN: Colour = Colour(background=RGBs.GREEN.value)
    B_BLUE: Colour = Colour(background=RGBs.BLUE.value)
    B_YELLOW: Colour = Colour(background=RGBs.YELLOW.value)
    B_CYAN: Colour = Colour(background=RGBs.CYAN.value)
    B_MAGENTA: Colour = Colour(background=RGBs.MAGENTA.value)
    B_ORANGE: Colour = Colour(background=RGBs.ORANGE.value)
    B_PURPLE: Colour = Colour(background=RGBs.PURPLE.value)
    B_GREY: Colour = Colour(background=RGBs.GREY.value)
    B_BROWN: Colour = Colour(background=RGBs.BROWN.value)
    B_DEFAULT_PYCHARM: Colour = Colour(background=RGBs.DEFAULT_BACKGROUND_PYCHARM.value)
    B_DEFAULT_REPLIT: Colour = Colour(background=RGBs.DEFAULT_BACKGROUND_REPLIT.value)
    B_DEFAULT_BACKGROUND_WSL = Colour(background=RGBs.DEFAULT_BACKGROUND_WSL.value)

Text Styles

Text styles represent the style of text printed to the console.

from enum import Enum
from xtermgui import Style


class Styles(Enum):
    NOT_STYLED: Style = Style()
    BOLD: Style = Style(bold=True)
    DIMMED: Style = Style(dimmed=True)
    ITALIC: Style = Style(italic=True)
    UNDERLINED: Style = Style(underlined=True)
    HIDDEN: Style = Style(hidden=True)
    CROSSED_OUT: Style = Style(crossed_out=True)

License

Distributed under the GNU General Public License v3.0. See LICENSE for further details.

(Back to Top)