Create a New Rust Project with Cargo

Mục tiêu: Learn to initialize a new Rust project for a terminal-based text editor using cargo new. This step covers understanding the project structure, the role of Cargo.toml, and running the initial ‘Hello, world!’ application.


Welcome to the first step of building your terminal-based text editor in Rust! Every great software project begins with a solid foundation, and in the Rust ecosystem, that foundation is laid by Cargo, Rust’s built-in build system and package manager. Before we can write a single line of code for our editor, we need to create a structured project environment, and Cargo is the perfect tool for the job.

What is Cargo?

Cargo handles a lot of the tedious work involved in managing a project, including:

  • Building your code: It compiles your Rust files into an executable binary or a library.
  • Managing dependencies: It downloads and compiles the external libraries (called “crates” in Rust) that your project needs.
  • Running tests: It provides a framework for writing and running unit and integration tests.

Using cargo is the standard, idiomatic way to create and manage Rust projects. It ensures your project follows a conventional structure, making it easier for you and other developers to navigate.

Creating Your Project

Let’s get started. Open your terminal or command prompt and navigate to the directory where you want to store your projects. Once there, run the following command:

cargo new kilo_rs

This single command performs several important actions:

  1. Creates a Directory: It creates a new directory named kilo_rs.
  2. Initializes a Git Repository: By default, cargo new initializes a new Git repository in the project directory, complete with a .gitignore file. This is a fantastic best practice that encourages version control from the very beginning.
  3. Generates Project Structure: It creates a standard, conventional file structure inside kilo_rs.

After running the command, you will see a confirmation message:

Created binary (application) `kilo_rs` package

Understanding the Project Structure

Now, let’s look at the structure Cargo has created for us. If you navigate into the new directory (cd kilo_rs) and list its contents, you’ll see the following:

kilo_rs/
├── .git/
├── .gitignore
├── Cargo.toml
└── src/
    └── main.rs

Let’s break down the two most important parts:

  • Cargo.toml: This is the manifest file for your project. It’s written in the TOML (Tom’s Obvious, Minimal Language) format. This file contains all the metadata Cargo needs to compile your project, such as the project name, version, author, and—most importantly for our upcoming tasks—the list of dependencies. We will be editing this file in the next task to add crossterm.
  • src/main.rs: This is the root source file for your application. By convention, a new binary project created with cargo new comes with a main.rs file that contains a simple “Hello, world!” program. This file is the entry point for your text editor.

You can open src/main.rs and see the default code:

// src/main.rs
fn main() {
    println!("Hello, world!");
}

Compiling and Running Your New Project

To ensure everything is set up correctly, you can compile and run this boilerplate application. While inside the kilo_rs directory, run:

cargo run

Cargo will first compile your project and then execute the resulting binary. You should see the following output in your terminal:

   Compiling kilo_rs v0.1.0 (/path/to/your/project/kilo_rs)
    Finished dev [unoptimized + debuginfo] target(s) in ...s
     Running `target/debug/kilo_rs`
Hello, world!

Congratulations! You have successfully created, compiled, and run your first Rust project. This clean, structured starting point is the perfect launchpad for building our text editor.

In our next task, we’ll edit the Cargo.toml file to add our first dependency, crossterm, which is the key to controlling the terminal.


Further Reading

To learn more about Cargo and its capabilities, check out these official resources:

Add crossterm Dependency to Cargo.toml

Mục tiêu: Modify the Cargo.toml file to add the crossterm crate as a project dependency. This is the first step in gaining control over the terminal for building a text editor.


In our last step, we used cargo new to create the skeleton of our project, kilo_rs. This command generated a crucial file: Cargo.toml. This file is the manifest for our entire project, and for now, its most important job is to tell Cargo which external libraries, or “crates,” our text editor needs to function.

To build a text editor that runs inside a terminal, we need a way to control that terminal directly. By default, terminals operate in “cooked mode” (also known as canonical mode). In this mode, the operating system processes your keystrokes, buffering them line by line, and handling special characters like backspace before your program ever sees them. For an application like println!("Hello, world!"), this is perfect. For a text editor, it’s a non-starter. We need to see every single key press ('w', 'a', arrow up, ctrl-q) the moment it happens.

This is where raw mode comes in. In raw mode, we get unprocessed, raw input directly from the terminal. This gives us the fine-grained control we need to move the cursor, write text anywhere on the screen, and react to key presses instantly.

Introducing crossterm

Manually enabling raw mode and controlling the terminal is complex and varies significantly between operating systems (like Windows, macOS, and Linux). Thankfully, the Rust ecosystem provides a powerful crate to handle this for us: crossterm.

crossterm is a pure-Rust, cross-platform library for terminal manipulation. It provides a simple API to: * Enable and disable raw mode. * Move the cursor. * Clear parts of the screen. * Change text colors and styles. * Capture keyboard, mouse, and terminal resize events.

It is the foundational tool that will allow us to build our editor’s interface.

Adding the Dependency

To tell Cargo that our project depends on crossterm, we need to add it to our Cargo.toml file. Open the Cargo.toml file in the root of your kilo_rs project. It should currently look something like this:

[package]
name = "kilo_rs"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Your task is to add crossterm under the [dependencies] section. You specify the crate name and the version you want to use.

[dependencies]
crossterm = "0.27"

By adding crossterm = "0.27", we are telling Cargo:

  1. Our project needs the crossterm crate.
  2. We want a version that is compatible with 0.27. According to Semantic Versioning (SemVer) rules that Cargo follows, this means Cargo can use version 0.27.0 or any later patch/minor version like 0.27.1 or 0.28.0, but not a major breaking version like 0.28.0. This ensures our code won’t break due to future updates in the crate while still allowing for bug fixes. (Note: Cargo’s default version requirements are a bit more nuanced, but this is the general idea).

Verifying the Change

How do you know it worked? You can ask Cargo to build the project. Run the following command in your terminal from the kilo_rs directory:

cargo build

The first time you do this after adding a new dependency, you will see Cargo downloading and compiling crossterm and all of its own dependencies. The output will look something like this:

    Updating crates.io index
   Compiling proc-macro2 v1.0.66
   Compiling unicode-ident v1.0.11
   Compiling quote v1.0.33
   ... (many more dependencies) ...
   Compiling crossterm v0.27.0
   Compiling kilo_rs v0.1.0 (/path/to/your/project/kilo_rs)
    Finished dev [unoptimized + debuginfo] target(s) in ...s

Seeing Compiling crossterm ... is your confirmation that Cargo.toml is configured correctly and Cargo has successfully integrated the new crate into your project’s build process.

With our primary tool now available to us, our next task is to write our first few lines of Rust code in main.rs to use crossterm and switch the terminal into the powerful raw mode that our editor requires.


Further Reading

Enable Terminal Raw Mode with Crossterm

Mục tiêu: Learn to switch the terminal from its default ‘cooked’ mode to ‘raw’ mode using the crossterm crate in Rust. This task covers the terminal::enable\_raw\_mode() function and the fundamentals of terminal control for building interactive applications.


Excellent! Having added the crossterm crate to your project’s dependencies in the last task, we are now ready to put it to use. Our goal is to take control of the terminal, and the very first step in doing that is to switch it from its default “cooked” mode into “raw” mode.

Understanding Terminal Modes: Cooked vs. Raw

Before we write any code, it’s crucial to understand what we’re about to do and why. By default, your terminal operates in what’s known as canonical mode, or “cooked mode.” In this mode, the terminal’s line discipline (a part of the operating system’s TTY driver) processes input for you. It buffers your keystrokes, handles special characters like Backspace to edit the line, and only sends the input to your application when you press Enter. This is perfect for simple command-line tools but completely unsuitable for a text editor where we need to react to every single key press instantly—be it an arrow key, a letter, or a control combination.

Raw mode is the opposite. It disables all this input processing. When raw mode is enabled: * Every key you press is sent directly to our application as it happens. * The keys you type are not automatically echoed back to the screen. Our application will be responsible for drawing every character. * Control combinations like Ctrl+C (interrupt) and Ctrl+Z (suspend) are no longer handled by the terminal; they are passed to our application as raw byte sequences.

Enabling raw mode gives us a blank canvas and complete control, which is exactly what a text editor needs.

Enabling Raw Mode with crossterm

Let’s modify our src/main.rs file to enable raw mode. Open the file and replace the “Hello, world!” boilerplate with the following code.

First, we need to bring the necessary crossterm module into scope with a use statement. The functionality for controlling the terminal’s mode resides in the terminal module.

Then, inside our main function, we’ll call the enable_raw_mode() function.

// src/main.rs

// Import the 'terminal' module from the 'crossterm' crate.
// This module contains functions for controlling the terminal, such as enabling raw mode.
use crossterm::terminal;

fn main() {
    // Call the enable_raw_mode function.
    // This function switches the terminal from its default "cooked" mode to "raw" mode.
    // It returns a Result type, which will be an Err if it fails to enable raw mode.
    // We use .unwrap() here for simplicity to panic the program if an error occurs.
    // In a real-world application, you would handle this error more gracefully.
    terminal::enable_raw_mode().unwrap();
}

Code Breakdown

Let’s break down exactly what this code does:

  1. use crossterm::terminal;

    • In Rust, crates are organized into modules. The crossterm crate has several modules like terminal, event, cursor, etc.
    • The use keyword brings a specific path into the local scope, allowing us to use it without writing the full path every time. Without this line, we would have to write crossterm::terminal::enable_raw_mode().unwrap();, which is more verbose.
  2. terminal::enable_raw_mode()

    • This is the core of our task. It’s a function call that tells crossterm to perform the necessary, operating-system-specific system calls to put the terminal into raw mode.
  3. .unwrap()

    • The enable_raw_mode() function doesn’t just run; it can potentially fail (e.g., if the program is not running in an interactive terminal). To handle this possibility, it returns a Result type.
    • A Result<T, E> in Rust is an enum that can be one of two variants: Ok(T) representing success (containing a value of type T), or Err(E) representing failure (containing an error of type E).
    • The .unwrap() method is a shortcut. If the Result is Ok, it extracts the value inside. If the Result is Err, it panics, which means the program will crash and display an error message.
    • For now, unwrap() is simple and acceptable. If our editor can’t even enter raw mode, there’s not much it can do, so crashing is a reasonable, albeit blunt, response. In later stages and more robust applications, you would typically use match or the ? operator to handle errors more gracefully.

Running the Code: An Important Observation

Now, navigate to your project directory in the terminal and run the application:

cargo run

You will notice something strange… your terminal prompt doesn’t return! The program is running, but it doesn’t do anything and doesn’t exit. Your terminal is now in raw mode. * Try typing something. Nothing appears. This is because input echoing is off. * Try pressing Ctrl+C. It likely won’t work to exit the program.

You have successfully put the terminal in raw mode, but you haven’t told it when to stop. Because our main function exits immediately after enabling raw mode, the program terminates, but it leaves the terminal in a raw state. You will likely need to close the terminal tab/window or type reset and press Enter (even if you can’t see it) to fix your terminal’s state.

This leads us directly to our next, crucial task: we must ensure that raw mode is always disabled when our program exits, whether it closes normally or crashes unexpectedly. We will tackle this in the next task by creating a clever structure that cleans up after itself automatically.


Further Reading

Graceful Terminal Cleanup in Rust with RAII

Mục tiêu: Implement a robust cleanup mechanism for a Rust terminal application. Use the RAII pattern and the Drop trait to create a RawModeGuard that automatically disables terminal raw mode upon exit, preventing a broken terminal state even if the program panics.


In our previous task, we successfully switched the terminal into raw mode. However, we immediately ran into a significant problem: upon exiting, our program left the terminal in a broken state, unable to process input or echo characters correctly. This happens because we enabled raw mode but never disabled it.

A simple solution might be to call crossterm::terminal::disable_raw_mode() at the end of our main function. But what happens if our program panics or crashes somewhere in the middle? The cleanup code at the end of main would never be reached, and we’d be left with a broken terminal again. We need a bulletproof way to guarantee that our cleanup code runs, no matter how the program exits.

This is where Rust’s powerful ownership system and a design pattern called RAII (Resource Acquisition Is Initialization) come to the rescue.

The RAII Pattern and Rust’s Drop Trait

RAII is a core concept in Rust. The idea is that a resource (like raw terminal mode, a file handle, or a network connection) is tied to the lifetime of an object. The resource is acquired when the object is created (initialized), and it is automatically released when the object is destroyed.

In Rust, this “destruction” is handled by the Drop trait. You can implement the Drop trait for any of your custom structs. This trait requires you to define a single method, drop(), which contains the cleanup code. Rust’s compiler guarantees that this drop() method will be called whenever an instance of your struct goes out of scope—whether that’s at the end of a function, inside a loop that finishes, or even during a panic!

This pattern provides a robust and predictable way to manage resources, preventing leaks and ensuring your application always cleans up after itself.

Implementing the RawModeGuard

Let’s create a special struct whose only job is to ensure raw mode gets disabled. We’ll call it RawModeGuard.

First, we define the struct itself. It doesn’t need to hold any data; its mere existence is its purpose. Then, we implement the Drop trait for it. Inside the drop method, we’ll place our call to disable_raw_mode().

Add the following code to your src/main.rs file, just below the use statement and before the main function.

// src/main.rs

use crossterm::terminal;

// We create a struct to manage the terminal state.
// Its sole purpose is to enable raw mode when it's created
// and disable it automatically when it's dropped (goes out of scope).
// This is an application of the RAII (Resource Acquisition Is Initialization) pattern.
struct RawModeGuard;

// We implement the `Drop` trait for our `RawModeGuard`.
// The `drop` method is called automatically by the Rust compiler when
// an instance of `RawModeGuard` is about to be destroyed.
impl Drop for RawModeGuard {
    fn drop(&mut self) {
        // When the guard is dropped, we ensure that raw mode is disabled.
        // This is crucial for returning the terminal to its original state,
        // even if the program panics.
        // We use .unwrap() here because if disabling raw mode fails,
        // there's not much we can do to recover, and panicking is a reasonable response.
        terminal::disable_raw_mode().unwrap();
        println!("Raw mode disabled, terminal restored."); // A temporary message to see it work
    }
}

fn main() {
    // ... existing code ...
}

Now, let’s update our main function to use this new guard. We will create an instance of RawModeGuard right after we enable raw mode. This variable will exist for the entire duration of the main function. When main finishes, the variable will go out of scope, triggering its drop method and automatically disabling raw mode.

Here are the changes for your main function:

// src/main.rs

// ... (RawModeGuard struct and impl Drop are above) ...

fn main() {
    terminal::enable_raw_mode().unwrap();
    // We create an instance of our guard. The variable name `_raw_mode_guard`
    // starts with an underscore, which is a common Rust convention to indicate
    // that we don't intend to use this variable directly. Its purpose is its
    // existence; it will automatically clean up when it goes out of scope.
    let _raw_mode_guard = RawModeGuard;

    // For now, the program enables raw mode, creates the guard, and then
    // immediately exits. As `main` exits, `_raw_mode_guard` is dropped,
    // and raw mode is disabled.
}

Verifying the Solution

Now, run the program again from your project directory:

cargo run

This time, something different will happen. The program will run and exit almost instantly, but you will see our temporary message, and crucially, your terminal prompt will return to normal!

   Compiling kilo_rs v0.1.0 (/path/to/kilo_rs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/kilo_rs`
Raw mode disabled, terminal restored.
user@machine:~/path/to/kilo_rs$ _

You have successfully implemented a robust cleanup mechanism. This is a fundamental pattern in idiomatic Rust and will be essential for building a stable application. You can now remove the temporary println! from the drop method.

With the terminal mode now safely managed, our next task is to clear the screen and hide the cursor, truly giving us a blank canvas on which to build our editor’s interface.


Further Reading

Clear the Terminal Screen with Crossterm

Mục tiêu: Learn to clear the terminal screen in a Rust application using the crossterm execute! macro. This task also introduces idiomatic Rust error handling with the ? operator by modifying the main function’s signature.


Building on our last task, we’ve successfully implemented the RawModeGuard to ensure our application behaves responsibly, always returning the terminal to its original state. With this safety net in place, we can now confidently start manipulating the terminal. The first step towards creating our editor’s interface is to present the user with a clean, blank screen, free from any previous command-line history.

From State Management to Screen Manipulation

So far, we’ve focused on state—specifically, the state of the terminal’s input mode. Now, we’re shifting our attention to output. How do we tell the terminal to perform actions like clearing the screen, moving the cursor, or changing colors?

The answer lies in sending special command sequences, often called ANSI escape codes, to the terminal’s standard output stream. Manually crafting these sequences is tedious and error-prone. This is another area where crossterm shines. It provides a high-level, cross-platform API for issuing these commands in an idiomatic Rust way.

The execute! Macro

The primary tool crossterm gives us for this is the execute! macro. This macro is designed to take a “writer” (a destination for the output, which will almost always be the terminal’s standard output) and one or more commands. It writes the necessary byte sequences for those commands to the writer and then “flushes” it, ensuring the commands are sent to the terminal and processed immediately.

To use this, we need two things:

  1. A handle to the terminal’s standard output, which we can get from Rust’s standard library via std::io::stdout().
  2. The specific crossterm command for clearing the screen, which is terminal::Clear(terminal::ClearType::All).

Upgrading Our Error Handling with ?

The execute! macro, like many I/O operations, can fail. If it does, it returns a std::io::Result. Until now, we’ve used .unwrap() to handle Result types, which crashes the program on failure. This is a good time to introduce a more idiomatic and robust error-handling pattern in Rust: the question mark ? operator.

The ? operator is syntactic sugar. When placed at the end of an expression that returns a Result, it does the following: * If the Result is Ok(value), it unwraps it and gives you the value. * If the Result is Err(error), it immediately returns from the current function, passing the error up to the caller.

For this to work, the function you’re in must also return a Result type that is compatible with the error being propagated. A common and simple way to set this up in main is to change its signature to fn main() -> std::io::Result<()>. This tells the Rust compiler that our main function can either succeed (returning Ok(()), where () is the empty “unit” type) or fail with an I/O error.

Putting It All Together

Let’s modify src/main.rs to clear the screen. We’ll update our use statements, change the main function’s signature, and use the execute! macro. We’ll also take this opportunity to refactor our previous .unwrap() call to use the ? operator.

Here are the complete changes for your src/main.rs file. Pay close attention to the highlighted additions and modifications.

// src/main.rs

// We add `execute` to our crossterm import.
// We also import `stdout` from the standard library's I/O module.
use crossterm::{execute, terminal};
use std::io::stdout;

struct RawModeGuard;

impl Drop for RawModeGuard {
    fn drop(&mut self) {
        // We can now remove the temporary println! message.
        // The unwrap here is acceptable because if disabling raw mode fails,
        // there is little we can do to recover.
        terminal::disable_raw_mode().unwrap();
    }
}

// UPDATED: Change the main function to return a Result.
// This allows us to use the '?' operator for concise error handling.
fn main() -> std::io::Result<()> {
    // UPDATED: Use the '?' operator instead of .unwrap().
    // If enable_raw_mode fails, main will immediately return the error.
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;

    // --- NEW CODE FOR THIS TASK ---
    // The `execute!` macro takes a writer (stdout()) and one or more commands.
    // `terminal::Clear` is a command that clears the terminal screen.
    // `terminal::ClearType::All` specifies that we want to clear the entire screen,
    // not just a single line or from the cursor down.
    // The '?' operator ensures that if this command fails, the error is returned from main.
    execute!(stdout(), terminal::Clear(terminal::ClearType::All))?;

    // If all operations succeed, we return Ok with the unit type `()`.
    Ok(())
}

What to Expect

Now, when you run your application with cargo run, you will see a slightly different behavior. The terminal will flicker, the entire screen will be cleared of any previous text, and then the program will exit, restoring your terminal prompt on a now-empty screen. You have successfully taken your first step in controlling the terminal’s display.

Our next task is another small but important refinement: hiding the blinking cursor to make our editor’s initial appearance even cleaner.


Further Reading

Hide and Show the Terminal Cursor with Crossterm

Mục tiêu: Learn to hide the terminal cursor on application startup and restore it upon exit using the crossterm::cursor module in Rust. This task involves updating the startup sequence and the RawModeGuard to ensure proper cursor state management for a polished terminal interface.


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

Hiding the Cursor for a Polished Entry

In the last task, you successfully cleared the entire terminal screen, giving us a pristine canvas to work on. It was a big step in taking visual control of the terminal. However, you might have noticed one small detail left over: a blinking cursor at the top-left corner. While essential for user interaction later, it looks a bit out of place on our otherwise empty screen during startup. For a truly professional feel, our editor should control precisely when and where the cursor appears.

Our goal in this task is to hide the cursor when the application starts and, just as importantly, ensure it reappears when the application exits. This continues our theme of responsible terminal management, building upon the RawModeGuard pattern we established earlier.

Introducing the crossterm::cursor Module

Just as the crossterm::terminal module gave us control over the terminal’s mode and screen content, the crossterm::cursor module provides everything we need to manipulate the cursor. It contains commands to move the cursor, change its style, and—for our current task—toggle its visibility.

The two commands we’re interested in are: * cursor::Hide: Sends the command to make the terminal cursor invisible. * cursor::Show: Sends the command to make the cursor visible again.

We will use the same execute! macro we introduced in the previous task to send these commands to the terminal.

Efficiently Combining Commands

A powerful feature of the execute! macro is its ability to take multiple commands at once. Instead of making two separate calls like this:

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

We can combine them into a single call:

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

This is more efficient because it batches the commands and writes them to the terminal in a single operation, potentially reducing the number of underlying system calls and minimizing screen flicker.

Implementing the Changes

Let’s update src/main.rs to hide the cursor on startup and restore it on exit. This involves three small changes:

  1. Add cursor to our use statement.
  2. Add the cursor::Hide command to our execute! macro in main.
  3. Add a command to Show the cursor in our RawModeGuard’s drop method to ensure it’s always restored.

Here is the complete code for src/main.rs with the new changes highlighted.

// src/main.rs

// NEW: Add `cursor` to the crossterm import list.
use crossterm::{cursor, execute, terminal};
use std::io::stdout;

struct RawModeGuard;

impl Drop for RawModeGuard {
    fn drop(&mut self) {
        // UPDATED: In our cleanup code, we now also ensure the cursor is shown.
        // This is crucial because if we don't, the user's terminal will be left
        // without a visible cursor after our program exits.
        // We do this before disabling raw mode.
        execute!(stdout(), cursor::Show).unwrap();
        terminal::disable_raw_mode().unwrap();
    }
}

fn main() -> std::io::Result<()> {
    terminal::enable_raw_mode()?;
    // The _raw_mode_guard ensures that when `main` exits, its `drop` method is called,
    // restoring the terminal state.
    let _raw_mode_guard = RawModeGuard;

    // UPDATED: We combine the Clear command with the new Hide command.
    // The execute! macro can take multiple command arguments.
    // This is more efficient as it performs a single write to the terminal.
    execute!(
        stdout(),
        terminal::Clear(terminal::ClearType::All),
        cursor::Hide
    )?;

    Ok(())
}

Verifying the Result

Run your application now:

cargo run

You should see the screen clear, and this time, there will be no blinking cursor. The program will exit, and your terminal prompt will reappear along with its cursor, thanks to our robust RawModeGuard.

Congratulations! You have now completed the entire first step of the project. You’ve set up a solid foundation: a Cargo project with the necessary dependency, a safe way to enter and exit raw mode, and a clean, cursor-free screen to build upon.

Next Steps

With our terminal properly initialized and under our control, we are ready to build the heart of our editor: the core event loop. In the next step, we will create a loop that continuously waits for user input, processes it, and keeps the application running until the user decides to quit.


Further Reading