Implement the Main Event Loop

Mục tiêu: Transform the program into a persistent application by adding an infinite loop {}. This is the first step in creating the editor’s core event loop, which will keep the application running and ready to process user input.


Of course! As an expert in technology solutions, I will provide you with a detailed guide to complete your current task.

The Heart of the Editor: The Event Loop

Congratulations on successfully setting up the entire foundation for our text editor! In the previous step, you orchestrated a series of crucial tasks: initializing the project, managing dependencies, and, most importantly, taking control of the terminal to create a clean, blank canvas. Your program now starts, prepares the screen, and then gracefully cleans up after itself when it exits.

However, it exits immediately. An editor isn’t very useful if it closes before we can type anything. Our next goal is to make the application stay open and wait for user interaction. To do this, we need to build its core engine: the event loop.

An event loop is a fundamental programming construct that forms the backbone of almost every interactive application, from video games to graphical user interfaces and, of course, our terminal text editor. Its job is simple but critical:

  1. Loop forever.
  2. Inside the loop, wait for an event (like a key press or a mouse click).
  3. Process that event.
  4. Update the application’s state.
  5. Redraw the screen to reflect the new state.
  6. Repeat.

Our current task is to implement the very first part of this pattern: creating the infinite loop that keeps our program alive.

From a Linear Program to a Persistent Application

Up until now, our main function has been a linear script: it executes a sequence of commands from top to bottom and then finishes. We are now changing that model. We will use Rust’s loop keyword to create a loop that, by default, never ends.

Let’s modify our main function. The loop will be the last thing we add, coming after all the initial setup code.

Here are the changes for your src/main.rs file. Notice how we are simply adding the loop {} block and removing the final Ok(()).

// src/main.rs

// ... (use statements and RawModeGuard struct remain the same) ...

fn main() -> std::io::Result<()> {
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;

    execute!(
        stdout(),
        terminal::Clear(terminal::ClearType::All),
        cursor::Hide
    )?;

    // --- NEW CODE FOR THIS TASK ---
    // This is the core event loop of our editor.
    // The `loop` keyword in Rust creates an infinite loop.
    // The program will now stay running inside this block until it's explicitly
    // told to exit (which we will implement in the next tasks).
    loop {
        // We will add event handling logic here soon.
    }

    // Notice that `Ok(())` is removed from the end of the function.
    // This is because the `loop {}` is a "diverging expression" - it never
    // finishes executing. Therefore, any code after it is unreachable,
    // and our function will no longer return `Ok` in the normal course of
    // execution. It will either run forever or return an `Err` if the
    // setup code above fails.
}

Code Breakdown

  • loop {}: This is Rust’s simplest and most performant way to create an infinite loop. The code inside the curly braces will execute repeatedly. For now, the block is empty, so the loop will “spin,” consuming a bit of CPU but effectively keeping our application from terminating.
  • Unreachable Code: A key concept here is that a loop {} expression is considered to be diverging. This is a way of telling the Rust compiler that this part of the code will never finish. Because of this, the compiler knows that any code placed after the loop is unreachable. If you were to leave Ok(()) after the loop, the compiler would issue a warning. By removing it, our function signature -> std::io::Result<()> now means: “this function will either return an I/O Error during setup, or it will run forever.”

What to Expect

Go to your terminal and run the application:

cargo run

This time, the program will not exit. Your screen will clear, the cursor will disappear, and then… nothing. The application is now running in its infinite loop, waiting for instructions that we haven’t yet told it how to receive.

You are now running a persistent terminal application!

Since we haven’t implemented an exit condition yet, you will need to manually stop the program. The most common way is to press Ctrl+C. When you do, you should see your normal terminal prompt return, perfectly restored thanks to the RawModeGuard that we built earlier.

Next Steps

Our loop is running, but it’s deaf. It has no way of hearing what the user is typing. In our very next task, we will give it ears by calling crossterm::event::read() inside the loop to wait for and capture user input events.


Further Reading

Implement Blocking Input for Terminal Events

Mục tiêu: Refactor the main application loop from a high-CPU spinning loop to an efficient blocking loop. Use the crossterm::event::read() function to wait for and capture user input events, significantly reducing CPU consumption.


Excellent work on creating the infinite loop! You’ve successfully transformed our program from a simple script into a persistent application. However, as you’ve noticed, our loop is currently “spinning”—it’s running as fast as the CPU will allow, consuming resources without actually doing any work. Our editor is alive, but it’s not yet listening.

In this task, we will give our application its senses. We will change the loop from a frantic, spinning one into a patient, efficient one that waits for user input before doing anything.

From Spinning to Blocking: A More Efficient Approach

The key to this efficiency is a concept called blocking I/O. Instead of constantly asking “Is there input yet? Is there input yet?”, a blocking function call tells the operating system, “Wake me up when there’s some input.” The OS then suspends our program, freeing up the CPU for other tasks. When an event (like a key press) occurs, the OS wakes our program back up and delivers the event data.

This is exactly what the crossterm::event::read() function does. It’s a blocking function that pauses the execution of our program until a terminal event is available. This is the heart of any efficient event-driven application.

Capturing the Event

Let’s modify our main function to use this powerful function. We need to do two things:

  1. Update our use statement to include the event module from crossterm.
  2. Call event::read() inside our loop and store the result.

Here are the changes for your src/main.rs file.

// src/main.rs

// Add `event` to our crossterm import list. This module contains
// the functions we need to read keyboard, mouse, and other terminal events.
use crossterm::{cursor, event, execute, terminal};
use std::io::stdout;

// The RawModeGuard struct and its Drop implementation remain unchanged.
struct RawModeGuard;

impl Drop for RawModeGuard {
    fn drop(&mut self) {
        execute!(stdout(), cursor::Show).unwrap();
        terminal::disable_raw_mode().unwrap();
    }
}

fn main() -> std::io::Result<()> {
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;

    execute!(
        stdout(),
        terminal::Clear(terminal::ClearType::All),
        cursor::Hide
    )?;

    loop {
        // --- NEW CODE FOR THIS TASK ---

        // `event::read()` is the blocking call that waits for a user input event.
        // It returns a `Result` containing an `Event` enum.
        // We use the `?` operator to handle any potential I/O errors, just as we did during setup.
        // If `read()` is successful, the `Event` is stored in the `event` variable.
        let event = event::read()?;

        // For now, we are not doing anything with the captured `event`.
        // The program captures one event, the loop finishes one iteration,
        // and then it immediately blocks again on `event::read()`, waiting for the next one.
        // We will inspect the contents of `event` in the next task.
    }
}

Code Breakdown

  • use crossterm::{..., event, ...};: We are now explicitly importing the event module, making its contents (like the read function) available in our scope.
  • let event = event::read()?;: This is the most important line in our application so far.
    • event::read() pauses the program.
    • When you press a key, move the mouse, or resize the window, read() “wakes up” and returns a Result.
    • The ? operator propagates any I/O error, causing our main function to return early and letting our RawModeGuard clean up.
    • On success, the Result is unwrapped, and the Event it contains is assigned to our new variable, event.

How to Verify the Change

When you run cargo run, the application will look identical to the previous step—a blank, waiting screen. But under the hood, a fundamental change has occurred.

You can verify this by opening your system’s activity monitor (like top or htop on Linux/macOS, or Task Manager on Windows) and finding your kilo_rs process. In the previous step, you would have seen it consuming a significant amount of CPU. Now, you should see it consuming 0% CPU while it’s waiting. It only uses the CPU for the brief moment it takes to process an event when you press a key.

This efficiency is crucial for creating a well-behaved application that doesn’t drain battery life or slow down the user’s system.

Next Steps

We have successfully taught our application how to listen! It now patiently waits and captures events. But capturing an event is only half the battle. Our next task is to inspect the event variable to figure out what kind of event it is. We will use a match statement to check if it’s a key press, a mouse event, or something else, so we can begin to act on the user’s input.


Further Reading

Handling Terminal Events with Rust’s match Statement

Mục tiêu: Learn to use Rust’s powerful match statement to inspect the crossterm::event::Event enum. This task involves differentiating between key presses, mouse events, and window resizing to handle each type of user input appropriately.


In our previous task, we successfully taught our application to listen. By calling crossterm::event::read(), we transformed our busy, spinning loop into an efficient, patient one that blocks until the user interacts with the terminal. We are now capturing events, but the event variable we created is still a black box. Our application knows that something happened, but it has no idea what happened. Was it a key press? A mouse click? Did the user resize the window?

Our current task is to open that black box and inspect its contents. We need a way to check the type of the event so we can begin to respond to it appropriately. For this, we will use one of Rust’s most powerful and expressive control flow constructs: the match statement.

Understanding the crossterm::event::Event Enum

The event::read() function returns an Event, which is an enum. In Rust, an enum (short for enumeration) is a type that can represent one of several possible variants. You can think of it like a light switch that can be either On or Off, but never both. The crossterm::event::Event enum is defined roughly like this:

pub enum Event {
    Key(KeyEvent),
    Mouse(MouseEvent),
    Resize(u16, u16),
    // ... and a few others
}

This means that an Event variable can be one of three main things: * A Key event, which contains a KeyEvent struct with details about which key was pressed. * A Mouse event, containing a MouseEvent struct with button and position info. * A Resize event, containing the new width and height of the terminal.

To handle this, we need a construct that can check which variant our event variable currently holds and execute different code for each case. This is precisely what match is for.

The Power of Pattern Matching with match

A match statement in Rust is like a super-powered switch statement from other languages. It takes a value (our event variable) and compares it against a series of “patterns.” When it finds a pattern that fits, it executes the code associated with that pattern.

What makes match so great for enums is that the Rust compiler enforces exhaustiveness. This means you must handle every single possible variant of the enum. If you forget one, your code won’t compile! This is a fantastic safety feature that eliminates a whole class of bugs where a program doesn’t know how to handle a specific state.

Implementing the Event Matcher

Let’s add a match statement inside our loop to inspect the event we receive from event::read(). For now, we’ll just print a message to the console to confirm that we’re correctly identifying each type of event.

Here are the changes for your src/main.rs file.

// src/main.rs

// ... (use statements, RawModeGuard, and main function signature are the same) ...

fn main() -> std::io::Result<()> {
    // ... (setup code is the same) ...

    loop {
        let event = event::read()?;

        // --- NEW CODE FOR THIS TASK ---
        // The `match` statement allows us to handle different types of events.
        // `event` is the variable we are matching against.
        match event {
            // This is the first "arm" of the match.
            // It matches if the `event` is the `Event::Key` variant.
            // The `(..)` part means we don't care about the `KeyEvent` data inside it *for now*.
            event::Event::Key(..) => {
                // The `\r\n` moves the cursor to the start of a new line.
                // This is a temporary way to see output in raw mode without messing up the screen too much.
                println!("Key Event!\r");
            }
            // This arm matches if the event is a mouse event.
            event::Event::Mouse(..) => {
                println!("Mouse Event!\r");
            }
            // This arm matches if the terminal window was resized.
            event::Event::Resize(width, height) => {
                println!("Resized to {}x{}\r", width, height);
            }
            // The `_` is a wildcard pattern. It matches any event that wasn't
            // caught by the arms above. This is necessary to make the match exhaustive.
            _ => {
                println!("Other Event\r");
            }
        }
    }
}

Code Breakdown

  • match event { ... }: This begins the match block, telling Rust we want to pattern match on the event variable.
  • event::Event::Key(..): This is the first pattern.
    • It checks if the event is of the variant Event::Key. We use the full path event::Event to be explicit.
    • The (..) is a pattern that means “I know there’s data inside this variant, but I want to ignore it for now.” It’s a placeholder for the KeyEvent struct.
  • => { ... }: The => (called a “fat arrow”) separates the pattern from the code that should run if the pattern matches.
  • println!(...);: For now, we’re just printing a confirmation message. Note the \r at the end of the string. This is a “carriage return,” which moves the cursor back to the beginning of the line. In raw mode, this helps prevent the printed text from scrolling the screen in a weird way. This is a temporary debugging tool.
  • event::Event::Resize(width, height): This pattern is more advanced. Not only does it match the Resize variant, but it also destructures it, binding the values inside (the new width and height) to variables named width and height that we can use in our code block.
  • _ => { ... }: This is the wildcard arm. The underscore _ is a special pattern that matches anything. If the event is not a Key, Mouse, or Resize event, it will be caught by this arm. This satisfies the compiler’s exhaustiveness check, ensuring our program knows what to do in all possible situations.

Verifying Your Work

Run the application with cargo run. The screen will clear as before. Now, try interacting with the terminal:

  1. Press any key: You should see Key Event! appear at the top-left of the screen.
  2. Move or click your mouse: You should see Mouse Event!. (Note: Not all terminals are configured to report mouse events by default).
  3. Resize the terminal window: You should see a message like Resized to 120x30.

The output might look a little jumbled, but the important thing is that your application is now correctly identifying and reacting to different kinds of user input. You will still need to use Ctrl+C to exit.

Next Steps

We’ve successfully identified that a key has been pressed. Now we need to know which key. In the next task, we will modify our Event::Key match arm to destructure the KeyEvent inside, allowing us to build the logic for our editor’s exit condition (Ctrl+Q).


Further Reading

Rust: Destructure KeyEvent to Identify Specific Key Presses

Mục tiêu: Refine a terminal application’s event handling by destructuring the KeyEvent struct from a crossterm Event::Key variant. This involves updating a match statement to capture and print detailed information about key presses, including key codes and modifiers, for debugging purposes.


Excellent! You’ve successfully built a match statement that can differentiate between various types of terminal events. Your application can now tell the difference between a key press, a mouse action, and a window resize. This is a huge step forward in building an interactive application.

Right now, our Key event arm is a bit generic. It knows a key was pressed, but it has no idea which one. Was it the letter ‘a’, the ‘Enter’ key, or an arrow key? To build a text editor, we need this level of detail. In this task, we will dive deeper into the Event::Key variant and extract the rich information it contains.

Unpacking the KeyEvent Struct

When crossterm::event::read() returns a key press event, it doesn’t just return a simple Event::Key signal. It returns an Event::Key variant that contains a KeyEvent struct. This struct is a treasure trove of information, typically holding:

  • code: An enum KeyCode that tells us the specific key that was pressed (e.g., Char('q'), Backspace, Left, F5).
  • modifiers: A set of flags that tell us if modifier keys like Ctrl, Shift, or Alt were being held down during the key press.
  • kind: The type of key event, such as Press, Release, or Repeat. For our editor, we’ll mostly be interested in Press.

Our goal is to change our match pattern to not just match the Event::Key variant, but to also capture the KeyEvent struct inside it. This is a common and powerful technique in Rust called destructuring.

Destructuring in a match Pattern

Destructuring is the act of taking a complex data type like a struct or enum and breaking it down into its constituent parts. In a match arm, we can replace the placeholder (..) with a variable name. Rust will then match the Event::Key variant and bind the inner KeyEvent value to that variable, making it available for us to use inside the match arm’s code block.

Let’s update our code. We’ll need to:

  1. Bring the KeyEvent struct into scope with a use statement.
  2. Change the pattern event::Event::Key(..) to Event::Key(key_event).
  3. Inside the block, we will print the key_event using Rust’s debug formatter ({:?}) to see exactly what information we’re getting.

Here are the specific changes for your src/main.rs file.

// src/main.rs

// We need to bring KeyEvent into scope to use it in our match arm.
// We can use a nested import to keep the use statement clean.
use crossterm::{
    cursor,
    event::{self, Event, KeyEvent}, // <-- MODIFIED HERE
    execute, terminal,
};
use std::io::stdout;

// ... RawModeGuard struct and impl Drop are unchanged ...
struct RawModeGuard;

impl Drop for RawModeGuard {
    fn drop(&mut self) {
        execute!(stdout(), cursor::Show).unwrap();
        terminal::disable_raw_mode().unwrap();
    }
}

fn main() -> std::io::Result<()> {
    // ... setup code is unchanged ...
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;
    execute!(
        stdout(),
        terminal::Clear(terminal::ClearType::All),
        cursor::Hide
    )?;

    loop {
        let event = event::read()?;

        match event {
            // UPDATED: We now destructure the KeyEvent from the Event::Key variant.
            // Instead of `(..)` which ignores the content, we use `(key_event)`.
            // This binds the inner KeyEvent struct to a new variable called `key_event`.
            Event::Key(key_event) => {
                // We use the debug formatter `{:?}` to print the entire KeyEvent struct.
                // This is an incredibly useful debugging technique to inspect the contents of a variable.
                // The `\r` carriage return ensures we print on the same line, overwriting previous output.
                println!("{:?}\r", key_event);
            }
            Event::Mouse(..) => {
                println!("Mouse Event!\r");
            }
            Event::Resize(width, height) => {
                println!("Resized to {}x{}\r", width, height);
            }
            _ => {
                println!("Other Event\r");
            }
        }
    }
}

Code Breakdown

  • use crossterm::event::{self, Event, KeyEvent};: This is a nested use statement. It’s equivalent to writing use crossterm::event;, use crossterm::event::Event;, and use crossterm::event::KeyEvent;. It’s a concise way to bring multiple items from the same module into scope. We’ve added KeyEvent so Rust knows what it is when we use it in the match arm.
  • Event::Key(key_event): This is our new, more powerful pattern. When the event variable is an Event::Key, this pattern matches, and Rust automatically takes the KeyEvent struct stored inside the variant and assigns it to a new variable we’ve named key_event.
  • println!("{:?}\r", key_event);: This is our debugging tool. The {:?} formatter tells println! to format the key_event variable for debugging purposes. Most structs and enums in Rust can be printed this way if they derive the Debug trait, which KeyEvent does. This will give us a detailed, human-readable view of the struct’s contents.

What to Expect Now

Run your application with cargo run. The screen will clear as before. Now, start pressing some keys and observe the output at the top-left of your terminal:

  • Press the q key. You will see something like: KeyEvent { code: Char('q'), modifiers: NONE, kind: Press, state: NONE }
  • Press Shift+A. You might see: KeyEvent { code: Char('A'), modifiers: SHIFT, kind: Press, state: NONE }
  • Press the up arrow key. You’ll see: KeyEvent { code: Up, modifiers: NONE, kind: Press, state: NONE }
  • Finally, try Ctrl+q. You should see: KeyEvent { code: Char('q'), modifiers: CONTROL, kind: Press, state: NONE }

You can now see the exact code and modifiers for every key press! This is the raw data we need to build our editor’s functionality.

Next Steps

You have successfully extracted the detailed key event information. We are now perfectly positioned to act on it. In the very next task, we will use this information to implement the first real feature of our editor: a way to exit cleanly by pressing Ctrl+Q.


Further Reading

Implement an Exit Command with Ctrl+Q in Rust

Mục tiêu: Learn to handle specific keyboard input in a Rust terminal application by implementing a graceful exit command. This task involves detecting the Ctrl+Q key combination using the crossterm library and using the break keyword to terminate the main event loop.


Of course! Let’s get this task done. Here is the detailed guide:

Giving Our Editor an Exit Strategy

In the last task, you successfully drilled down into the Event::Key variant to extract a detailed KeyEvent struct. By printing it to the screen, you’ve seen firsthand the rich data we get with every key press: the character code, the modifier keys (Ctrl, Shift, Alt), and more. This is the raw material we need to build our editor’s features.

An application that you can’t close is not very friendly. So far, we’ve had to rely on Ctrl+C to terminate our program. The first real feature we’ll implement is a proper exit command. The standard convention in many terminal applications, inspired by the Vim editor, is to use Ctrl+Q to quit.

Our goal is to detect when the user presses this specific combination and, when they do, to gracefully break out of our infinite loop, allowing the program to terminate.

Analyzing the Ctrl+Q KeyEvent

From our debugging in the previous task, we know that when a user presses Ctrl+Q, the KeyEvent struct we receive looks something like this:

KeyEvent { code: Char('q'), modifiers: CONTROL, ... }

This gives us a clear recipe for what we need to check:

  1. The code field of the KeyEvent must be equal to KeyCode::Char('q').
  2. The modifiers field must be equal to KeyModifiers::CONTROL.

We need to check for both of these conditions simultaneously inside our Event::Key match arm.

Implementing the Exit Condition with break

We will use a simple if statement to check these conditions. If the conditions are met, we will use the break keyword. In Rust, break immediately terminates the execution of the current loop (in our case, the main loop {}).

Once the loop is broken, program execution continues from the line of code immediately following the loop. Since our loop is the last major statement in our main function, breaking it will cause main to finish. However, our main function has a return type of std::io::Result<()>, so we must return Ok(()) at the end to signify a successful exit. This line was previously unreachable, but now it will be the final step of a normal program shutdown.

Let’s modify src/main.rs. We’ll need to bring KeyCode and KeyModifiers into our use statement, and then add the logic to our loop.

Here are the complete changes. Pay close attention to the highlighted lines.

// src/main.rs

use crossterm::{
    cursor,
    // Add KeyCode and KeyModifiers to the import list
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    execute, terminal,
};
use std::io::stdout;

// RawModeGuard and its impl Drop are unchanged.
struct RawModeGuard;
impl Drop for RawModeGuard {
    fn drop(&mut self) {
        execute!(stdout(), cursor::Show).unwrap();
        terminal::disable_raw_mode().unwrap();
    }
}

fn main() -> std::io::Result<()> {
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;
    execute!(
        stdout(),
        terminal::Clear(terminal::ClearType::All),
        cursor::Hide
    )?;

    loop {
        let event = event::read()?;

        match event {
            // UPDATED: The entire logic inside this arm is new.
            Event::Key(key_event) => {
                // Check for the specific combination of Ctrl + q.
                // The `key_event.code` tells us which key was pressed.
                // The `key_event.modifiers` tells us if Ctrl, Shift, or Alt were held.
                if key_event.code == KeyCode::Char('q')
                    && key_event.modifiers == KeyModifiers::CONTROL
                {
                    // If the condition is met, we break out of the infinite loop.
                    break;
                }
            }
            Event::Mouse(..) => {
                println!("Mouse Event!\r");
            }
            Event::Resize(width, height) => {
                println!("Resized to {}x{}\r", width, height);
            }
            _ => {
                println!("Other Event\r");
            }
        }
    }

    // NEW: This line is now reachable.
    // When `break` is called, the loop terminates and execution continues here.
    // We return `Ok(())` to indicate that the program finished successfully.
    Ok(())
}

Code Breakdown

  • use crossterm::event::{..., KeyCode, ..., KeyModifiers}: We explicitly import the KeyCode enum and the KeyModifiers struct so we can refer to them directly in our if condition without writing their full paths (e.g., event::KeyCode::Char('q')).
  • if key_event.code == KeyCode::Char('q') && key_event.modifiers == KeyModifiers::CONTROL: This is our core logic. We access the code and modifiers fields of the key_event variable we captured. The && is a logical AND, ensuring both conditions must be true for the if block to execute.
  • break;: This simple keyword is the key to our exit strategy. It tells Rust to stop executing the loop and move on.
  • Ok(()): This is the successful return value for our main function. Now that there’s a way to exit the loop, this code is no longer “unreachable,” and it provides the proper success signal when the application closes via Ctrl+Q.

What to Expect

Run your application with cargo run. The screen will clear, and the application will wait for input as before.

  • Press any key other than Ctrl+Q (e.g., ‘a’, ‘b’, ‘c’). Nothing will happen. This is because we removed the println! from the Event::Key arm.
  • Now, press Ctrl+Q.

The application will exit instantly and cleanly. Your terminal prompt will be restored, and you won’t see any error messages. You have successfully implemented the editor’s first command!

Next Steps

This is a fantastic milestone. We now have a running application with a controlled lifecycle. The next and final task in this step is to provide some feedback to the user for all the other keys they press, verifying that our input capture is working comprehensively. We will bring back the println! for any key that isn’t our exit command.


Further Reading

Provide Feedback for All Key Presses in the Event Loop

Mục tiêu: Modify the Rust event loop to handle all key presses that are not the Ctrl+Q exit command. Add an else block to print the KeyEvent struct, providing immediate user feedback that their input is being captured in raw terminal mode.


Fantastic work on implementing the editor’s exit condition! You’ve created the most fundamental feature of any application: a way to close it gracefully. With the Ctrl+Q combination now reliably breaking the event loop, we have a complete, controlled application lifecycle.

Before we move on to building the complex data structures that will hold our text, let’s complete our event loop by adding one final piece of functionality: user feedback. Right now, pressing any key other than Ctrl+Q does nothing. While this is technically correct, it leaves the user wondering if the application has frozen or if their input is being ignored. Our final task in this step is to provide immediate confirmation that we are, in fact, capturing every other key press.

Handling the “Everything Else” Case

In programming, it’s common to handle one or more specific cases and then have a general “catch-all” case for everything else. This is a perfect scenario for an if-else expression. You’ve already written the if part to handle the specific case of Ctrl+Q. Now, we will add an else block to handle every other key press.

Inside this else block, we’ll bring back a familiar friend: the debug println!. This will print the full KeyEvent struct to the screen, just as we did a few tasks ago for debugging. This serves as a simple, temporary way to “echo” the user’s input, confirming that the editor is alive and listening.

Let’s modify the Event::Key arm in the match statement in your src/main.rs file.

// src/main.rs

// ... (use statements, RawModeGuard, and main function signature are the same) ...

fn main() -> std::io::Result<()> {
    // ... (setup code is the same) ...

    loop {
        let event = event::read()?;

        match event {
            Event::Key(key_event) => {
                if key_event.code == KeyCode::Char('q')
                    && key_event.modifiers == KeyModifiers::CONTROL
                {
                    break;
                } else {
                    // --- NEW CODE FOR THIS TASK ---
                    // This `else` block catches all key events that are NOT Ctrl+Q.
                    // We print the `KeyEvent` using the debug formatter `{:?}`.
                    // The `\r\n` ensures that each new printout appears on a new line
                    // and the cursor starts at the beginning of that line, which is
                    // much cleaner for viewing a sequence of inputs in raw mode.
                    println!("{:?}\r\n", key_event);
                }
            }
            Event::Mouse(..) => {
                // We'll leave these as-is for now.
                // Note: It's good practice to clear these temporary println!s
                // before moving on to real UI rendering.
                println!("Mouse Event!\r\n");
            }
            Event::Resize(width, height) => {
                println!("Resized to {}x{}\r\n", width, height);
            }
            _ => {
                println!("Other Event\r\n");
            }
        }
    }

    Ok(())
}

Code Breakdown

  • else { ... }: This else block is attached to our previous if statement. Its code will only execute if the condition in the if statement (key_event.code == KeyCode::Char('q') && ...) is false. This creates a clear separation of logic: one path for exiting, and another path for everything else.
  • println!("{:?}\r\n", key_event);: We are reusing the debug print from a previous task. The key difference now is the use of \r\n.
    • \r is a Carriage Return. It moves the cursor to the beginning of the current line.
    • \n is a Line Feed (or newline). It moves the cursor down to the next line.
    • Using them together (\r\n) is the most reliable way across different terminals to start printing on a fresh line at the very beginning. This will create a clean log of key presses, one after another, instead of overwriting the same line.

What to Expect

Run the application with cargo run. The screen will clear, and the editor will wait.

  1. Press a few different keys: a, b, c, Enter, ArrowUp.
  2. For each key press, you will see its corresponding KeyEvent struct printed on a new line at the top of the screen.
  3. Press Ctrl+Q. The program will exit cleanly, just as before.

Congratulations! You have now completed the entire second step of the project. You have a robust event loop that can listen for input, specifically handle an exit condition, and provide feedback for all other inputs. This is the engine that will drive the rest of your editor.

Next Steps

With the event loop running, we need a place to store the editor’s “state”—things like the text the user has typed, the position of the cursor, and the size of the screen. In the next major step, we will design and implement the core data structures for the editor’s state, starting with the creation of an Editor struct to hold everything together.


Further Reading