An expressive GUI framework for compatible terminals
Explore the docs »
Getting Started
·
Usage
·
Documentation
·
API
·
License
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.
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
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.
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.
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()
...
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
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
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()
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()
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 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.
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()
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
Details of the XtermGUI API are listed below for quick reference.
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 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 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 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)
Distributed under the GNU General Public License v3.0. See LICENSE for further details.