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 were 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
functionality 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 key presses 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 were 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.
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 key presses 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 occurred, 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.
Similarly, 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
separately 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 repeatedly 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
shortens 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 horizontal 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 dimensions, 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 key presses. 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 were 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.