Previous chapter - Overview - Appendices - Next Chapter
In this chapter, we will tackle reading from and writing to the terminal. But first, we need to make our code more idiomatically. Beware! The beginning of this chapter will contain a lot of prose, which you can safely skip if you are not interested.
Writing idiomatic code
Whenever you deal with newer languages such as Rust, you will often hear about idiomatic code. Apparently, it is not enough to make the code solve a problem, it should be idiomatic as well. Let’s discuss first why this makes sense. Since the term idiomatic comes from languages, we start off with a linguistic example: If I told you that with this tutorial, you could kill two flies with one swat, because you learn Rust and write your very own editor at the same time, would you understand my meaning?
If you are a German, you would probably not have any trouble, because “Killing to flies with one swat” is a near-literal translation of a German saying. If you are Russian, “killing two rabbits with one shot” would be more understandable for you. But if you are not familiar with German or Russian, you would have to try and extract the meaning of these sentences out of the context. The idiomatic way of saying this in English is, of course, “To kill two birds with one stone”. The point is: Using the correct idiom in english makes sure everyone understands the meaning without thinking about it. If you speak unidiomatically, you force people to think about the wording you’re using, and not the arguments you’re making.
Writing idiomatic code is similar. It’s easier to maintain for others, since it sticks to certain rules and conventions, which are what the designers of the language had in mind. Your code is typically only reviewed when it doesn’t work - either because a feature is missing and someone wants to extend it, or because it has a bug. Making it easier for others to read and understand your code is generally a good idea.
We saw before that the Rust compiler can give you some advise on idiomatic code - for instance, when we prefixed a variable that we where not using with an underscore. My advise is to not ignore compiler warnings, your final code should always compile without warnings.
In this tutorial, though, we are sometimes adding functionality a bit ahead of time, which will cause Rust to complain about unreachable code. This is usually fixed a step or two later.
Let’s start this chapter by making our code a bit more idiomatic.
Read keys instead of bytes
In the previous steps, we have worked directly on bytes, which was both fun and valuable. However, at a certain point you should ask yourself if the funtionality you are implementing could not be replaced by a library function, as in many cases, someone else has already solved your problem, and probably better.
For me, handling with bit operations is a huge red flag that tells me that I am probably too deep down the rabbit hole.
Fortunately for us, our dependency, termion
, makes things already a lot easier, as it can already group individual bytes to keypresses and pass them to us. Let’s implement this.
@@ -1,11 +1,8 @@
|
|
1
|
-
use std::io::{self, stdout
|
1
|
+
use std::io::{self, stdout};
|
2
|
+
use termion::event::Key;
|
3
|
+
use termion::input::TermRead;
|
2
4
|
use termion::raw::IntoRawMode;
|
3
5
|
|
4
|
-
fn to_ctrl_byte(c: char) -> u8 {
|
5
|
-
let byte = c as u8;
|
6
|
-
byte & 0b0001_1111
|
7
|
-
}
|
8
|
-
|
9
6
|
fn die(e: std::io::Error) {
|
10
7
|
panic!(e);
|
11
8
|
}
|
@@ -13,19 +10,19 @@ fn die(e: std::io::Error) {
|
|
13
10
|
fn main() {
|
14
11
|
let _stdout = stdout().into_raw_mode().unwrap();
|
15
12
|
|
16
|
-
for
|
17
|
-
match
|
18
|
-
Ok(
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
for key in io::stdin().keys() {
|
14
|
+
match key {
|
15
|
+
Ok(key) => match key {
|
16
|
+
Key::Char(c) => {
|
17
|
+
if c.is_control() {
|
18
|
+
println!("{:?}\r", c as u8);
|
19
|
+
} else {
|
20
|
+
println!("{:?} ({})\r", c as u8, c);
|
21
|
+
}
|
25
|
-
if b == to_ctrl_byte('q') {
|
26
|
-
break;
|
27
22
|
}
|
28
|
-
|
23
|
+
Key::Ctrl('q') => break,
|
24
|
+
_ => println!("{:?}\r", key),
|
25
|
+
},
|
29
26
|
Err(err) => die(err),
|
30
27
|
}
|
31
28
|
}
|
We are now working with keys instead of bytes.
With that change, we where able to get rid of manually checking if Ctrl has been pressed, as all keys are now properly handled for us. Termion provides us with values which represent keys: Key::Char(c)
represents single character presses, Key::Ctrl(c)
represents all characters pressed together with Ctrl, Key::Alt(c)
represents all characters pressed together with Alt and so on.
We are still mainly interested in characters and in Ctrl-Q.
Note the subtle difference in the inner match: Key::Char(c)
matches any Character and binds it to the variable c
, whereas Key::Ctrl('q')
matches specifically Ctrl-q p.
Before the change, we were working with bytes which we converted to characters in order to print them out. Now, termion hands us the characters, so in order to print out the byte value, we use c as u8
for the conversion.
We have also added another case to our inner match
, and that is a special case: The case _
is called for every case that has not already been handled.
Matches need to be exhaustive, so every possibility must be handled. _
is the default option for everything that has not been handled before. In this case, if anything is pressed that is neither a character nor Ctrl-Q, we just print it out.
We also had to import a few things to make our code working. Similar as with into_raw_mode
, we need to import TermRead
so that we can use the keys
method on stdin
, but we could delete the import for std::io::Read
in return.
Separate the code into multiple files
It’s idiomatic that the main method itself does not do much more than providing the entry point for the app. This is the same in many programming languages, and Rust is no exception. We want the code to be placed where it makes sense, so that it’s easier to locate and maintain code later. There are more benefits as well, which I will explain when we encounter them.
The code we have is too low-level for now. We have to understand the whole code to understand that, in essence, it simply echoes any pressed key to the user and quits if Ctrl-Q is being pressed. Let’s improve this code by creating a code representation of our editor.
Let’s start with a new file:
@@ -0,0 +1,3 @@
|
|
1
|
+
pub struct Editor {
|
2
|
+
|
3
|
+
}
|
A struct
is a collection of variables and, eventually, functions which are grouped together to form a meaningful entity - in our case, the Editor (It’s not very meaningful yet, but we’ll get to that!). The pub
keyword tells us that this struct can be accessed from outside the editor.rs
. We want to use it from main
, so we use pub
. This is already the next advantage of separating our code: We can make sure that certain functions are only called internally, while we expose others to other parts of the system.
Now, our editor needs some functionality. Let’s provide it with a run()
function, like this:
@@ -1,3 +1,33 @@
|
|
1
|
-
|
1
|
+
use std::io::{self, stdout};
|
2
|
+
use termion::event::Key;
|
3
|
+
use termion::input::TermRead;
|
4
|
+
use termion::raw::IntoRawMode;
|
2
5
|
|
3
|
-
}
|
6
|
+
pub struct Editor {}
|
7
|
+
|
8
|
+
impl Editor {
|
9
|
+
pub fn run(&self) {
|
10
|
+
let _stdout = stdout().into_raw_mode().unwrap();
|
11
|
+
|
12
|
+
for key in io::stdin().keys() {
|
13
|
+
match key {
|
14
|
+
Ok(key) => match key {
|
15
|
+
Key::Char(c) => {
|
16
|
+
if c.is_control() {
|
17
|
+
println!("{:?}\r", c as u8);
|
18
|
+
} else {
|
19
|
+
println!("{:?} ({})\r", c as u8, c);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
Key::Ctrl('q') => break,
|
23
|
+
_ => println!("{:?}\r", key),
|
24
|
+
},
|
25
|
+
Err(err) => die(err),
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
fn die(e: std::io::Error) {
|
32
|
+
panic!(e);
|
33
|
+
}
|
You already know the implementation of run
- it’s copy-pasted from our main
, and so are the imports and die
.
Let’s focus on what’s new:
The impl
block contains function definition which can be called on the struct (We see how this works in a second). The function gets the pub
keyword, so we can call it from the outside. And the run
function accepts a parameter called &self
, which will contain a reference to the struct it was called upon (The &
before self
indicates that we are dealing with a reference). This is equivalent to having a function outside of the impl
block which accepts a &Editor
as the first parameter.
Let’s see this working in practice by refactoring our main.rs
:
@@ -1,29 +1,8 @@
|
|
1
|
-
|
1
|
+
mod editor;
|
2
|
-
use termion::event::Key;
|
3
|
-
use termion::input::TermRead;
|
4
|
-
use termion::raw::IntoRawMode;
|
5
2
|
|
6
|
-
|
3
|
+
use editor::Editor;
|
7
|
-
panic!(e);
|
8
|
-
}
|
9
4
|
|
10
5
|
fn main() {
|
11
|
-
let
|
12
|
-
|
6
|
+
let editor = Editor {};
|
7
|
+
editor.run();
|
13
|
-
for key in io::stdin().keys() {
|
14
|
-
match key {
|
15
|
-
Ok(key) => match key {
|
16
|
-
Key::Char(c) => {
|
17
|
-
if c.is_control() {
|
18
|
-
println!("{:?}\r", c as u8);
|
19
|
-
} else {
|
20
|
-
println!("{:?} ({})\r", c as u8, c);
|
21
|
-
}
|
22
|
-
}
|
23
|
-
Key::Ctrl('q') => break,
|
24
|
-
_ => println!("{:?}\r", key),
|
25
|
-
},
|
26
|
-
Err(err) => die(err),
|
27
|
-
}
|
28
|
-
}
|
29
8
|
}
|
As you can see, we have removed nearly everything from the main.rs
. We are creating a new instance of Editor
and we call run()
on it. If you run the code now, you should see that it works just fine.
Now, let’s make the last remaining lines of the main
a bit better. Structs allow us to group variables, but for now, our struct is empty - it does not contain any variables. As soon as we start adding things to the struct, we have to set all the fields as soon as we create a new Editor
. This means that for every new entry in Editor
, we have to go back to the main
and change the line let editor = editor::Editor{};
to set the new field values. That is not great, so let’s refactor that.
Here is the change:
@@ -26,6 +26,9 @@ impl Editor {
|
|
26
26
|
}
|
27
27
|
}
|
28
28
|
}
|
29
|
+
pub fn default() -> Self {
|
30
|
+
Editor{}
|
31
|
+
}
|
29
32
|
}
|
30
33
|
|
31
34
|
fn die(e: std::io::Error) {
|
@@ -3,6 +3,6 @@ mod editor;
|
|
3
3
|
use editor::Editor;
|
4
4
|
|
5
5
|
fn main() {
|
6
|
-
let editor = Editor
|
6
|
+
let editor = Editor::default();
|
7
7
|
editor.run();
|
8
8
|
}
|
We have now created a new function called default
, which constructs a new Editor
for us. Note that the one line in default
does not contain the keyword return
, and it does not end with a ;
. Rust treats the result of the last line in a function as its output, and by omitting the ;
, we are telling rust that we are interested in the value of that line, and not only in executing it. Play around with that by adding the ;
and seeing what happens.
Unlike run
, default
is not called on an already-existing Editor
instance. This is indicated by the missing &self
parameter in the function signature of default
. This is called a static
method, and these are called by using ::
as follows: Editor::default()
.
Now, we can leave the main.rs
alone while we focus on the editor.rs
.
“It looks like you’re writing a program, would you like help?”
Let’s conclude our detour towards idiomatic code by using another very useful feature: Clippy. Clippy is both an annoying Windows 95 feature, and a mechanism to point out possible improvements in our code. You can run it from the command line by executing:
$ cargo clippy
Running clippy now does not produce a result - our code is good enough to pass Clippy’s default flags. However, that’s not good enough for us - we want Clippy to annoy us, so that we can learn from it. First, we run cargo clean
, as Clippy only creates output during compilation, and as we saw earlier, Cargo only compiles changed files.
$ cargo clean
$ cargo clippy -- -W clippy::pedantic
The output is now:
Compiling libc v0.2.62
Checking numtoa v0.1.0
Checking termion v1.5.3
Checking hecto v0.1.0 (/home/philipp/repositories/hecto)
warning: unnecessary structure name repetition
--> src/editor.rs:30:9
|
30 | Editor{}
| ^^^^^^ help: use the applicable keyword: `Self`
|
= note: `-W clippy::use-self` implied by `-W clippy::pedantic`
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#use_self
Finished dev [unoptimized + debuginfo] target(s) in 6.16s
Not only does Clippy point out a weakness in our code, it also provides a link to the documentation, so that we can read all about that error. That’s great!
We can tell Clippy which flags we want to be used by default, for instance by adding code to our main.rs
. Let’s do this, and also fix the issue it pointed out:
@@ -27,7 +27,7 @@ impl Editor {
|
|
27
27
|
}
|
28
28
|
}
|
29
29
|
pub fn default() -> Self {
|
30
|
-
|
30
|
+
Self {}
|
31
31
|
}
|
32
32
|
}
|
33
33
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
#![warn(clippy::all, clippy::pedantic)]
|
1
2
|
mod editor;
|
2
3
|
|
3
4
|
use editor::Editor;
|
The pedantic setting is really valuable for beginners: As we don’t know yet how to write code idiomatically, we need someone at our side who points out how things could be done better.
Separate reading from evaluating
Let’s make a function for keypress reading, and another function for mapping keypresses to editor operations. We’ll also stop printing out keypresses at this point.
@@ -9,26 +9,31 @@ impl Editor {
|
|
9
9
|
pub fn run(&self) {
|
10
10
|
let _stdout = stdout().into_raw_mode().unwrap();
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
loop {
|
13
|
+
if let Err(error) = self.process_keypress() {
|
14
|
+
die(error);
|
15
|
-
Key::Char(c) => {
|
16
|
-
if c.is_control() {
|
17
|
-
println!("{:?}\r", c as u8);
|
18
|
-
} else {
|
19
|
-
println!("{:?} ({})\r", c as u8, c);
|
20
|
-
}
|
21
|
-
}
|
22
|
-
Key::Ctrl('q') => break,
|
23
|
-
_ => println!("{:?}\r", key),
|
24
|
-
},
|
25
|
-
Err(err) => die(err),
|
26
15
|
}
|
27
16
|
}
|
28
17
|
}
|
29
18
|
pub fn default() -> Self {
|
30
19
|
Self {}
|
31
20
|
}
|
21
|
+
fn process_keypress(&self) -> Result<(), std::io::Error> {
|
22
|
+
let pressed_key = read_key()?;
|
23
|
+
match pressed_key {
|
24
|
+
Key::Ctrl('q') => panic!("Program end"),
|
25
|
+
_ => (),
|
26
|
+
}
|
27
|
+
Ok(())
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
fn read_key() -> Result<Key, std::io::Error> {
|
32
|
+
loop {
|
33
|
+
if let Some(key) = io::stdin().lock().keys().next() {
|
34
|
+
return key;
|
35
|
+
}
|
36
|
+
}
|
32
37
|
}
|
33
38
|
|
34
39
|
fn die(e: std::io::Error) {
|
We have now added a loop
to run
. Loops are repeated forever until they are explicitly interrupted.
Within that loop we use another feature of Rust: if let
. This is a shortcut for using a match
where we only want to handle one case and ignore all other possible cases. Look at the code of process_keypress()
to see a case of match
which could be fully replaced by if let
.
In run
, we execute self.process_keypress()
and see if the result matches Err
. If so, we pass the unwrapped error to die
, if not, nothing happens.
We can see this more clearly while investigating the signature of process_keypress
:
fn process_keypress(&self) -> Result<(), std::io::Error>
The part behind the ->
says: This function returns a Result
. The stuff in <>
tells us what to expect as contents of Ok
and Err
, respectively: Ok
will be wrapping ()
, which means “Nothing”, and Err
will be wrapping std::io::Error
.
process_keypress()
waits for a keypress, and then handles it. Later, it will map various Ctrl key combinations and other special keys to different editor functions, and insert any alphanumeric and other printable keys’ characters into the text that is being edited. That’s why we are using match
instead of if let
here.
The last line of this function is a bit difficult to understand for beginners. Conceptually, we don’t want the function to return anything. So why the Ok(())
? The thing is: Since an error can occur when calling read_key
, we want to pass that error up to the calling function. Since we don’t have a try..catch
, we have to return something that says “Everything is OK”, even though we are not returning any value. That is precisely what Ok(())
does: It says “Everything is OK, and nothing has been returned”.
But what if something goes wrong? Well, we can tell by the signature of read_key
that an error can be passed to us. If that’s the case, there’s no point in continuing, we want the error to be returned as well. But in case no error occured, we want to continue with the unwrapped value.
That’s what the question mark after read_key
does for us: If there’s an error, return it, if not, unwrap the value and continue.
Try playing around with these concepts by removing the ?
, or the Ok(())
, or by changing the return value.
read_key
also includes a loop. In that case, the loop is repeated until the function returns, that is, as soon a valid key has been pressed. The value returned by io::stdin().lock().keys().next()
is very similar to the Result
we just discussed - It’s a so-called Option
. We will use Option
s in depth later. For now, it’s enough to understand that an Option
can be None
- meaning in this case that no key has been pressed and continuing the loop. Or it can wrap a value with Some
, in which case we return that unwrapped value from read_key
.
What makes this slightly more complicated is that the actual return value of io::stdin().lock().keys().next()
is a Key
wrapped inside of a Result
which, in turn, is wrapped inside an Option
. We unwrap the Option
in read_key()
, and the Result
in process_keypress()
.
That is how the error makes its way into run
, where it is finally handled by die
. Speaking of die
, there is a new ugly wart in our code: Because we don’t know yet how to exit our code from within the program, we are panic
king now when the user uses Ctrl-Q.
We could instead call the proper method to end the program (std::process::exit
, in case you are interested), but similar to how we do not want our program to crash randomly deep within our code, we also don’t want it to exit somewhere deep down, but in run
. We solve this by adding our first element to the Editor
struct: a boolean which indicates if the user wants to quit.
@@ -3,7 +3,9 @@ use termion::event::Key;
|
|
3
3
|
use termion::input::TermRead;
|
4
4
|
use termion::raw::IntoRawMode;
|
5
5
|
|
6
|
-
pub struct Editor {
|
6
|
+
pub struct Editor {
|
7
|
+
should_quit: bool,
|
8
|
+
}
|
7
9
|
|
8
10
|
impl Editor {
|
9
11
|
pub fn run(&self) {
|
@@ -16,7 +18,7 @@ impl Editor {
|
|
16
18
|
}
|
17
19
|
}
|
18
20
|
pub fn default() -> Self {
|
19
|
-
Self {}
|
21
|
+
Self { should_quit: false }
|
20
22
|
}
|
21
23
|
fn process_keypress(&self) -> Result<(), std::io::Error> {
|
22
24
|
let pressed_key = read_key()?;
|
We have to initialize should_quit
in default
right away, or we won’t be able to compile our code. Let’s set the boolean now and quit the program when it is true
.
@@ -8,22 +8,25 @@ pub struct Editor {
|
|
8
8
|
}
|
9
9
|
|
10
10
|
impl Editor {
|
11
|
-
pub fn run(&self) {
|
11
|
+
pub fn run(&mut self) {
|
12
12
|
let _stdout = stdout().into_raw_mode().unwrap();
|
13
13
|
|
14
14
|
loop {
|
15
15
|
if let Err(error) = self.process_keypress() {
|
16
16
|
die(error);
|
17
17
|
}
|
18
|
+
if self.should_quit {
|
19
|
+
break;
|
20
|
+
}
|
18
21
|
}
|
19
22
|
}
|
20
23
|
pub fn default() -> Self {
|
21
24
|
Self { should_quit: false }
|
22
25
|
}
|
23
|
-
fn process_keypress(&self) -> Result<(), std::io::Error> {
|
26
|
+
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
24
27
|
let pressed_key = read_key()?;
|
25
28
|
match pressed_key {
|
26
|
-
Key::Ctrl('q') =>
|
29
|
+
Key::Ctrl('q') => self.should_quit = true,
|
27
30
|
_ => (),
|
28
31
|
}
|
29
32
|
Ok(())
|
@@ -4,6 +4,5 @@ mod editor;
|
|
4
4
|
use editor::Editor;
|
5
5
|
|
6
6
|
fn main() {
|
7
|
-
|
7
|
+
Editor::default().run();
|
8
|
-
editor.run();
|
9
8
|
}
|
Instead of panicking, we are now setting should_quit
, which we check in run
. If it’s true
, we use the keyword break
to end the loop. You should confirm that exiting the program is now cleaner than it was before.
In addition to this change, we had to do a couple of other things. Since we are mutating self
now in process_keypress()
, we had to change &self
to &mut self
in the signature. This indicates that we intend to mutate the reference we’re having. Rust is very strict about mutable references, as we will see later.
Similarily, we had to change the signature from run
, since we call process_keypress()
from within.
Last but not least, we had to change main
. let editor = ...
indicates that editor
is a read-only reference, so we can’t run
on it, which mutates editor
. We could have solved this by changing it to let mut editor
. Instead, since we’re not doing anything with editor
, we have now removed the extra variable and are calling run()
now directly on the return value of default()
.
Now we have simplified run()
, and we will try to keep it that way.
Clear the screen
We’re going to render the editor’s user interface to the screen after each keypress. Let’s start by just clearing the screen.
@@ -1,4 +1,4 @@
|
|
1
|
-
use std::io::{self, stdout};
|
1
|
+
use std::io::{self, stdout, Write};
|
2
2
|
use termion::event::Key;
|
3
3
|
use termion::input::TermRead;
|
4
4
|
use termion::raw::IntoRawMode;
|
@@ -12,17 +12,25 @@ impl Editor {
|
|
12
12
|
let _stdout = stdout().into_raw_mode().unwrap();
|
13
13
|
|
14
14
|
loop {
|
15
|
-
if let Err(error) = self.
|
15
|
+
if let Err(error) = self.refresh_screen() {
|
16
16
|
die(error);
|
17
17
|
}
|
18
18
|
if self.should_quit {
|
19
19
|
break;
|
20
20
|
}
|
21
|
+
if let Err(error) = self.process_keypress() {
|
22
|
+
die(error);
|
23
|
+
}
|
21
24
|
}
|
22
25
|
}
|
23
26
|
pub fn default() -> Self {
|
24
27
|
Self { should_quit: false }
|
25
28
|
}
|
29
|
+
|
30
|
+
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
31
|
+
print!("\x1b[2J");
|
32
|
+
io::stdout().flush()
|
33
|
+
}
|
26
34
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
27
35
|
let pressed_key = read_key()?;
|
28
36
|
match pressed_key {
|
We add a new function refresh_screen
which we are calling before exiting the program. We move process_keypress()
down, which means that after a user exits the program, we still refresh the screen one more time before exiting. This will allow us to print an exit message later.
To clear the screen, we use print
to write 4
bytes out to the terminal. The first byte is \x1b
, which is the escape character, or 27
in decimal. The other three bytes are [2J
.
We are writing an escape sequence to the terminal. Escape sequences always start with an escape character (27
, which, as we saw earlier, is also produced by Esc) followed by a [
character. Escape sequences instruct the terminal to do various text formatting tasks, such as coloring text, moving the cursor around, and clearing parts of the screen.
We are using 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 says to clear the entire screen. \x1b[1J
would clear the screen up to where the cursor is, and \x1b[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 \x1b[J
by itself would also clear the screen from the cursor to the end.
In this tutorial, we will be mostly looking at VT100 escape sequences, which are supported very widely by modern terminal emulators. See the VT100 User Guide for complete documentation of each escape sequence.
After writing out to the terminal, we call flush()
, which forces stdout
to print out everything it has (it might buffer some values and not print them out directly). We are also returning the result of flush()
, which, similar as above, either wraps nothing or an error in case the flushing failed. This is hard to miss: Had we added a ;
after flush()
, we would not have returned its result.
termion
eliminates the need for us to write the escape sequences directly to the terminal ourselves, so let’s change our code as follows:
@@ -28,7 +28,7 @@ impl Editor {
|
|
28
28
|
}
|
29
29
|
|
30
30
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
31
|
-
print!("
|
31
|
+
print!("{}", termion::clear::All);
|
32
32
|
io::stdout().flush()
|
33
33
|
}
|
34
34
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
From here on out, we will be using termion
directly in the code instead of the escape characters.
By the way, since we are now clearing the screen every time we run the program, we might be missing out on valuable tips the compiler might give us. Don’t forget that you can run cargo build
seperately to take a look at the warnings. Remember, though, that Rust does not recompile your code if it hasn’t changed, so running cargo build
immediately after cargo run
won’t give you the same warnings. Run cargo clean
and then run cargo build
to recompile the whole project and get all the warnings.
Reposition the cursor
You may notice that the \x1b[2J
command left the cursor at the bottom of the screen. Let’s reposition it at the top-left corner so that we’re ready to draw the editor interface from top to bottom.
@@ -28,7 +28,7 @@ impl Editor {
|
|
28
28
|
}
|
29
29
|
|
30
30
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
31
|
-
print!("{}", termion::clear::All);
|
31
|
+
print!("{}{}", termion::clear::All, termion::cursor::Goto(1, 1));
|
32
32
|
io::stdout().flush()
|
33
33
|
}
|
34
34
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
The escape sequence behind termion::cursor::Goto
uses the H
command (Cursor Position) to position the cursor. The H
command actually takes two arguments: the row number and the column number at which to position the cursor. So if you have an 80×24 size terminal and you want the cursor in the center of the screen, you could use the command \x1b[12;40H
. (Multiple arguments are separated by a ;
character.) As rows and columns are numbered starting at 1
, not 0
, the termion
method is also 1-based.
Clear the screen on exit
Let’s clear the screen and reposition the cursor when our program crashes. If an error occurs in the middle of rendering the screen, we don’t want a bunch of garbage left over on the screen, and we don’t want the error to be printed wherever the cursor happens to be at that point. We also take the opportunity to print out a farewell message in case the user decides to leave hecto
.
@@ -29,6 +29,9 @@ impl Editor {
|
|
29
29
|
|
30
30
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
31
31
|
print!("{}{}", termion::clear::All, termion::cursor::Goto(1, 1));
|
32
|
+
if self.should_quit {
|
33
|
+
println!("Goodbye.\r");
|
34
|
+
}
|
32
35
|
io::stdout().flush()
|
33
36
|
}
|
34
37
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
@@ -50,5 +53,6 @@ fn read_key() -> Result<Key, std::io::Error> {
|
|
50
53
|
}
|
51
54
|
|
52
55
|
fn die(e: std::io::Error) {
|
56
|
+
print!("{}", termion::clear::All);
|
53
57
|
panic!(e);
|
54
58
|
}
|
Tildes
It’s time to start 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 lines that come after the end of the file being edited.
@@ -31,6 +31,9 @@ impl Editor {
|
|
31
31
|
print!("{}{}", termion::clear::All, termion::cursor::Goto(1, 1));
|
32
32
|
if self.should_quit {
|
33
33
|
println!("Goodbye.\r");
|
34
|
+
} else {
|
35
|
+
self.draw_rows();
|
36
|
+
print!("{}", termion::cursor::Goto(1, 1));
|
34
37
|
}
|
35
38
|
io::stdout().flush()
|
36
39
|
}
|
@@ -42,6 +45,11 @@ impl Editor {
|
|
42
45
|
}
|
43
46
|
Ok(())
|
44
47
|
}
|
48
|
+
fn draw_rows(&self) {
|
49
|
+
for _ in 0..24 {
|
50
|
+
println!("~\r");
|
51
|
+
}
|
52
|
+
}
|
45
53
|
}
|
46
54
|
|
47
55
|
fn read_key() -> Result<Key, std::io::Error> {
|
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.
We don’t know the size of the terminal yet, so we don’t know how many rows to draw. For now we just draw 24
rows (The _
in for..in
indicates that we are not interested in any value, we just want to repeat the command a bunch of times)
After we’re done drawing, we reposition the cursor back up at the top-left corner.
Window size
Our next goal is to get the size of the terminal, so we know how many rows to draw in editor_draw_rows()
. It turns out that termion
provides us with a method to get the screen size. We are going to use that in a new data structure which represents the terminal. We place it in a new file called terminal.rs
.
@@ -1,3 +1,4 @@
|
|
1
|
+
use crate::Terminal;
|
1
2
|
use std::io::{self, stdout, Write};
|
2
3
|
use termion::event::Key;
|
3
4
|
use termion::input::TermRead;
|
@@ -5,6 +6,7 @@ use termion::raw::IntoRawMode;
|
|
5
6
|
|
6
7
|
pub struct Editor {
|
7
8
|
should_quit: bool,
|
9
|
+
terminal: Terminal,
|
8
10
|
}
|
9
11
|
|
10
12
|
impl Editor {
|
@@ -24,7 +26,10 @@ impl Editor {
|
|
24
26
|
}
|
25
27
|
}
|
26
28
|
pub fn default() -> Self {
|
27
|
-
Self {
|
29
|
+
Self {
|
30
|
+
should_quit: false,
|
31
|
+
terminal: Terminal::default().expect("Failed to initialize terminal"),
|
32
|
+
}
|
28
33
|
}
|
29
34
|
|
30
35
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
@@ -46,7 +51,7 @@ impl Editor {
|
|
46
51
|
Ok(())
|
47
52
|
}
|
48
53
|
fn draw_rows(&self) {
|
49
|
-
for _ in 0..
|
54
|
+
for _ in 0..self.terminal.size().height {
|
50
55
|
println!("~\r");
|
51
56
|
}
|
52
57
|
}
|
@@ -1,7 +1,8 @@
|
|
1
1
|
#![warn(clippy::all, clippy::pedantic)]
|
2
2
|
mod editor;
|
3
|
-
|
3
|
+
mod terminal;
|
4
4
|
use editor::Editor;
|
5
|
+
pub use terminal::Terminal;
|
5
6
|
|
6
7
|
fn main() {
|
7
8
|
Editor::default().run();
|
@@ -0,0 +1,22 @@
|
|
1
|
+
pub struct Size {
|
2
|
+
pub width: u16,
|
3
|
+
pub height: u16,
|
4
|
+
}
|
5
|
+
pub struct Terminal {
|
6
|
+
size: Size,
|
7
|
+
}
|
8
|
+
|
9
|
+
impl Terminal {
|
10
|
+
pub fn default() -> Result<Self, std::io::Error> {
|
11
|
+
let size = termion::terminal_size()?;
|
12
|
+
Ok(Self {
|
13
|
+
size: Size {
|
14
|
+
width: size.0,
|
15
|
+
height: size.1,
|
16
|
+
},
|
17
|
+
})
|
18
|
+
}
|
19
|
+
pub fn size(&self) -> &Size {
|
20
|
+
&self.size
|
21
|
+
}
|
22
|
+
}
|
Let’s focus first on the contents of the new file. In it, we define Terminal
and a helper struct called Size
. In default
, we are getting termion’s terminal_size
, convert it to a Size
and return the new instance of Terminal
. To account for the potential error, we wrap it into Ok
.
We also don’t want callers from the outside to modify the terminal size. So we do not mark size
as public with pub
. Instead, we add a method called size
which returns a read-only reference to our internal size
.
Let’s quickly discuss the data types here. Size
. width
and height
are both u16
s, which is an unsigned 16 bit integer and ends at around 65,000. That makes sense for the terminal, at least for virtually every terminal I have ever seen.
Now that we have seen the new struct, let’s investigate how it is referenced from the editor. First, we introduce our new struct in main.rs
the same as we did with editor
. Then, we say that we want to use the Terminal
struct and add a pub
before that statement. What does that do?
In editor.rs
, we can now import the terminal with use crate::Terminal
. Without the pub use
statement in main.rs
, we could not have done it like that, instead we would have needed to use use crate::terminal::Terminal
. In essence, we are re-exporting the Terminal
struct at the top level and make it reachable via crate::Terminal
.
In our editor struct, we are adding a reference to our terminal while initializing the editor in default()
. Remember that Terminal::default
returns a Terminal or an Error. We unwrap the Terminal with expect
, which does the following: If we have a value, we return it. If we don’t have a value, we panic with the text passed to expect
. We don’t need to die
here, since die
is mostly useful while we are repeatadly drawing to the screen.
There are other ways to handle that error. We could have used expect
also within Terminal
- but that’s not the point. The point is that Rust forces you to think about it very early - you have to make a conscious decision on what to do, or the program won’t even compile.
Now that we have a representation of the terminal in our code, let’s move a bit of code there.
@@ -1,8 +1,5 @@
|
|
1
1
|
use crate::Terminal;
|
2
|
-
use std::io::{self, stdout, Write};
|
3
2
|
use termion::event::Key;
|
4
|
-
use termion::input::TermRead;
|
5
|
-
use termion::raw::IntoRawMode;
|
6
3
|
|
7
4
|
pub struct Editor {
|
8
5
|
should_quit: bool,
|
@@ -11,8 +8,6 @@ pub struct Editor {
|
|
11
8
|
|
12
9
|
impl Editor {
|
13
10
|
pub fn run(&mut self) {
|
14
|
-
let _stdout = stdout().into_raw_mode().unwrap();
|
15
|
-
|
16
11
|
loop {
|
17
12
|
if let Err(error) = self.refresh_screen() {
|
18
13
|
die(error);
|
@@ -33,17 +28,18 @@ impl Editor {
|
|
33
28
|
}
|
34
29
|
|
35
30
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
36
|
-
|
31
|
+
Terminal::clear_screen();
|
32
|
+
Terminal::cursor_position(0, 0);
|
37
33
|
if self.should_quit {
|
38
34
|
println!("Goodbye.\r");
|
39
35
|
} else {
|
40
36
|
self.draw_rows();
|
41
|
-
|
37
|
+
Terminal::cursor_position(0, 0);
|
42
38
|
}
|
43
|
-
|
39
|
+
Terminal::flush()
|
44
40
|
}
|
45
41
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
46
|
-
let pressed_key = read_key()?;
|
42
|
+
let pressed_key = Terminal::read_key()?;
|
47
43
|
match pressed_key {
|
48
44
|
Key::Ctrl('q') => self.should_quit = true,
|
49
45
|
_ => (),
|
@@ -57,15 +53,7 @@ impl Editor {
|
|
57
53
|
}
|
58
54
|
}
|
59
55
|
|
60
|
-
fn read_key() -> Result<Key, std::io::Error> {
|
61
|
-
loop {
|
62
|
-
if let Some(key) = io::stdin().lock().keys().next() {
|
63
|
-
return key;
|
64
|
-
}
|
65
|
-
}
|
66
|
-
}
|
67
|
-
|
68
56
|
fn die(e: std::io::Error) {
|
69
|
-
|
57
|
+
Terminal::clear_screen();
|
70
58
|
panic!(e);
|
71
59
|
}
|
@@ -1,9 +1,15 @@
|
|
1
|
+
use std::io::{self, stdout, Write};
|
2
|
+
use termion::event::Key;
|
3
|
+
use termion::input::TermRead;
|
4
|
+
use termion::raw::{IntoRawMode, RawTerminal};
|
5
|
+
|
1
6
|
pub struct Size {
|
2
7
|
pub width: u16,
|
3
8
|
pub height: u16,
|
4
9
|
}
|
5
10
|
pub struct Terminal {
|
6
11
|
size: Size,
|
12
|
+
_stdout: RawTerminal<std::io::Stdout>,
|
7
13
|
}
|
8
14
|
|
9
15
|
impl Terminal {
|
@@ -14,9 +20,28 @@ impl Terminal {
|
|
14
20
|
width: size.0,
|
15
21
|
height: size.1,
|
16
22
|
},
|
23
|
+
_stdout: stdout().into_raw_mode()?,
|
17
24
|
})
|
18
25
|
}
|
19
26
|
pub fn size(&self) -> &Size {
|
20
27
|
&self.size
|
21
28
|
}
|
29
|
+
pub fn clear_screen() {
|
30
|
+
print!("{}", termion::clear::All);
|
31
|
+
}
|
32
|
+
pub fn cursor_position(x: u16, y: u16) {
|
33
|
+
let x = x.saturating_add(1);
|
34
|
+
let y = y.saturating_add(1);
|
35
|
+
print!("{}", termion::cursor::Goto(x, y));
|
36
|
+
}
|
37
|
+
pub fn flush() -> Result<(), std::io::Error> {
|
38
|
+
io::stdout().flush()
|
39
|
+
}
|
40
|
+
pub fn read_key() -> Result<Key, std::io::Error> {
|
41
|
+
loop {
|
42
|
+
if let Some(key) = io::stdin().lock().keys().next() {
|
43
|
+
return key;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
22
47
|
}
|
What did we do? We moved all the low-level terminal stuff to Terminal
, leaving all the higher-level stuff in the mod.rs
. Along the way, we have cleaned up a few things:
- We do not need to keep track of
stdout
for the raw mode in the editor. This is handled internally inTerminal
now - as long as theTerminal
struct lives,_stdout
will be present. - We have hidden the fact that the terminal is 1-based from the caller by making
Terminal::cursor_position
0-based. - We are preventing an overflow of our
u16
incursor_position
.
A few quick words on overflowing: Our types have a maximum size which they can take. As mentioned before, this limit lies around 65,000 for u16
. So what happens if you add 1 to the maximum value? It becomes the smallest possible value, so in the case of an unsigned type, 0! This is called an overflow. Why does this happen? Let’s consider a 3-bit datatype. We start at the value 000
, which encodes 0, and whenever we want to add 1, we use the following algorithm:
- Starting at the right, if you see a
0
, flip it to a1
. - If you see a
1
, flip it to a0
, move one step to the left and repeat.
This gives us the following sequence:
Number | Binary |
---|---|
0 | 000 |
1 | 001 |
2 | 010 |
3 | 011 |
4 | 100 |
5 | 101 |
6 | 110 |
7 | 111 |
Now what happens if we try to add 1 more? All the 1s are flipped to 0, but there is no bit left over to flip to 1. So we are back at 000
, which is 0.
By the way, the normal handling of overflows in Rust is as follows: In debug mode (which we are using by default), the program crashes. This is what you want: The compiler should not try to keep your program alive, but slap the bug in your face. In production mode, an overflow occurs. This is also what you want, because you don’t want to crash your application unexpectedly in production, instead it could make sense continuing with the overflown values. In our case, it would mean that the cursor is not placed at the bottom or right of the screen, but at the left, which would be annoying, but not enough to warrant a crash. Luckily, our new code avoids this anyways: We use saturating_add
, which attempts to add 1
, and if that’s not possible, it just returns the maximum value.
(If you want to try out the overflow logic of Rust: you can build your application for production with cargo build --release
, which will place the production executable in target/release
).
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"
like on any other line, but this causes the terminal to scroll in order to make room for a new, blank line. Since we want to have a status bar at the bottom later anyways, let’s just change the range in which we are drawing rows for now. We will revisit this later and make this more robust against overflows/underflows, but for now let’s focus on getting this working.
@@ -47,7 +47,7 @@ impl Editor {
|
|
47
47
|
Ok(())
|
48
48
|
}
|
49
49
|
fn draw_rows(&self) {
|
50
|
-
for _ in 0..self.terminal.size().height {
|
50
|
+
for _ in 0..self.terminal.size().height - 1 {
|
51
51
|
println!("~\r");
|
52
52
|
}
|
53
53
|
}
|
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.
@@ -28,6 +28,7 @@ impl Editor {
|
|
28
28
|
}
|
29
29
|
|
30
30
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
31
|
+
Terminal::cursor_hide();
|
31
32
|
Terminal::clear_screen();
|
32
33
|
Terminal::cursor_position(0, 0);
|
33
34
|
if self.should_quit {
|
@@ -36,6 +37,7 @@ impl Editor {
|
|
36
37
|
self.draw_rows();
|
37
38
|
Terminal::cursor_position(0, 0);
|
38
39
|
}
|
40
|
+
Terminal::cursor_show();
|
39
41
|
Terminal::flush()
|
40
42
|
}
|
41
43
|
fn process_keypress(&mut self) -> Result<(), std::io::Error> {
|
@@ -44,4 +44,10 @@ impl Terminal {
|
|
44
44
|
}
|
45
45
|
}
|
46
46
|
}
|
47
|
+
pub fn cursor_hide() {
|
48
|
+
print!("{}", termion::cursor::Hide);
|
49
|
+
}
|
50
|
+
pub fn cursor_show() {
|
51
|
+
print!("{}", termion::cursor::Show);
|
52
|
+
}
|
47
53
|
}
|
Under the hood, we use escape sequences to tell the terminal to hide and show the cursor by writing \x1b[25h
, the h
command (Set Mode) and \x1b[25l
, the l
command (Reset Mode). These commands are used to turn on and turn off various terminal features or “modes”. The VT100 User Guide just linked to doesn’t document argument ?25
which we use above. It appears the cursor hiding/showing feature appeared in later VT models.
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 replace termion::clear::All
(clear entire screen) escape sequence with \x1b[K
sequence at the beginning of each line we draw with termion::clear::CurrentLine
.
@@ -29,9 +29,9 @@ impl Editor {
|
|
29
29
|
|
30
30
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
31
31
|
Terminal::cursor_hide();
|
32
|
-
Terminal::clear_screen();
|
33
32
|
Terminal::cursor_position(0, 0);
|
34
33
|
if self.should_quit {
|
34
|
+
Terminal::clear_screen();
|
35
35
|
println!("Goodbye.\r");
|
36
36
|
} else {
|
37
37
|
self.draw_rows();
|
@@ -50,6 +50,7 @@ impl Editor {
|
|
50
50
|
}
|
51
51
|
fn draw_rows(&self) {
|
52
52
|
for _ in 0..self.terminal.size().height - 1 {
|
53
|
+
Terminal::clear_current_line();
|
53
54
|
println!("~\r");
|
54
55
|
}
|
55
56
|
}
|
@@ -50,4 +50,7 @@ impl Terminal {
|
|
50
50
|
pub fn cursor_show() {
|
51
51
|
print!("{}", termion::cursor::Show);
|
52
52
|
}
|
53
|
+
pub fn clear_current_line() {
|
54
|
+
print!("{}", termion::clear::CurrentLine);
|
55
|
+
}
|
53
56
|
}
|
Note that we are now clearing the screen before displaying our goodbye message, to avoid the effect of showing the message on top of the other lines before the program finally terminates.
Welcome message
Perhaps it’s time to display a welcome message. Let’s display the name of our editor and a version number a third of the way down the screen.
@@ -1,6 +1,8 @@
|
|
1
1
|
use crate::Terminal;
|
2
2
|
use termion::event::Key;
|
3
3
|
|
4
|
+
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
5
|
+
|
4
6
|
pub struct Editor {
|
5
7
|
should_quit: bool,
|
6
8
|
terminal: Terminal,
|
@@ -49,9 +51,14 @@ impl Editor {
|
|
49
51
|
Ok(())
|
50
52
|
}
|
51
53
|
fn draw_rows(&self) {
|
52
|
-
|
54
|
+
let height = self.terminal.size().height;
|
55
|
+
for row in 0..height - 1 {
|
53
56
|
Terminal::clear_current_line();
|
54
|
-
|
57
|
+
if row == height / 3 {
|
58
|
+
println!("Hecto editor -- version {}\r", VERSION)
|
59
|
+
} else {
|
60
|
+
println!("~\r");
|
61
|
+
}
|
55
62
|
}
|
56
63
|
}
|
57
64
|
}
|
We have added a constant called VERSION
to our code. Since our Cargo.toml
already contains our version number, we use the env!
macro to retrieve it. We add it to our welcome message.
However, we need to deal with the fact that our message might be cut off due to the terminal size. We do that now.
@@ -55,7 +55,9 @@ impl Editor {
|
|
55
55
|
for row in 0..height - 1 {
|
56
56
|
Terminal::clear_current_line();
|
57
57
|
if row == height / 3 {
|
58
|
-
|
58
|
+
let welcome_message = format!("Hecto editor -- version {}", VERSION);
|
59
|
+
let width = std::cmp::min(self.terminal.size().width as usize, welcome_message.len());
|
60
|
+
println!("{}\r", &welcome_message[..width])
|
59
61
|
} else {
|
60
62
|
println!("~\r");
|
61
63
|
}
|
The [...width]
syntax means that we want to slice the string from its beginning until width
. width
has been calculated as the minimum of the screen width or the welcome message length, which makes sure that we are never slicing more of a string than what is already there.
Now let’s center the welcome message, and while we’re at it, let’s move our code to draw the welcome message to a separate function.
@@ -50,14 +50,22 @@ impl Editor {
|
|
50
50
|
}
|
51
51
|
Ok(())
|
52
52
|
}
|
53
|
+
fn draw_welcome_message(&self) {
|
54
|
+
let mut welcome_message = format!("Hecto editor -- version {}", VERSION);
|
55
|
+
let width = self.terminal.size().width as usize;
|
56
|
+
let len = welcome_message.len();
|
57
|
+
let padding = width.saturating_sub(len) / 2;
|
58
|
+
let spaces = " ".repeat(padding.saturating_sub(1));
|
59
|
+
welcome_message = format!("~{}{}", spaces, welcome_message);
|
60
|
+
welcome_message.truncate(width);
|
61
|
+
println!("{}\r", welcome_message);
|
62
|
+
}
|
53
63
|
fn draw_rows(&self) {
|
54
64
|
let height = self.terminal.size().height;
|
55
65
|
for row in 0..height - 1 {
|
56
66
|
Terminal::clear_current_line();
|
57
67
|
if row == height / 3 {
|
58
|
-
|
68
|
+
self.draw_welcome_message();
|
59
|
-
let width = std::cmp::min(self.terminal.size().width as usize, welcome_message.len());
|
60
|
-
println!("{}\r", &welcome_message[..width])
|
61
69
|
} else {
|
62
70
|
println!("~\r");
|
63
71
|
}
|
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: width/2 - welcome_len/2
, which simplifies to (width - 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. repeat
is a nice helper function which repeats the character we pass to i, and truncate
shortenes a string to a specific width if necessary.
You should be able to confirm that it’s working: If the string is too wide, it’s being truncated, otherwise the welcome string is centered.
Move the cursor
Let’s focus on input now. We want the user to be able to move the cursor around. The first step is to keep track of the cursor’s x
and y
position in the editor state. We’re going to add another struct
to help us with that.
@@ -3,9 +3,15 @@ use termion::event::Key;
|
|
3
3
|
|
4
4
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
5
5
|
|
6
|
+
struct Position {
|
7
|
+
x: usize,
|
8
|
+
y: usize,
|
9
|
+
}
|
10
|
+
|
6
11
|
pub struct Editor {
|
7
12
|
should_quit: bool,
|
8
13
|
terminal: Terminal,
|
14
|
+
cursor_position: Position,
|
9
15
|
}
|
10
16
|
|
11
17
|
impl Editor {
|
@@ -26,6 +32,7 @@ impl Editor {
|
|
26
32
|
Self {
|
27
33
|
should_quit: false,
|
28
34
|
terminal: Terminal::default().expect("Failed to initialize terminal"),
|
35
|
+
cursor_position: Position { x: 0, y: 0 },
|
29
36
|
}
|
30
37
|
}
|
31
38
|
|
cursor_position
is a struct where x
will hold the horiozontal coordinate of the cursor (the column), and y
will hold the vertical coordinate (the row), where (0,0)
is at the top left of the screen. We initialize both of them to 0
, as we want the cursor to start at the top-left of the screen.
Two considerations are noteworthy here. We are not adding Position
to Terminal
, even though you might intuitively think that if we modify the cursor position in Terminal
, it should only be natural that we are keeping track of it there, either. However, cursor_position
will soon describe the position of the cursor in our current document, and not on the screen. and is therefore different from the position of the cursor on the terminal.
This is directly related to the other consideration: Even though we use u16
as our data type for the terminal dimesions, we are using the type usize
for the cursor position. As discussed before, u16
goes up until around 65,000, which is too small for our purposes - it would mean that hecto
could not handle documents longer than 65,000 lines. But how big is usize
? THe answer is: It depends on the architecture we are compiling for, either 32 bit or 64 bit.
Now, let’s add code to refresh_screen()
to move the cursor to the position stored in cursor_position
. While we’re at it, let’s rewrite cursor_position
to accept a Position
.
@@ -3,9 +3,9 @@ use termion::event::Key;
|
|
3
3
|
|
4
4
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
5
5
|
|
6
|
-
struct Position {
|
7
|
-
x: usize,
|
8
|
-
y: usize,
|
6
|
+
pub struct Position {
|
7
|
+
pub x: usize,
|
8
|
+
pub y: usize,
|
9
9
|
}
|
10
10
|
|
11
11
|
pub struct Editor {
|
@@ -38,13 +38,13 @@ impl Editor {
|
|
38
38
|
|
39
39
|
fn refresh_screen(&self) -> Result<(), std::io::Error> {
|
40
40
|
Terminal::cursor_hide();
|
41
|
-
Terminal::cursor_position(0, 0);
|
41
|
+
Terminal::cursor_position(&Position { x: 0, y: 0 });
|
42
42
|
if self.should_quit {
|
43
43
|
Terminal::clear_screen();
|
44
44
|
println!("Goodbye.\r");
|
45
45
|
} else {
|
46
46
|
self.draw_rows();
|
47
|
-
Terminal::cursor_position(
|
47
|
+
Terminal::cursor_position(&self.cursor_position);
|
48
48
|
}
|
49
49
|
Terminal::cursor_show();
|
50
50
|
Terminal::flush()
|
@@ -3,6 +3,7 @@ mod editor;
|
|
3
3
|
mod terminal;
|
4
4
|
use editor::Editor;
|
5
5
|
pub use terminal::Terminal;
|
6
|
+
pub use editor::Position;
|
6
7
|
|
7
8
|
fn main() {
|
8
9
|
Editor::default().run();
|
@@ -1,3 +1,4 @@
|
|
1
|
+
use crate::Position;
|
1
2
|
use std::io::{self, stdout, Write};
|
2
3
|
use termion::event::Key;
|
3
4
|
use termion::input::TermRead;
|
@@ -29,9 +30,14 @@ impl Terminal {
|
|
29
30
|
pub fn clear_screen() {
|
30
31
|
print!("{}", termion::clear::All);
|
31
32
|
}
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
|
34
|
+
#[allow(clippy::cast_possible_truncation)]
|
35
|
+
pub fn cursor_position(position: &Position) {
|
36
|
+
let Position{mut x, mut y} = position;
|
37
|
+
x = x.saturating_add(1);
|
38
|
+
y = y.saturating_add(1);
|
39
|
+
let x = x as u16;
|
40
|
+
let y = y as u16;
|
35
41
|
print!("{}", termion::cursor::Goto(x, y));
|
36
42
|
}
|
37
43
|
pub fn flush() -> Result<(), std::io::Error> {
|
We are using destructuring to initialitze x
and y
in cursor_position
: let Position{mut x, mut y} = position;
creates new variables x and y and binds their values to the fields of the same name in position
.
At this point, you could try initializing cursor_position
with a different value, to confirm that the code works as intended so far.
We are also doing a conversion from the usize
data type within Position
to u16
. u16
can not hold values big enough for u16
to handle, in which case the value is truncated. That is OK for now - we will add logic to make sure we are always within the bounds of u16
later - so we add a small directive here to tell Clippy to not annoy us with this kind of error.
Speaking of annoying Clippy warnings, we are carrying around an old warning from Clippy, which we are going to fix now. Next, we’ll allow the user to move the cursor using the arrow keys.
@@ -53,10 +53,22 @@ impl Editor {
|
|
53
53
|
let pressed_key = Terminal::read_key()?;
|
54
54
|
match pressed_key {
|
55
55
|
Key::Ctrl('q') => self.should_quit = true,
|
56
|
+
Key::Up | Key::Down | Key::Left | Key::Right => self.move_cursor(pressed_key),
|
56
57
|
_ => (),
|
57
58
|
}
|
58
59
|
Ok(())
|
59
60
|
}
|
61
|
+
fn move_cursor(&mut self, key: Key) {
|
62
|
+
let Position { mut y, mut x } = self.cursor_position;
|
63
|
+
match key {
|
64
|
+
Key::Up => y = y.saturating_sub(1),
|
65
|
+
Key::Down => y = y.saturating_add(1),
|
66
|
+
Key::Left => x = x.saturating_sub(1),
|
67
|
+
Key::Right => x = x.saturating_add(1),
|
68
|
+
_ => (),
|
69
|
+
}
|
70
|
+
self.cursor_position = Position { x, y }
|
71
|
+
}
|
60
72
|
fn draw_welcome_message(&self) {
|
61
73
|
let mut welcome_message = format!("Hecto editor -- version {}", VERSION);
|
62
74
|
let width = self.terminal.size().width as usize;
|
Now you should be able to move the cursor around with those keys.
Prevent moving the cursor off screen
Currently, you can cause the cursor_position
values to go past the right and bottom edges of the screen. Let’s prevent that by doing some bounds checking in move_cursor()
.
@@ -60,11 +60,22 @@ impl Editor {
|
|
60
60
|
}
|
61
61
|
fn move_cursor(&mut self, key: Key) {
|
62
62
|
let Position { mut y, mut x } = self.cursor_position;
|
63
|
+
let size = self.terminal.size();
|
64
|
+
let height = size.height.saturating_sub(1) as usize;
|
65
|
+
let width = size.width.saturating_sub(1) as usize;
|
63
66
|
match key {
|
64
67
|
Key::Up => y = y.saturating_sub(1),
|
65
|
-
Key::Down =>
|
68
|
+
Key::Down => {
|
69
|
+
if y < height {
|
70
|
+
y = y.saturating_add(1);
|
71
|
+
}
|
72
|
+
}
|
66
73
|
Key::Left => x = x.saturating_sub(1),
|
67
|
-
Key::Right =>
|
74
|
+
Key::Right => {
|
75
|
+
if x < width {
|
76
|
+
x = x.saturating_add(1);
|
77
|
+
}
|
78
|
+
}
|
68
79
|
_ => (),
|
69
80
|
}
|
70
81
|
self.cursor_position = Position { x, y }
|
You should be able to confirm that you can now move around the visible area, with the cursor staying within the terminal bounds. You can also place it on the last line, which still does not have a tilde, a fact that is not forgotten and will be fixed later during this tutorial.
Navigating with Page Up, Page Down Home and End
To complete our low-level terminal code, we need to detect a few more special keypresses. We are going to map Page Up, Page Down Home and End to position our cursor at the top or bottom of the screen, or the beginning or end of the line, respectively.
@@ -53,7 +53,14 @@ impl Editor {
|
|
53
53
|
let pressed_key = Terminal::read_key()?;
|
54
54
|
match pressed_key {
|
55
55
|
Key::Ctrl('q') => self.should_quit = true,
|
56
|
-
Key::Up
|
56
|
+
Key::Up
|
57
|
+
| Key::Down
|
58
|
+
| Key::Left
|
59
|
+
| Key::Right
|
60
|
+
| Key::PageUp
|
61
|
+
| Key::PageDown
|
62
|
+
| Key::End
|
63
|
+
| Key::Home => self.move_cursor(pressed_key),
|
57
64
|
_ => (),
|
58
65
|
}
|
59
66
|
Ok(())
|
@@ -76,6 +83,10 @@ impl Editor {
|
|
76
83
|
x = x.saturating_add(1);
|
77
84
|
}
|
78
85
|
}
|
86
|
+
Key::PageUp => y = 0,
|
87
|
+
Key::PageDown => y = height,
|
88
|
+
Key::Home => x = 0,
|
89
|
+
Key::End => x = width,
|
79
90
|
_ => (),
|
80
91
|
}
|
81
92
|
self.cursor_position = Position { x, y }
|
Conclusion
I hope this chapter has given you a first feeling of pride when you saw how your text editor was taking shape. We were talking a lot about idiomatic code in the beginning, and where busy refactoring our code into separate files for quite some time, but the payoff is visible: The code is cleanly structured and therefore easy to maintain. Since we now know our way around Rust, we won’t have to worry that much about refactoring in the upcoming chapters and can focus on adding functionality.
In the next chapter, we will get our program to display text files.