|
|
Subscribe / Log in / New account

Textual: a framework for terminal user interfaces

LWN.net needs you!

Without subscribers, LWN would simply not exist. Please consider signing up for a subscription and helping to keep LWN publishing

April 18, 2023

This article was contributed by Koen Vervloesem

For developers seeking to create applications with terminal user interfaces (TUIs), options have been relatively limited compared to the vast number of graphical user interface (GUI) frameworks available. As a result, many command-line applications reinvent the same user interface elements. Textual aims to remedy this: it's a rapid-application-development framework for Python TUI applications. Offering cross-platform support, Textual incorporates layouts, CSS-like styles, and an expanding collection of widgets.

While colors, interactivity, and all sorts of widgets are standard features in graphical applications, terminal applications are often a bit more basic. And although many terminal applications support the mouse, it's an exception to see scroll bars, clickable links, and buttons. Python developer Will McGugan saw an opportunity there, and started working on Textual in 2021.

Textual supports Linux, macOS, and Windows, and is MIT-licensed. After installing it using Python's package manager pip, running "python -m textual" displays a nice demo of its capabilities, as seen in the image below. A typical Textual application has a header with the application's title or other information, a footer with some shortcut keys for commands, a sidebar that can be toggled, and a main area with various widgets.

[Textual demo]

Those widgets are components of the user interface responsible for generating output for a specific region of the screen. They may respond to events, such as clicking them or typing text in them. Textual comes with two dozen basic widgets, including buttons, checkboxes, text inputs, labels, radio buttons, and even tabs. There are also some widgets for complex data structures, such as a data table, (directory) tree, list view, and Markdown viewer. Developers can also create custom widgets by extending a built-in widget class.

Textual builds upon Rich, a project that McGugan started in 2020. It started out as a simple tool to colorize text in the terminal, but has since grown into a library for rich text and nice formatting in command-line Python applications. Rich is targeted at enhancing the appearance of text output in existing console applications that aren't necessarily interactive. It doesn't offer interactive input handling or a framework for building complete applications. However, Rich renderables, which are components that implement the Rich Console protocol, can be used in Textual applications. For example, this is useful to add complex content such as a tree view in cells of Textual's DataTable, or a table as an element of Textual's OptionList.

Inspired by the web

Before building Rich and Textual, McGugan was a web developer. This shows in Textual's architecture, which heavily borrows from web-development techniques. The design of a Textual application can be done entirely outside of the Python code, by including a file with Cascading Style Sheets (CSS) directives. That way, a Textual application's code purely describes its behavior, while the CSS file defines the layout, colors, size, and borders of various widgets.

For example, here's the Python code for a simple Textual app, based on one of the examples in the Textual Guide:

    from textual.app import App, ComposeResult
    from textual.containers import Container, Horizontal
    from textual.widgets import Header, Footer, Static, Button

    QUESTION = "Do you want to learn about Textual CSS?"

    class ExampleApp(App):
	BINDINGS = [
	    ("d", "toggle_dark", "Toggle dark mode"),
	    ("q", "quit", "Quit"),
	]
	CSS_PATH = "question.css"

	def compose(self) -> ComposeResult:
	    """Place all widgets."""
	    yield Header()
	    yield Footer()
	    yield Container(
		Static(QUESTION, classes="question"),
		Horizontal(
		    Button("Yes", variant="success"),
		    Button("No", variant="error"),
		    classes="buttons",
		),
		id="dialog",
	    )

	def action_toggle_dark(self) -> None:
	    """Toggle dark mode."""
	    self.dark = not self.dark

	def on_button_pressed(self, event: Button.Pressed) -> None:
	    """Exit app with id of the button pressed."""
	    self.exit(event.button.label)

    if __name__ == "__main__":
	app = ExampleApp()
	print(app.run())
[Textual example]

This defines an app with bindings for shortcut keys. The compose() method adds widgets to the application screen: a header, a footer, and a container with some other widgets. When a "d" is pressed on the keyboard, its corresponding action method action_toggle_dark() is called. And when one of the buttons in the dialog is pressed with the mouse, the on_button_pressed() method is called. The action_quit() method to exit the application is already defined in the base class.

The corresponding CSS file looks like this:

    /* The top level dialog */
    #dialog {
	height: 100%;
	margin: 4 8;
	background: $panel;
	color: $text;
	border: tall $background;
	padding: 1 2;
    }

    /* The button class */
    Button {
	width: 1fr;
    }

    /* Matches the question text */
    .question {
	text-style: bold;
	height: 100%;
	content-align: center middle;
    }

    /* Matches the button container */
    .buttons {
	width: 100%;
	height: auto;
	dock: bottom;
    }

The Textual CSS dialect is simpler than the full CSS specification for the web, because it reflects the leaner capabilities of the terminal. Each Textual widget comes with a default CSS style, which can be changed by adding a .css file in the same directory as the application's Python files and assigning its name to the class variable CSS_PATH. In its default light and dark themes, Textual defines a number of colors as CSS variables, such as $panel, $text, and $background that are seen in the example.

Just like its web counterpart, Textual CSS knows how to use CSS selectors to define a style for a specific type of widget or a widget with a specific ID or class. In the CSS file above, #dialog refers to the Textual widget with ID dialog, Button styles all of the Button objects, and .question and .buttons define the styles for all objects with CSS classes question and buttons, respectively. There are also pseudo-classes to match widgets in a specific state, such as having the mouse cursor hover over it, being enabled or disabled, or having input focus.

If a widget needs to be rendered differently based on its state, this can be done by defining CSS classes for the different states in the .css file. Each CSS class has a different style, and the application's Python code can change the CSS class of a widget in response to an event such as a button press. And for developers who are not that comfortable with defining their own CSS classes, Vincent Warmerdam has created tuilwindcss, which is a set of CSS classes for Textual widgets. It's inspired by the Tailwind CSS framework for web sites.

Textual also has a Document Object Model (DOM), inspired by its namesake in a web browser, although Textual doesn't use documents but widgets. In Textual CSS, the DOM is a tree-like structure of the application's widgets. For example, a dialog widget may contain button widgets. CSS selectors can also be used in the application's Python code to get a list of widgets matching a selector.

Another essential concept in Textual's architecture, borrowed from web frameworks such as Vue.js and React, is reactive attributes. These are special attributes of a widget. Every time the code writes to these attributes, the widget will automatically update its output in the terminal. Developers can also implement a watch method, which is a method with a name beginning with watch_ followed by the name of the reactive attribute. This watch method will then be called whenever the attribute is modified. Changes to a reactive attribute can also be validated, for example to restrict numbers to a given range.

Async-agnostic

It's important to note that Textual is an asynchronous framework. It has an event system for key presses, mouse actions, and internal state changes. Event handlers are methods of the application or widget class prefixed with on_ followed by the name of the event. For example, when a user types in an Input widget, Textual creates a key event for each key press and sends it to the widget's message queue. Each widget runs an asyncio task, picks the message from the queue, and calls the on_key() method with the event as its first argument. The Textual documentation describes its input handling in terms of mouse actions and key presses.

Initially, Textual required the application developer to use the async and await keywords, but currently they are optional. McGugan explained in a blog article how Textual accomplishes this async-independence. His rationale for making this optional is:

This is not because I dislike async. I'm a fan! But it does place a small burden on the developer (more to type and think about). With the current API you generally don't need to write coroutines, or remember to await things. But async is there if you need it.

In the recent Textual 0.18.0 release, the developers added a Worker API to make it even easier to manage async tasks and threads. The new @work decorator turns a coroutine or a regular function into a Textual Worker object by scheduling it as either an asyncio task or a thread. This should make concurrency in Textual applications, for example handling data from the network, less error-prone.

Developer-friendly

The Textual repository on GitHub has a number of example applications, ranging from a calculator and a code browser to a puzzle game. Learning Textual development is best done by reading through the project's extensive tutorial, which builds a stopwatch application from scratch, explaining Textual's concepts along the way.

Textual also offers a couple of useful tools during development. A command like "textual run --dev my_app.py" runs an application in development mode. This allows live editing of CSS files: any changes in that file will immediately appear in the terminal without having to restart the application.

Textual also has a way to debug applications using the framework. Because TUI applications are generally unable to use print(), since it would overwrite the other application content, Textual has a debug console that shows the output of print() commands in a separate console. This can be started with a simple "textual console" command. This console also shows log messages about all events happening in the Textual app.

On the roadmap

Textual's widgets already cover many common use cases, but there are still a lot of other widgets on the roadmap. The list includes a color picker, date picker, drop-down menu, progress bar, form, and multi-line input. The developers also plan some eye candy like sparklines, plots such as bar, line, and candlestick charts, as well as images using sixels.

Some existing widgets will also be extended with extra functionality. For example, the DataTable class will gain an API to update specific rows, as well as a lazy loading API to improve the performance of large tables. Similarly, the Input widget will be extended with validation, error and warning states, and templates for specific input types such as IP addresses, currency, or credit-card numbers.

Among the other features on the roadmap, accessibility appears to be an important one. Textual currently has a monochrome mode, but there will also be a high-contrast theme and color-blind themes. Integration with screen readers for blind people is also planned.

Textual is still quite a new project, with breaking changes occasionally appearing in new releases. Developers who use Textual in their own applications should probably follow its development closely. Distribution packages of the library are likely to be outdated, so installing Textual directly from PyPI is recommended.

Fortunately, the development team is quite approachable, with a Discord server to talk to its members, as well as a blog where the developers regularly share news. It's also interesting to note that McGugan founded Textualize at the end of 2021 to develop Rich and Textual. The company's four-person developer team is planning a cloud service to run Textual apps in the web browser as easily as in the terminal.

Conclusion

In its short life, Textual has already made great strides in demonstrating the capabilities of terminal user interfaces. Various Textual-based applications, many of which can be found on Textualize employee Dave Pearson's list, showcase its potential. They include an ebook reader, task manager, Bluetooth Low Energy scanner (one of my own projects, HumBLE Explorer, see the image below), file browser, and sticky-notes application.

[HumBLE Explorer]

Textual's inspiration from web-development techniques, including its CSS dialect for styling and reactive attributes, make it a TUI framework with an innovative approach. In addition, it will be interesting to see how Textualize's plans for a cloud service turn out.


Index entries for this article
GuestArticlesVervloesem, Koen


(Log in to post comments)

Textual: a framework for terminal user interfaces

Posted Apr 18, 2023 20:08 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link]

It'd be nice if it could also be rendered as simple HTML. This way it'd be perfect for small devices that need a local configuration UI that can be accessed over SSH or HTTP.

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 0:57 UTC (Wed) by jkingweb (subscriber, #113039) [Link]

I've often wish for a TUI for Syncthing. Maybe I should write one.

Textual: a framework for terminal user interfaces

Posted Apr 29, 2023 14:15 UTC (Sat) by iainn (guest, #64312) [Link]

The good news is the web interface for textual is “coming soon”, according to the repository.

Textual: a framework for terminal user interfaces

Posted Apr 18, 2023 21:26 UTC (Tue) by ccchips (subscriber, #3222) [Link]

Running the demo, I found I couldn't figure out how to get out of the spreadsheet and back to cursor-scrolling the demo. Maybe there's something I have to learn yet...?

I sure like this idea though!!!

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 16:38 UTC (Wed) by ccchips (subscriber, #3222) [Link]

Found it....<shift-tab> seems to take me out of the spreadsheet. Strangely, though, <tab> makes the spreadsheet disappear!

Textual: a framework for terminal user interfaces

Posted Apr 18, 2023 23:01 UTC (Tue) by roc (subscriber, #30627) [Link]

Reminds me of Turbo Vision.

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 7:01 UTC (Wed) by pbonzini (subscriber, #60935) [Link]

A big limitation of Turbo Vision was that all the layout was done in absolute coordinates and sizes. That was fine when the choice was 80x25 or 80x50, but it doesn't work too well with arbitrarily resizable terminals. So it's really nice to have a similar UI but with a more modern approach to layout.

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 9:04 UTC (Wed) by faramir (subscriber, #2327) [Link]

In the couple of minutes I put into it, I couldn't find out how Textual actually puts things onto a screen. It claims to be cross-platform (Linux, MacOS, Windows), but what I really want to know is whether it will work with an old ADM-3A terminal or even a more modern VT100? :-)

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 9:21 UTC (Wed) by pbonzini (subscriber, #60935) [Link]

As far as I understand it uses ANSI sequences to send output to the terminal. For input, it's either console file descriptors (for Unix) or the Win32 console API.

Textual: a framework for terminal user interfaces

Posted Apr 20, 2023 9:29 UTC (Thu) by faramir (subscriber, #2327) [Link]

So it might work for my VT-100 serial terminal (which I believe was the model for ANSI codes), but definitely not an ADM-3A.

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 18:54 UTC (Wed) by dankamongmen (subscriber, #35141) [Link]

agreed that html+css-style layout is the best way forward for TUI design. i've planned to do as much in Notcurses, but not yet gotten to it (though others have, see e.g. TuiCSS and the others listed under "Declarative" here).

with that said, "scroll bars, clickable links, and buttons" have been in TUIs for years and years. Autumn Lamonte's Jexer, my own Notcurses, Sonzogni's FTXUI and to a degree even CDK (now distributed with NCURSES, and around since the early 90s) all have them. the difficulty is not in making widgets available, but in getting developers to use them (and working within wildly disparate terminal geometries and capabilities).

tree: https://notcurses.com/notcurses_tree.3.html
table: https://notcurses.com/notcurses_tabbed.3.html
tons of other widgets: https://notcurses.com/

it's always nice to see others playing in the TUI space, though!

Textual: a framework for terminal user interfaces

Posted Apr 19, 2023 19:06 UTC (Wed) by dankamongmen (subscriber, #35141) [Link]

and i wish good luck regarding sixels, which are more complicated than they might at first appear. check out the Notcurses III demo to see what can be done with them.

Textual: a framework for terminal user interfaces

Posted Apr 21, 2023 11:33 UTC (Fri) by mtaht (guest, #11087) [Link]

Needeth a forms library.

Textual: a framework for terminal user interfaces

Posted Apr 20, 2023 10:10 UTC (Thu) by dmoreno (subscriber, #46333) [Link]

I used textual UI for a mini project (https://github.com/davidmoreno/iptables-tui, a visualizer for iptables rules) and was on one hand very happy with the results, but on the other hand not so happy about some decisions specially about events, and some problems with layout, scrollbars, the way to manage optional views...

It might be my fault of not understaning how to doit, but as I love React for web development I started my own TUI library (of course!) (https://github.com/davidmoreno/tuidom) which is totally WIP, but way more similar to React and thus easier for me to think about it.

Textual: a framework for terminal user interfaces

Posted Apr 22, 2023 8:21 UTC (Sat) by lunaryorn (subscriber, #111088) [Link]

Take a look at ink (https://github.com/vadimdemedes/ink) perhaps?


Copyright © 2023, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds