Did you know it’s possible to build a rich UI that runs completely in the terminal? Even some experienced Python developers avoid using the terminal in favor of Graphical User Interfaces (GUIs). But, today, many developer tools use a Terminal User Interface (TUI) since they often spend a lot of time working in the terminal. Not only are there programs and tools, but there are also several games that run entirely in the terminal.
So, in this article, we will first develop a good understanding of TUIs and then move on to creating an interesting TUI using PyScripter to manage to-do lists.
Table of Contents
What is a TUI?
A TUI, short for Terminal User Interface, uses the terminal’s advanced features as the basis for a kind of GUI, generally occupying the full area of the terminal. It uses escape codes on UNIX platforms and proprietary console commands on the Windows platform to take control of the entire console window and display a user interface. TUIs can handle input via the keyboard, but some modern systems also use mouse input. This is what makes them very special and different from conventional terminals.
Thus, a TUI allows users to interact with a program via the terminal. The TUI applications interact via UI, although many TUI programs also accept commands.
Why is Textual popular for making TUIs?
Textual is a Python framework for creating interactive applications that run in your terminal. It mainly adds interactivity to Rich, a rich text and formatting library, with a Python API inspired by modern web development.
On modern terminal software, Textual apps offer more than 16 million colors, mouse support, and smooth, flicker-free animation. Furthermore, a powerful layout engine and reusable components make it possible to build apps that rival the desktop and web experience.
Currently, the Textual framework runs on Linux, macOS, and Windows and requires Python 3.7 or above.
How to build a ToDo list TUI in Python?
Let us now move to the main part of this tutorial and build a to-do list in Python. But first, there are some requirements that you need to install.
What are the requirements for this TUI tutorial?
For this tutorial, you will need Python 3.9+ installed on your computer. You might also need to install a feature-packed IDE like PyScripter to write the code.
Then, you can install textual
via PyPI.
If you intend to run Textual apps simply, install textual using the following command:
1 |
pip install textual |
However, if you plan to develop Textual apps, you should install "textual[dev]"
. This is because the [dev] part installs a few extra dependencies for development.
1 |
pip install "textual[dev]" |
Now that we are done setting up our environment, let’s start building our ToDo list manager.
How to create a basic ToDo in textual?
We’ll start this tutorial by creating a TUI in Textual that serves our purpose and then add to it using Textual’s amazing CSS-supported features.
First, we will create a file named Todo.py
and import some functions required for our ToDo application:
1 2 3 4 5 |
from textual.app import App, ComposeResult from textual.containers import Container from textual.widgets import Button, Header, Footer, Static done = [] # All completed tasks will be added |
The first line imports the App
class that will act as the base of our application. Next, we import Container
from textual.containers
. As the name implies, this acts as a widget containing other widgets.
Finally, we import Header
, Footer
, Button
, and Static
. The Header
widget shows up at the top of our app and shows the title at the top of the app. The Footer
, on the other hand, shows a bar at the bottom of the screen with bound keys. The Button
creates a clickable button and Static
acts as a base class for simple control. In addition, we also define a list named done
that will contain all of the user’s completed tasks.
Let’s first create our Task
class:
1 2 |
class Task(Static): """A widget to display the Task.""" |
The Task
class is a simple class that inherits from Static
. This creates a static widget allowing us to display the tasks we want on our app.
Next, we will create a Todo
widget that will cleanly display our Task
widgets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ToDo(Static): """A ToDo widget.""" def __init__(self, text="") -> None: self.text = text super().__init__() def on_button_pressed(self, event: Button.Pressed) -> None: """Event handler called when a button is pressed.""" self.add_class("Task_Done") # Color Changing to Green done.append(self.text) def compose(self) -> ComposeResult: """Create child widgets of a ToDo.""" yield Button("Done", id="Done", variant="success") yield Task(self.text) |
As you can see the ToDo
widget class also extends Static
. This class has a compose()
method that yields child widgets consisting of a Button
object and a single Task
object. The Button
constructor takes a label argument that displays the text on the button. It also takes two optional parameters:
id
is an identifier that can help identify different buttons and help in applying styles.variant
is a string that selects a default style. The “success
” variant makes the button green.
These widgets will form a single Task in our application.
In addition, we also define an on_button_pressed()
method that acts as an event handler. Event handlers are methods called by Textual in response to an event, such as a key press, mouse click, etc. Event handlers begin with on_
followed by the name of the event they will handle. Hence, on_button_pressed
will handle the Button.Pressed
event. Our event handler assigns a CSS class named “Task_Done
” (which we will define later) to the ToDo
Class whenever our Button is pressed. This will effectively change the color of our task showing that the task is done.
Finally, we can start building our ToDoApp
. We will first begin by creating our ToDoApp
class that will inherit from App
. We also create a All_Tasks
list that will store all our tasks:
1 2 3 |
class ToDoApp(App): """A Textual app to manage ToDos.""" All_Tasks = [] |
Next, we a compose()
method that displays our widgets to our app:
1 2 3 4 5 |
def compose(self) -> ComposeResult: """Called to add widgets to the app.""" yield Header() yield Footer() yield Container(id="Tasks") # Associate Container widget with Task object |
Here we only call three widgets. The Header
, Footer
, and Container
. The container will contain all of the ToDo
widgets in our TUI. Additionally, we add an id
parameter to our Container
to help us add widgets.
How to add key bindings in a Python TUI app?
Next, we add bindings to our app. For our application, we will bind four keys:
- The
q
key binding will help us quit our TUI. - The
d
key binding will help us toggle between a dark and light mode in our TUI. - The
r
binding will read all the tasks in ourtodo.txt
file and add them to our TUI. - Finally, we add a
s
binding that will save all the tasks that are not done in our TUI.
To create our key bindings, we create a BINDINGS
list containing tuples that map (or bind) keys to actions in your app. The first value in the tuple is any one of the keys on the keyboard; the second value is the action/ function to be called; the final value is a short description of the function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ToDoApp(App): """A Textual app to manage ToDos.""" BINDINGS = [ ("d", "toggle_dark", "Toggle dark mode"), # Bind the d key to toggle_dark ("q", "quit", "Quit"), # Bind the q key to quit ("r", "read", "Read"), # Bind the r key to read ("s", "save", "Save") # Bind the s key to save ] def compose(self) -> ComposeResult: """Called to add widgets to the app.""" yield Header() yield Footer() yield Container(id="Tasks") # Associate Container widget with Task object |
Each key binding is mapped to a function that begins as action_. For example, d
will be mapped to the action_toggle_dark()
function by adding action_
at the beginning of the toggle_dark
that’s present in the tuple. So, now let us define our functions.
We first define the dark mode function, action_toggle_dark()
, which inverts the dark attribute:
1 2 3 |
def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" self.dark = not self.dark |
Next, we define a helper method named add_ToDo()
:
1 2 3 4 5 |
def add_ToDo(self, text) -> None: """An action to add a Task.""" new_ToDo = ToDo(text=text) self.query_one("#Tasks").mount(new_ToDo) new_ToDo.scroll_visible() |
This function simply takes in a text argument, creates a ToDo
object, and adds it to our Tasks
Container. This will help us add multiple tasks in our app using a file. Now, we define the action_read()
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def action_read(self) -> None: """An action to read tasks from todo.txt and display in Container.""" Tasks = self.query("ToDo") # Get current tasks in the container if there were any if Tasks: Tasks.remove() # If there were some tasks then remove all of them try: # Read txt file and add all the tasks to Container with open("todo.txt", "r") as f: formatted = f.read().split("n") for i in formatted: self.All_Tasks.append(i) self.add_ToDo(i) except: # If no File found Print Error. print("Invalid or no filename given!") |
The action_read()
method takes in no arguments and is triggered when we press "
r
"
on the keyboard. The function uses self.query("ToDo")
to retrieve all ToDo
objects in our app. If we have any existing ToDo
objects, we simply remove them by calling .remove()
method.
Next, we open and read the contents of our todo.txt
file. The file contains a single task in one line. We iterate over all the tasks in our file and use the add_ToDo()
helper function to add each task to our Container
.
Finally, we can define our action_save()
method that overwrites our existing todo.txt
file:
1 2 3 4 5 6 7 8 9 10 |
def action_save(self) -> None: """Called to update todo.txt file such that all tasks done are removed.""" not_done = [] for i in self.All_Tasks: if i in done: continue not_done.append(i) with open("todo.txt", "w") as file: # Rewrite the file with only those tasks that are not done file.write("n".join(not_done)) |
The function simply reads all the tasks that we have in the given text file and checks them against all the tasks that are selected as done (via the done Button on_pressed
method) in our global done
list. It then overwrites the todo.txt
file with all tasks that are not completed.
What does the final example code for the Python ToDo list TUI app look like?
Here is what our final Todo.py
looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
from textual.app import App, ComposeResult from textual.containers import Container from textual.widgets import Button, Header, Footer, Static done = [] # All completed tasks will be added class Task(Static): """A widget to display the Task.""" class ToDo(Static): """A ToDo widget.""" def __init__(self, text="") -> None: self.text = text super().__init__() def on_button_pressed(self, event: Button.Pressed) -> None: """Event handler called when a button is pressed.""" self.add_class("Task_Done") # Color chanigng to green via Text_Done class in CSS done.append(self.text) # Add task to done list def compose(self) -> ComposeResult: """Create child widgets of a ToDo.""" yield Button("Done", id="Done", variant="success") # Adding a button that says Done yield Task(self.text) # Display text according to Task object details in CSS class ToDoApp(App): """A Textual app to manage ToDos.""" All_Tasks = [] BINDINGS = [ # Binding details displayed Footer ("d", "toggle_dark", "Toggle dark mode"), # Bind the d key to toggle_dark ("q", "quit", "Quit"), # Bind the q key to quit ("r", "read", "Read"), # Bind the r key to read ("s", "save", "Save") # Bind the s key to save ] def compose(self) -> ComposeResult: """Called to add widgets to the app.""" yield Header() yield Footer() yield Container(id="Tasks") # Accociate Container widget with Task object def add_ToDo(self, text) -> None: """An action to add a Task.""" new_ToDo = ToDo(text=text) self.query_one("#Tasks").mount(new_ToDo) new_ToDo.scroll_visible() def action_save(self) -> None: """Called to update todo.txt file such that all tasks done are removed.""" not_done = [] for i in self.All_Tasks: if i in done: continue not_done.append(i) with open("todo.txt", "w") as file: # Rewrite the file with only those tasks that are not done file.write("n".join(not_done)) def action_read(self) -> None: """An action to read tasks from todo.txt and display in Container.""" Tasks = self.query("ToDo") # Get current tasks in the container if there were any if Tasks: Tasks.remove() # If there were some tasks then remove all of them try: # Read txt file and add all the tasks to Container with open("todo.txt", "r") as f: formatted = f.read().split("n") for i in formatted: self.All_Tasks.append(i) self.add_ToDo(i) except: # If no File found Print Error. print("Invalid or no filename given!") def action_toggle_dark(self) -> None: """An action to toggle dark mode.""" self.dark = not self.dark if __name__ == "__main__": app = ToDoApp() app.run() |
How to run and test our Python ToDo list TUI app?
Before running this code, let’s create some tasks. Create a todo.txt
file in the same directory as your Todo.py
file and add the following tasks in separate lines:
Now let’s execute Todo.py
. This will give you the following output in the terminal:
If we press "r"
, the tasks contained in todo.txt
will appear in the TUI:
Clicking the “Done
” button will cause these tasks to be appended to our done
list. For example, pressing the “Done
” button corresponding to “Gym
” will cause “Gym
” to be appended to our done
list.
Pressing “s
” will cause the completed tasks to be removed from todo.txt
:
Even though the buttons are clickable and you can scroll the container, our TUI is not very interactive. This is because we have not applied any styles to our new widgets. That is where Textual’s new features come in.
How to enhance this Python TUI using CSS?
CSS files are data files loaded by your app which contain information about styles to apply to your widgets. Textual has CSS support, allowing you to customize your application. As mentioned before, we can make our TUI more visually appealing using CSS. For this, let’s go ahead and create a CSS file, Todo.css
.
CSS files contain several declaration blocks. Here’s the first block that Todo.css
will contain:
1 2 3 4 5 6 7 8 |
ToDo { layout: horizontal; background: #36454F; height: 7; margin: 1; min-width: 50; padding: 2; } |
The first line tells Textual that the styles should apply to the ToDo
widget, while the lines between the curly brackets contain the styles themselves. layout: horizontal
aligns child widgets horizontally from left to right. background: #36454F
sets the background color to #36454F
or charcoal
but there are also other ways to specify colors, such as rgb(54,69,79)
. height: 7
sets the widget’s height equivalent to seven lines of text. margin: 1
will set a one-cell margin around the ToDo
widget to create some space between widgets in the list. Now, min-width: 50
sets the width of the widget to at least fifty cells. Note that there is no maximum so that the width can go on to cover an entire window. Lastly, padding: 2
will set a padding of two cells around its child widgets to avoid cluttering.
Following are the contents of the rest of Todo.css
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Task { content-align: center middle; text-opacity: 70%; height: 3; } Button { width: 10; dock: left; } .Task_Done { text-style: bold; background: $success; color: $text; } |
The Task
block aligns text to the center (horizontally) and middle (vertically) and sets its height
to three lines. Using text-opacity
, we can even fade the text slightly.
The Button
block sets the width
of buttons to ten cells equivalent to character widths and sets the dock
style to left
to align the widget to the left edge.
We also wanted our ToDo
widget to have two states. One would be a default state with a Done Button
and the other when the Done Button
is clicked; the widget has a green background and bold text. Thus, the last class is a rule where “.” indicates that .Task_Done
refers to a CSS class called Task_Done
. The new styles will only be applied to widgets with this CSS class. In the background
and color
options, the $
prefix picks a pre-defined color from the built-in theme.
And we’re all done with styling our TUI. So, let’s add the path of this CSS path to our ToDoApp
class in Todo.py
to integrate our CSS file with our TUI app (it is interesting to note that Textual supports multiple CSS paths):
1 |
CSS_PATH = "Todo.css" |
Finally, we can run our app again to test it out:
You can mark “Cook
” and “Study
” as done by clicking on their corresponding buttons:
You can then save this updated to-do list by pressing the “s
” button. If we press “r
” again, we can see the changes:
We can also confirm these changes by rechecking todo.txt
:
You may have noticed that we have used PyScripter throughout this project. This is because PyScripter is one of the best tools that you can have to organize your code and understand it better. So if you are starting with Python, PyScripter should be part of your starter kit.
Why should you use PyScripter to create TUIs?
Embarcadero sponsors PyScripter, a popular, free, and open-source Python IDE. This IDE has all the features you’d expect from a modern Python IDE while remaining lightweight and fast. PyScripter is natively compiled for Windows, allowing it to use as little memory as possible while providing maximum performance. It also has full Python debugging capabilities, both locally and remotely. Another noteworthy feature of this IDE is its integration with Python tools like PyLint, TabNanny, Profile, etc. It can also run or debug files directly from memory.
PyScripter includes all the features you’d expect from a professional IDE, such as brace highlighting, code folding, code completion, and syntax checking as you type. It also has features you’d expect to find in a legitimate native app, such as dragging and dropping files into the PyScripter IDE. These features work in tandem to save time and make the development process simple and enjoyable.
In addition, PyScripter includes a Python interpreter with call indications and code completion. This program lets you run scripts without saving them and keeps a record of your command history. This IDE also includes a remote Python debugger for debugging Python code. As a result, variables, the watch window, and the call stack are visible. Additional debugging tools built into PyScripter include conditional breakpoints and thread debugging. Furthermore, PyScripter has debugger indications to help you debug your files without saving them, as well as the ability to destroy them.
Thus, PyScripter is a fantastic IDE seeking to create a Python IDE that can compete with other languages’ traditional Windows-based IDEs. It is lightweight, versatile, and extendable with a lot of features.
Furthermore, since it was built from the ground up for Windows, it is substantially faster and more responsive than cumbersome text editors, making it perfect for making TUIs.
Are you ready to build your own Python TUI?
Congratulations on building a stunning to-do manager TUI! In this tutorial, you have learned several useful things about Python!
Textual is a TUI framework for Python inspired by modern web development. It has a very active repository on GitHub. This awesome framework is built on Rich and is currently a work in progress, but usable by programmers who don’t mind some API instability between updates.
You can conveniently build complex and simple TUIs with Textual in PyScripter. This IDE will make the entire process very seamless and enjoyable for you.
What are the FAQs on building a Python TUI?
What are some use cases of TUIs?
TUIs can be used to make programs like stopwatches, calculators, code and other file viewers, basic games, and to-do lists (as shown in this article).
There are several other possibilities. The only limit is your imagination!!
Which other TUI libraries are available in Python?
Pytermgui, picotui, and pyTermTk are other frameworks and libraries that can create interactive and attractive TUIs in Python.
What is the difference between CLI, GUI, and TUI?
GUI means Graphical User interface. You must have heard this word most commonly. It is the GUI that has enabled the common user to use computers. A GUI application is an application that can be run with a mouse, touchpad, or touchscreen.
The CLI is a command line program that accepts text input to perform a certain task. Any application you can use via commands in the terminal falls into this category.
TUI, i.e., Terminal User Interface, falls between GUI and CLI. It can also be called a kind of text-based GUI. Before the arrival of the GUI, everything used to run on the command line; then, the terminal user interface provided a kind of GUI on the command line. TUI programs can be run via a mouse or keyboard.
What is Python4Delphi?
In short, P4D offers a selection of real-world apps that can be modified to your individual needs and used in Python GUI.
Consider integrating Delphi’s GUI and desktop app development power with Python in your applications to give world-class solutions to your clients’ needs. Python4Delphi is the answer to your problems.
Python for Delphi (P4D) is a free Delphi and Lazarus component library that wraps the Python DLL. They make it simple to run Python scripts while allowing for new Python modules and types. The best feature of P4D is that it simplifies the use of Python as a scripting language. It also includes numerous customizable demos and tutorials that are ready to use in the development of real-world apps.