Build Your Text Editor With Rust! Part 3

Kofi Otuo
8 min readOct 5, 2021

--

In the previous chapter, we saw how to read keypresses from the user and now various keypresses are interpreted. We also migrated our project to use crossterm to make things easier. Let’s now modify how we exit the program.

Using Ctrl + Q to exit 🛑

Let’s use Ctrl + Q to exit our program instead of just q :

We’re now using event::KeyModifiers::CONTROL instead of event::KeyMOdifiers::NONE.

Refactor Keyboard Input ♻️️

Let’s refactor our code so we have a function for low-level keypress reading, and another function for mapping keypresses to editor operations. We’ll also stop printing out keypresses.

First, let’s create a struct that would read various keypresses. We would name it Reader:

Then add a method to read key events:

Reader struct

We create a read_key function, similar to what we already had.

Now let’s create a new struct Editor which would be the main master mind for our project. That struct would be responsible for running our program.

We also create a new method to create a new instance of Editor.

Note that our project is just contained in main.rs so it doesn’t really matter how you arrange the various struct. You could put struct Editor before or after struct Reader. Just place it in an order that makes navigating your code easier.

Now let’s process the events returned by Reader and create a run function:

In process_keypress function, we return whether or not we should continue reading key events. If we return false, it means the program should terminate since we don’t want to read key events again. Now let’s modify our main() to use Editor.run() instead:

Editor run

Now we have a simple main() function and we would like to keep it as such.

Clear The Screen 💻

We’re going to render the editor’s user interface to the screen after each keypress. But first, let’s clear the screen so we have the whole screen to ourselves. We will do that by first creating an Output struct that handles all output, just as we created a struct to read keypresses.

We make clear_screen() an association function since we don’t need an instance of Output before we can clear the screen.

What the clear_screen function actually does is writing escape sequences to the terminal. These sequences modifies the behavior of the terminal and can be used to do other things like add color and so on. For ANSI terminals, the escape sequence for ClearAll is \x1b[2J (you can use print!("\x1b[2j”);stdout().flush();to try it). x1b is the escape character (similar to pressing Esc on the keyboard) and it is followed by [. The J command (Erase In Display) to clear the screen. Escape sequence commands take arguments, which come before the command. In this case the argument is 2, which implies clear the entire screen. <esc>[1J would clear the screen up to where the cursor is, and <esc>[0J would clear the screen from the cursor up to the end of the screen. Also, 0 is the default argument for J, so just <esc>[J by itself would also clear the screen from the cursor to the end. Note that crossterm provides an abstraction over various escape sequences

Now let’s put our function into effect

Clear screen

Reposition The Cursor 🖱

You may have notice that the cursor isn’t positioned at the top left of the screen. Let’s position it there so we can draw our editor from top to bottom

Clear Screen On Exit 🧹

Let’s clear the screen and reposition the cursor when our program exits. If an error occurs in the middle of rendering the screen, we don’t want the output of the program to be left over on the screen, and we don’t want the error to be printed wherever the cursor happens to be at that point.

As you may have guessed, we would put the function to clear the screen when our program exits either successfully or not in Cleanup:

Tildes 〰️

Let’s now begin drawing. Let’s draw a column of tildes (~) on the left hand side of the screen, like vim does. In our text editor, we’ll draw a tilde at the beginning of any line that come after the end of the file being edited.

draw_rows()will handle drawing each row of the buffer of text being edited. For now it draws a tilde in each row, which means that row is not part of the file and can’t contain any text. After drawing, we send the cursor back to the top left of the screen. Let’s now modify the code to draw the proper number of tildes.

Screen size

First, we modify Output to hold the window size since we’re going to use the window’s size for a number of calculations. We then set the value of win_size when we create an instance of output. The type of the integers in win_size is usize but terminal::size() returns a tuple with type (u16,16), so we have to convert u16 to usize. We use .map to do that. If terminal::size().unwrap() panics on your system, you should create a new issue.

The Last Line 📉

Maybe you noticed the last line of the screen doesn’t seem to have a tilde. That’s because of a small bug in our code. When we print the final tilde, we then print a "\r\n"(println!() adds a new line) like on any other line, but this causes the terminal to scroll in order to make room for a new, blank line. Let’s make the last line an exception when we print our "\r\n".

Last line

Note that it’s necessary to explicitly call stdout().flush() since we’re using print!().

Append Buffer 🧳

It’s not a good idea to make a number of small writes to stdout every time we refresh the screen. It would be better to do one big write, to make sure the whole screen updates at once. Otherwise there could be small unpredictable pauses between various writes, which would cause an annoying flicker effect.

First let’s create a struct that would handle the editor’s contents. We would write to it instead of stdout.

Now let’s now implement std::io::Write for EditorContent:

First, we convert the bytes passed into the write function to str so that we can add it to the content. We return the len of the string if the bytes can be converted to string else we return error. When we call flush() on EditorContents, we want it to write to the stdout so we use the write!() macro and then call stdout.flush(). We also have to clear the content so we can use it for the next screen refresh.

Now let’s modify our code to use EditorContents. We will add it as a field in Output:

Note that we have changed draw_rows to use &mut self so our code won’t compile yet. Let’s edit it a bit to get rid of the errors:

EditorContents

Hide The Cursor When Repainting ⬛️

There is another possible source of the annoying flicker effect we will take care of now. It’s possible that the cursor might be displayed in the middle of the screen somewhere for a split second while the terminal is drawing to the screen. To make sure that doesn’t happen, let’s hide the cursor before refreshing the screen, and show it again immediately after the refresh finishes.

Hide cursor

Clear Lines One At A Time 📃

Instead of clearing the entire screen before each refresh, it seems more optimal to clear each line as we redraw them. Let’s remove Clear(ClearType::All), and instead put Clear(ClearType::UntilNewLine) at the end of each line we draw.

Clear line

Let’s now draw a welcome message.

Welcome Message 🌄

Let’s simply display the name of our editor and version third of the way down the screen.

We use the format!() macro to join the VERSION to our welcome message. We then check if the welcome length it greater than what the screen can display at a time. If the length is more than, we truncate it.

Now add some padding to center it:

Welcome message

To center a string, you divide the screen width by 2, and then subtract half of the string’s length from that. In other words: screen_columns/2 - welcome.len()/2, which simplifies to (screen_columns - welcome.len()) / 2. That tells you how far from the left edge of the screen you should start printing the string. So we fill that space with space characters, except for the first character, which should be a tilde

Move The Cursor 🔁

Let’s now move to cursor control. Currently arrow keys nor any other key moves the cursor. Let’s start by moving the cursor with wasdkeys.

We’ll first create a struct named CursorController which holds the position of the cursor:

cursor_x is the horizontal coordinate of the cursor (the column) and cursor_y is the vertical coordinate (the row). We initialize both of them to 0, as we want the cursor to start at the top-left of the screen. Now let’s add a cursor_controller field to the Output struct and update refresh_screen() to use cursor_x and cursor_y:

Now we add a method to CursorController to interpret various keypresses:

We then modify Output to provide a move_cursor method since we want Editor to interact with all output through Output struct

Finally we pass the appropriate keypresses to move_cursor():

Move Cursor

Note ✍️ the syntax in the match block. We use the @ operator. What it basically does is, it creates a variable and checks if the variable matches the condition provided. So in this case, it creates the variable val and then checks if the variable is equal to the various char provided in the parenthesis. It’s roughly equal to the following code:

Now you should be able to move the cursor around. Don’t worry if the program crashes, as you move the cursor around, due to OutOfBounds error i.e. attempt to subtract with overflow. We will fix that soon.

Move Cursor With Arrow Keys 🔝

Let’s now modify the program to use arrow keys to move the cursor. It is similar to moving the cursor with wasd keys. First change the process_keypress function:

Then we modify move_cursor function accordingly:

Fix The Out Of Bounds Error 🛡

Currently, if you try to move the cursor above the screen or move the cursor too much to the left, the program would crash. That’s because cursor_y and cursor_x cannot be negative (since they are usize)so if you attempt to send it to the negatives, Rust would panic. Let’s prevent that by doing some bounds checking before we subtract or add 1 to cursor_x or cursor_y.

Note ✍️ ️ that .saturating_sub(rhs) checks for overflow, similar to

lhs = if lhs < rhs { 0 } else { lhs - rhs }// in our case it simply means:if lsh != 0 {
lsh -=1;
}

I added it just to show more ways to perform subtraction without the compiler panicking.

Finally we modify our Output struct:

Bounds Checking

Page Up, Page Down, Home And End 🔄

We’ll now add functionality to move the cursor when these special keys are pressed.

Let’s begin with PageUp and PageDown. We’ll simulate the user pressing the or keys enough times to move to the top or bottom of the screen. Implementing Page Up and Page Down in this way will make it a lot easier for us later, when we implement scrolling.

Page Up And Down

If you’re on a laptop with an Fn key, you may be able to press Fn+↑ and Fn+↓ to simulate pressing the Page Up and Page Down keys.

For Home and End, it’s quite straightforward:

Home, End

If you’re on a laptop with an Fn key, you may be able to press Fn + ← and Fn + → to simulate pressing the Home and End keys.

That is all for this part. It’s been quite a run 😛.

In the next part, we will get our program to display text files, complete with vertical and horizontal scrolling and a status bar.

--

--

Kofi Otuo

Sr. Software Engineer in Systems, Mobile and Blockchain programming. I write coding tutorials. Reach Me: https://www.upwork.com/freelancers/~0196d30a485de56f48