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:
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:
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. ForANSI
terminals, the escape sequence forClearAll
is\x1b[2J
(you can useprint!("\x1b[2j”);stdout().flush();
to try it).x1b
is the escape character (similar to pressingEsc
on the keyboard) and it is followed by[
. TheJ
command (Erase In Display) to clear the screen. Escape sequence commands take arguments, which come before the command. In this case the argument is2
, 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 forJ
, so just<esc>[J
by itself would also clear the screen from the cursor to the end. Note thatcrossterm
provides an abstraction over various escape sequences
Now let’s put our function into effect
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.
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"
.
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:
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.
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.
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:
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 wasd
keys.
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()
:
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
:
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.
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:
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.