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 Text User Interface or 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 or proprietary console commands (the latter on Windows, the former on most other things) to take control of the entire console window and display a user interface. TUIs Usually handle input via the keyboard, but some modern systems also use mouse input.
Thus, a TUI allows users to interact with a program via the terminal. The terminal does not interact via commands, 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 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, Textual runs on Linux, macOS, and Windows. This framework 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 start building 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 will also need to install an IDE a feature-packed IDE like PyScripter.
Then, you can install Textual via PyPI.
If you intend to simply run Textual apps, then install textual
and textual_inputs
using the following command:
1 |
pip install textual, textual_inputs |
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]" |
Moreover, to make our to-do list more appealing will be using Rich. The Rich API will help us add color and style to our terminal output. In addition, Rich can also render attractive tables, progress bars, markdown, syntax highlighted source code, tracebacks, and other features right out of the box.
After installing Textual, you can install Rich using the following PyPI command:
1 |
pip install rich |
How to Create a Basic TUI in Textual?
Let’s start this tutorial by creating a simple TUI in Textual, and then we will build upon this to create a TUI that serves our purpose.
The following code builds a TUI with a text box Text that takes in some text from the user and displays it in the Output panel when the enter key is pressed. It has a simple Header and Footer where the Footer has a button q
that can be clicked using the mouse to exit the program.
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 |
import rich.box from rich.panel import Panel from textual.app import App from textual.widgets import Footer, Header, Static from textual_inputs import TextInput class SimpleForm(App): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) async def on_load(self) -> None: # Bind the q button to quit. await self.bind("q", "quit", "Quit") # Bind the enter key to the action_submit function. await self.bind("enter", "submit", "Submit") async def on_mount(self) -> None: # Placing Header and Footer at the top and bottom await self.view.dock(Header(), edge="top") await self.view.dock(Footer(), edge="bottom") # Creates a Text Input that allows you to type text self.text = TextInput( name="Text", placeholder="enter your Text...", title="Text", ) # Creates a Panel that displays your text. self.output = Static( renderable=Panel( "", title="Output", border_style="blue", box=rich.box.SQUARE ) ) # Defining the Placement for the output panel and text. await self.view.dock(self.output, edge="left", size=40) await self.view.dock(self.text, edge="top") async def action_submit(self) -> None: # Retrieve the value from the Input text box. val = self.text.value # Format and update the value in the output panel await self.output.update( Panel(val, title="Output", border_style="blue", box=rich.box.SQUARE) ) # Empty the text box for the next input. self.text.value = "" # Running our form here... if __name__ == "__main__": SimpleForm.run(title="Simple TUI", log="textual.log") |
Running this code will give you the following output in the terminal:
If we press enter
, our text will appear on the output
panel:
If you cannot follow the program above, do not fear as we walk you through it step by step as we build a more complex To-do list over this code.
How To Create An Instance of Our to-do List?
We will start by importing several libraries and functions to create our TUI:
1 2 3 4 5 6 7 8 |
import rich.box from rich.panel import Panel from textual.app import App from textual.reactive import Reactive from textual.widgets import Footer, Header, Static from textual_inputs import TextInput formatted = "" |
Above, we created a global variable formatted
that helps us keep track of tasks in our to-do list by storing all our tasks in a string format.
Next, we define a class named ToDoForm
that inherits from App
imported from the textual.app
. This will serve as the primary class to help us run our TUI to-do list. We also define a constructor that calls the parent class’s constructor, which will help us instantiate our TUI to-do list.
1 2 3 4 5 6 |
class ToDoForm(App): def __init__(self, **kwargs) -> None: """ Constructor helps initialize the main ToDo list. """ super().__init__(**kwargs) |
We can now move on and start creating the user interface for our TUI.
How Do We Create a User Interface For Our TUI?
To create a user interface, we define an async
function on_mount()
that will format our TUI user interface. We define the function as a class method called when we instantiate our ToDoForm
object.
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 |
async def on_mount(self) -> None: # Placing Header and Footer at the top and bottom await self.view.dock(Header(), edge="top") await self.view.dock(Footer(), edge="bottom") # Creates a Text Input that allows you to type text self.todo = TextInput( name="Text", placeholder="enter your Text...", title="Text", ) self.file = TextInput( name="File Import", placeholder="enter your ToDo File Name...", title="File Import", ) # Creates a Panel that displays your text. self.output = Static( renderable=Panel( "", title="Output", border_style="blue", box=rich.box.SQUARE ) ) # Defining the Placement for the output panel and text. await self.view.dock(self.output, edge="left", size=40) await self.view.dock(self.text, edge="top") |
We start by first placing a Header
and Footer
on our TUI. This is done by using self.view.dock()
, adding an instance of either a Header
or Footer
as a parameter and specifying its placement in the optional edge
argument. Here we define the Header
to be at the top and the Footer
to be at the bottom.
Next, we define a text input that will help us append tasks to our TUI to-do list. We do this by creating an instance of the TextInput
object and assigning it to a variable self.todo
. We then specify its title, name,
and placeholder.
Note: The title is what will be displayed on top of the TextInput
text box.
The self.todo
will receive inputs from the user, which we will later use to add items to our tasks in formatted.
Similarly, we create another TextInput
instance named self.file that we will use to take file names as input and add their contents to our to-do list.
Finally, to output our to-do list, we instantiate a Static
object, and within it, a Panel
. We then assign it to a variable named self.output
. This will serve as the output of our TUI. We then define the placement of our text input and output panel by using the self.view.dock()
and specify its placement using edge
. For our output panel, we also use another size
argument that helps us define a custom size for our panel.
But the question arises, how can we use these variables to add values to our to-do list?
How To Create Functions to Add Entries to Our to-do list?
To add entries to our to-do list, we create async
functions that will use the inputs provided in the self.todo
and self.file
objects, and then add them to our to-do list.
First, we define an async function action_submit()
that will use the self.todo
text box.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
async def action_submit(self) -> None: """ Adds the Todo entries to the Panel when an enter is pressed. This function also saves the ToDo entries into a ToDos.txt file, whenever it is called. """ global formatted # Retrieve the value from the Input text box. do = self.todo.value # Format and update the value in the ToDo panel formatted += "-" + do + "n" await self.output.update( Panel(formatted, title="ToDo List", border_style="blue", box=rich.box.SQUARE) ) # Empty the text box for the next ToDo. self.todo.value = "" # Write the ToDo into a ToDo.txt file. with open("ToDos.txt", "w") as f: f.write(formatted) |
The function simply uses the .value
attribute of the TextInput
object to retrieve the value in the self.todo
text box. It then formats the input by removing a hyphen and then adds it to self.output
list using the .update()
method.
Finally, the function empties the text box by setting the value of self.todo.value
to an empty string before writing the contents of the to-do list into a file named “ToDo.txt.
”
Next, we define a function action_read()
that reads the file provided in the text box of self.file
, and writes its contents into our to-do list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
async def action_read(self) -> None: """ Reads the file that you have provided in your "File Import" textbox. This function then diplays all your ToDos defined in your file on the Todo Panel. """ global formatted # Read the file with n separated inputs. try: with open(self.file.value, "r") as f: formatted = f.read() + "n" # Display the contents of the File in the ToDo Panel. await self.output.update( Panel(formatted, title="ToDo List", border_style="blue", box=rich.box.SQUARE) ) except: # If no File found Print Error. print("Invalid or no filename given!") |
Like the action_submit()
function, we first read the contents provided in the self.file
text box using the .value
attribute. If a file has been provided, the function reads the file’s contents, parses and formats it, then appends the items in the file to our to-do list
. If no file has been provided, the function simply prints an error.
How Can We Add Bindings to Our TUI?
Now that we’ve defined our functions, we can call them by pressing some keys on our keyboard. This is done through key bindings.
Textual allows us to create key bindings for our TUI easily. This can be done using the .bind()
method, which takes three arguments. The first argument specifies the key that we will bind, the second argument specifies the action to bind to, and the final argument is optional and provides a short description of the action.
For our application, we will bind three keys:
- The “q” key binding will help us quit our TUI.
- The “enter” key binding will help us submit entires to our to-do list.
- Finally, pressing the “ctrl+r” key binding will allow us to read specific files that we specify in the
self.file
text box.
To create our key bindings, we create an async function on_load()
that runs when we create an instance of our TUI:
1 2 3 4 5 6 7 8 9 10 |
async def on_load(self) -> None: # Bind the q button to quit. await self.bind("q", "quit", "Quit") # Bind the enter key to the action_submit function. await self.bind("enter", "submit", "Submit") # Bind the r key to the action_read function. await self.bind("r", "read", "Read") |
How Do We Run The Code We Have Created?
Now all that’s left is to create an instance of our ToDoForm
and run it. Here is what our final code 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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
import rich.box from rich.panel import Panel from textual.app import App from textual.widgets import Footer, Header, Static from textual_inputs import TextInput formatted = "" class ToDoForm(App): def __init__(self, **kwargs) -> None: """ Constructor helps initialize the main ToDo list. """ super().__init__(**kwargs) async def on_load(self) -> None: """ On loading of our ToDo, we bind some keyboard keys to certain actions. """ # Bind the q button to quit. await self.bind("q", "quit", "Quit") # Bind the enter key to the action_submit function. await self.bind("enter", "submit", "Submit") # Bind the (control + r) key to the action_read function. await self.bind("ctrl+r", "read", "Read") async def on_mount(self) -> None: """ Initialization function that helps create the main Todo list. """ # Placing a Normal Header and Footer at the top and bottom # of the Todo form. await self.view.dock(Header(), edge="top") await self.view.dock(Footer(), edge="bottom") # Creates a Text Input that allows you to add your next Todo. self.todo = TextInput( name="ToDo", placeholder="enter your next ToDo...", title="ToDo", ) # Creates a TextInput that allows you to import a file. self.file = TextInput( name="File Import", placeholder="enter your ToDo File Name...", title="File Import", ) # Creates a Panel that displays all your Todos. self.output = Static( renderable=Panel( "", title="ToDo List", border_style="blue", box=rich.box.SQUARE ) ) # Defining the Placement for the output and two textboxes. await self.view.dock(self.output, edge="left", size=40) await self.view.dock(self.todo, self.file, edge="top") async def action_read(self) -> None: """ Reads the file that you have provided in your "File Import" textbox. This function then diplays all your ToDos defined in your file on the Todo Panel. """ global formatted # Read the file with n separated inputs. try: with open(self.file.value, "r") as f: formatted = f.read() + "n" # Display the contents of the File in the ToDo Panel. await self.output.update( Panel( formatted, title="ToDo List", border_style="blue", box=rich.box.SQUARE ) ) except: # If no File found Print Error. print("Invalid or no filename given!") async def action_submit(self) -> None: """ Adds the Todo entries to the Panel when an enter is pressed. This function also saves the ToDo entries into a ToDos.txt file, whenever it is called. """ global formatted # Retrieve the value from the Input text box. do = self.todo.value # Format and update the value in the ToDo panel formatted += "-" + do + "n" await self.output.update( Panel(formatted, title="ToDo List", border_style="blue", box=rich.box.SQUARE) ) # Empty the text box for the next ToDo. self.todo.value = "" # Write the ToDo into a ToDo.txt file. with open("ToDos.txt", "w") as f: f.write(formatted) # Running our ToDo form here... if __name__ == "__main__": ToDoForm.run(title="ToDo List Creator", log="textual.log") |
Let’s say you wanted to add some tasks to your to-do list
. You can write it down in the ToDo
text box:
If you press enter, it will be added to your to-do list
.
This creates a file, “ToDo.txt
” in the same directory.
Let us now create a text file named “tasks.txt
” inside the application directory and define some tasks inside it. First, we will add three tasks to the file, with each task beginning with a hyphen:
With the code running, add the file’s name to the TUI, then press “ctrl+r” together. This adds the tasks to your to-do list
:
Let’s add more tasks to your to-do list
:
All this content has been appended to the text file, “ToDo.txt
“:
Congratulations on reaching the end of this tutorial. You may have noticed that we have been using 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 the ability to drag and drop files into the PyScripter IDE. These features work in tandem to save time and make the development process simple and enjoyable.
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 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 This Topic?
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 be used to 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.