Writing a Text Editor in 7 Minutes using Textual


I was trying to prepare a lightning talk at work and thought about a small experiment I did with Textual. I played around with writing a client that interacts with Outlook through the win32 API. Trying to open the Python file in Vim, I noticed I didn't have Vim installed 😱. That's when the question hit me: Could you live-code a text editor during my lightning talk, in 10 minutes or less? Spoiler: yes, yes you can. You just need the right tools.
A few things before we get started. The full code that I will walk through in this post is available in this gist. The dependencies for this little application are available here. In the post, I will use ...
in the code to signify the code we have already written. This is to keep things a bit neater. Now, let's dive in!
Before getting into the main part, I will add some scaffolding for the application. First, we will add all the imports for the application. Then we create a class Editor
that inherits from the App
class from Textual. This is all that is required to create an empty application. I also want to solve argument parsing right off the bat. For this, we will use ArgumentParser
from argparse
in the standard library. Here we take a single argument, folder
, the folder the editor should open. We run some basic input validation, like checking that the folder exists. We also assume that the user wants to open the parent folder if we have a file in the path.
from argparse import ArgumentParser
from pathlib import Path
from textual.app import App
from textual.widgets import TextArea, DirectoryTree
from textual.containers import Horizontal
class Editor(App):
pass
if __name__ == "__main__":
parser = ArgumentParser("My Editor")
parser.add_argument("folder", type=Path)
args = parser.parse_args()
folder: Path = args.folder
if not folder.exists():
raise FileNotFoundError(f"No folder found for path: {folder}")
if not folder.is_dir():
folder = folder.parent
app = Editor(folder)
app.run()
As we expect to pass a folder to the Editor
class, we have to implement the __init__
method.
class Editor(App):
def __init__(self, folder):
super().__init__()
self.folder = folder
self.file = None
We take the folder as an input argument, storing it as a field. We also create a field for file
that we will use to keep track of the current file open in the editor. Note that it´s essential to do the super().__init__()
call here so textual can wire up the application properly.
Running the application, we still see just an empty screen, which is quite boring. So, let´s start getting some widgets on the screen.
class Editor(App):
...
def compose(self):
yield Horizontal(
DirectoryTree(self.folder),
TextArea("", id="editor")
)
Now we get a screen with two columns. To the left, it shows the file and folder structure of the folder we asked it to open. To the right, it shows an empty text area. You can write in the text area, however, you can't open any file. Let's fix that next.
Textual uses the naming of methods to hook into the event system. This has the general syntax of _on_<object_type>_<event>
. Here we want to hook into:
an event on the
DirectoryTree
->_on_directory_tree_<event>
and trigger when a file is selected ->
_on_directory_tree_file_selected
class Editor(App):
...
def _on_directory_tree_file_selected(self, event):
path: Path = event.path
if not path.is_file():
return
self.file = path
text_editor = self.query_one("#editor")
text_editor.text = self.file.read_text()
text_editor.language = "python" if self.file.suffix == ".py" else None
In this method, we check that the path is indeed a file, returning early otherwise. We make sure to assign this path to self.file
, we will need this later. To open the file, we need to get a reference to the editor text area. We can query for it using the ID we defined earlier, prefixing it with a #
. Now we copy the text from the file into the text area. To add some flair, we also add syntax highlighting from Python files. Note that we need an extra dependency for syntax highlighting, using pip install textual[syntax]
installs this for you.
Yaaaay, now we can edit the file in our editor! We are done!
... but wait, how do I save the file ???
To do this in Textual, we will use keyboard bindings and actions. Let's define bindings for saving a file as ctrl+s
. And, while we're here, let's beat Vim in quitability by binding ctrl+q
to quit the editor 😉. We declare binding by assigning a list of tuples to a class-level variable named BINDING
. The first value is the keyboard shortcut, and the second value, "save_file"
and "quit"
, will be mapped to actions. The action quit
is already defined in Textual, but save_file
isn't. Instead, we define our own method named action_save_file
that Textual will, by using the name, hook up to the binding.
class Editor:
BINDINGS = [
("ctrl+s", "save_file"),
("ctrl+q", "quit"),
]
...
def action_save_file(self):
if self.file is None:
return
editor = self.query_one("#editor")
self.file.write_text(editor.text)
We return early if no file is open. Otherwise, we get the text area and write the text content back into the original file. It's for this step that we need self.file
, to know where to store the new content.
Below is a screenshot of the editor we just coded up. It’s not the most impresive editor in the world, but it’s pretty damn good for something you can write in 7 miuntes.
I got a bit carried away and wrote an extended version of the editor; the code is available here, and a preview is shown below. This adds some extra elements to the UI and a command palette, allowing you to open files in the directory. The command palette is a powerful feature built into Textual that allows for searchable, dynamically generated commands. In the improved editor, I used this to generate a list of commands to open files in the directory. I will not go into the details here, but in the gist for the improved editor, you can see how this was done.
Finally, now that you know how it's done, can you beat my time and write it in less than 7 minutes? Or maybe extend the application in some interesting way? Maybe you have some other tool that would work well as a terminal application? Either way, I would recommend that you experiment a bit with textual. It’s a powerful library and really, really fun to play with.
Subscribe to my newsletter
Read articles from Fredrik Sjöstrand directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Fredrik Sjöstrand
Fredrik Sjöstrand
Hello! I've been working as a developer since 2019 when I got my combined bachelor's and master's degree in computer science and engineering with a specialization in machine learning and AI. Since then I have mainly been developing CLI applications, services, and data-heavy solutions using Python. I have deployed an automation system with a machine learning core. Finally, over the last two years, I have been doing a lot of data analytics and visualizations. Changes between roles and projects, but Python remains 🐍