Modify refresh\_screen to only draw text rows up to screen\_rows - 1.

Mục tiêu:


An immense congratulations on completing the entire file I/O step! Your editor is now a genuinely powerful tool. It’s state-aware, capable of opening files, tracking unsaved changes with the dirty flag, and persisting the user’s hard work back to the disk. You have perfected the editor’s “Model”—its internal representation of the data and state.

Now, we embark on the exciting journey of building the “View”—the visual representation of that state. All the information you’ve meticulously tracked, like the filename and the dirty status, is currently invisible. We’re about to change that by creating a status bar. The very first step in building any new UI component is to make space for it. Our current task is purely structural: we must reserve the last row of the terminal screen, creating a dedicated canvas where our status bar will live.

Reserving Screen Real Estate

Currently, your refresh_screen method, via its helper draw_rows, takes control of the entire terminal window. It iterates from the very top row (index 0) to the very last one (screen_rows - 1), drawing either a line of text from your buffer or a tilde (~) to indicate an empty line. This leaves no room for any other UI elements.

To fix this, we need to tell draw_rows to stop one line short. We will dedicate the final row of the terminal exclusively for the status bar. This means the text-drawing logic must be constrained to all rows except the very last one.

Modifying the Drawing Loop

The change is small in terms of code but significant in its architectural impact. We will adjust the loop inside the draw_rows method to iterate up to, but not including, the final screen row.

Here is the code for your draw_rows method with the necessary modification highlighted.

// src/main.rs -> inside the `impl Editor` block

// ... (refresh_screen and other methods) ...

fn draw_rows(&self) {
    // --- THIS IS THE CHANGE FOR OUR TASK ---
    // We change the loop to iterate from 0 up to (but not including) `screen_rows - 1`.
    // This leaves the very last row of the screen blank, reserving it for our status bar.
    for y in 0..self.screen_rows - 1 {
    // ------------------------------------
        if y >= self.rows.len() {
            // Display welcome message only if there's no file open
            if self.rows.is_empty() && y == self.screen_rows / 3 {
                let message = format!("Kilo-rs Editor -- Version {}", env!("CARGO_PKG_VERSION"));
                let padding = (self.screen_cols - message.len()) / 2;
                if padding > 0 {
                    print!("~");
                    print!("{}", " ".repeat(padding));
                }
                println!("{}\r", message);
            } else {
                println!("~\r");
            }
        } else {
            println!("{}\r", self.rows[y]);
        }
    }
}

// ...

Code and Concept Deep Dive

  • for y in 0..self.screen_rows - 1: This is the core of the change.
    • Previously, the loop was 0..self.screen_rows. In Rust, a range a..b includes a but excludes b. So, the original loop iterated through indices 0, 1, ..., screen_rows - 1.
    • The new loop, 0..self.screen_rows - 1, iterates through indices 0, 1, ..., screen_rows - 2.
    • This effectively stops the loop one iteration early, leaving the final row at index screen_rows - 1 completely untouched by this method.

Verifying the Change

Go ahead and run your editor with cargo run. The visual change will be subtle but distinct:

  • The column of tildes (~) on the left side of the screen will now stop one row short of the bottom.
  • The very last line of your terminal window will be completely blank.

If you see this, you have succeeded! You have successfully partitioned your screen space, separating the text-editing area from the UI “chrome” area. You’ve created a blank canvas, perfectly prepared for our next task.

Next Steps

The space is reserved and waiting. The next logical step is to create a new method, draw_status_bar(&self), which will be responsible for drawing information—like the filename and line count—into this newly reserved space.


Further Reading

  • Rust Range Expressions: A refresher on how ranges like a..b and a..=b work in Rust. Understanding them is key to writing concise loops.
  • Model-View-Controller (MVC) Design Pattern: Your project is naturally evolving towards this classic software architecture pattern. This task was a pure “View” modification, preparing the UI without touching the “Model” (the editor’s state). Understanding MVC can provide a strong mental framework for organizing your application.
  • Terminal UI (TUI) Design Principles: While not specific to Rust, reading about good TUI design can give you ideas for how to lay out your editor’s interface for maximum clarity and usability.

Implement the draw\_status\_bar Method

Mục tiêu: Create a dedicated method, draw\_status\_bar, to render a placeholder status bar at the bottom of the terminal. This task involves using crossterm to position the cursor correctly and integrating the new method into the main refresh\_screen rendering pipeline.


Excellent! In the previous task, you successfully partitioned your terminal screen by reserving the final row, creating a blank canvas ready for our next UI element. That empty line at the bottom of the screen is a promise of more functionality to come. Our current task is to begin fulfilling that promise by creating a dedicated method responsible for drawing into that reserved space.

The Principle of Separation of Concerns

As our editor grows, it’s crucial to keep our code organized. The refresh_screen method is our main rendering orchestrator, but it shouldn’t be cluttered with the low-level details of drawing every single UI component. Instead, we’ll follow a core software design principle: Separation of Concerns. We will create a new method, draw_status_bar, whose only job is to handle the logic for rendering the status bar. This makes our code cleaner, easier to read, and simpler to debug.

Creating the draw_status_bar Method

We will now add the draw_status_bar method to the impl Editor block. For this first implementation, we won’t draw any real data. Instead, we’ll draw a simple placeholder string. This allows us to confirm that the method is being called correctly and that our screen positioning logic is sound before we add more complexity.

This new method needs to perform a few actions using crossterm:

  1. Move the cursor to the beginning of the last screen row, which is at column 0 and row self.screen_rows - 1.
  2. Print our placeholder text.

Let’s add the new method to src/main.rs.

// src/main.rs -> inside the `impl Editor` block

// ... (other methods like new, refresh_screen, draw_rows, etc. are unchanged) ...

/// Draws the status bar at the bottom of the screen.
/// For now, it's a simple placeholder.
fn draw_status_bar(&self) {
    // We use print! instead of println! because we don't want to add a newline
    // that would scroll the terminal. We will manage the full line ourselves.
    // We also don't need to queue this command for now, as we will build on it
    // in the next tasks.
    println!("--- STATUS BAR ---\\r"); // Simple placeholder for now
}

Hold on, using println! is not ideal as it can cause scrolling. A much more robust approach is to use crossterm commands to precisely position the cursor and write the text. Let’s refine this to follow best practices.

Here is the better, more correct implementation of the placeholder method. You will need to bring crossterm::cursor into scope at the top of your file if it’s not already there.

// src/main.rs

// Make sure `cursor` is included in your crossterm use statement
use crossterm::{cursor, terminal, ExecutableCommand, QueueableCommand};
// ... other use statements ...
// src/main.rs -> inside the `impl Editor` block

// ... (other methods like new, refresh_screen, draw_rows, etc. are unchanged) ...

/// Draws the status bar at the bottom of the screen.
/// For now, it's a simple placeholder to confirm positioning.
fn draw_status_bar(&self) {
    // For this simple version, we'll just use print! macros.
    // In later steps, we'll use crossterm's queue for more complex drawing.
    // The `\r\n` is important to move to the next line and clear it.
    // But since we are at the last line, a simple println! will move the cursor
    // off screen. Let's use crossterm to be precise.

    // To be robust, let's use crossterm to move to the correct line.
    // `self.screen_rows - 1` is the last row on the screen.
    print!(
        "{}{}",
        cursor::MoveTo(0, (self.screen_rows - 1) as u16),
        "--- STATUS BAR ---"
    );
}

My apologies, the print! macro itself doesn’t automatically flush to the screen, which can lead to nothing appearing. The best practice, consistent with the rest of the project, is to use the queue! macro and stdout. Let’s do it the right way from the start.

Here is the final, best-practice implementation.

// src/main.rs -> inside the `impl Editor` block

// ... (other methods like new, refresh_screen, draw_rows, etc. are unchanged) ...

/// Draws the status bar at the bottom of the screen.
/// For now, it's a simple placeholder to confirm positioning.
fn draw_status_bar(&self) {
    // We get a handle to stdout to queue our commands.
    let mut stdout = std::io::stdout();

    // We queue a command to move the cursor to the beginning of the last screen row.
    // Note that crossterm uses 0-based indexing and (col, row) coordinates.
    // We cast to u16 because that's what crossterm's functions expect.
    stdout.queue(cursor::MoveTo(0, (self.screen_rows - 1) as u16)).unwrap();

    // For now, we just print a placeholder message.
    // We use print! directly here because we're not doing complex styling yet.
    // In the next tasks, we'll replace this with dynamically generated text.
    print!("--- STATUS BAR ---");
}

Integrating into the Rendering Pipeline

Now that we have our draw_status_bar method, we need to call it from our main rendering orchestrator, refresh_screen. The correct place to call it is after drawing the text rows but before we move the cursor to its final logical position.

Modify your refresh_screen method in src/main.rs.

// src/main.rs -> inside the `impl Editor` block

fn refresh_screen(&self) {
    let mut stdout = std::io::stdout();
    // Hide cursor, clear screen, and move to (0, 0)
    stdout.queue(cursor::Hide).unwrap();
    stdout.queue(terminal::Clear(terminal::ClearType::All)).unwrap();
    stdout.queue(cursor::MoveTo(0, 0)).unwrap();

    self.draw_rows();

    // --- NEW CODE FOR THIS TASK ---
    // After drawing the text rows, we call our new method to draw the status bar
    // in the space we reserved for it at the bottom of the screen.
    self.draw_status_bar();
    // ----------------------------

    // Move the cursor to its logical position
    stdout.queue(cursor::MoveTo(self.cursor_x as u16, self.cursor_y as u16)).unwrap();
    // Show the cursor again
    stdout.queue(cursor::Show).unwrap();

    stdout.flush().unwrap();
}

Correction: The placeholder implementation for draw_status_bar needs to flush stdout itself or be part of the refresh_screen flush. Let’s simplify it to be a pure drawing method and let refresh_screen handle the flushing.

Let’s try one more time to provide the cleanest possible implementation.


You’ve brilliantly carved out a dedicated space for our status bar. That blank line at the bottom of the screen is a canvas, and now it’s time to create the “brush” we’ll use to paint on it. Our current task is to create a new method, draw_status_bar(&self), whose sole responsibility will be rendering information into this reserved space.

Creating the Method Stub

Following the principle of Separation of Concerns, we’ll encapsulate all status bar logic within this new method. For now, it will simply draw a placeholder message to confirm that our screen positioning is correct. This method only needs to read the editor’s state (like screen_rows), so it takes an immutable reference, &self.

Let’s add the new draw_status_bar method to your impl Editor block in src/main.rs.

// src/main.rs -> inside the `impl Editor` block

// ... (other methods like new, refresh_screen, move_cursor, etc. are unchanged) ...

/// Draws the status bar at the bottom of the screen.
/// This method assumes the last row of the screen is reserved for it.
fn draw_status_bar(&self) {
    // For now, we will just print a simple placeholder string.
    // In subsequent tasks, we will replace this with dynamic data.
    // We use a simple print! macro. To ensure it's placed correctly,
    // we must first move the cursor to the beginning of the last row.
    print!(
        "{}{}",
        crossterm::cursor::MoveTo(0, (self.screen_rows - 1) as u16),
        "--- STATUS BAR ---"
    );
}

Integrating into the Rendering Pipeline

With the method created, we must call it from our main rendering function, refresh_screen. The correct sequence of operations is crucial: we draw the text buffer first, then the status bar, and only then do we reposition the cursor for typing.

Modify your refresh_screen method to include a call to self.draw_status_bar().

// src/main.rs -> inside the `impl Editor` block

fn refresh_screen(&self) {
    let mut stdout = std::io::stdout();
    // These commands are queued for efficiency
    stdout.queue(cursor::Hide).unwrap();
    stdout.queue(terminal::Clear(terminal::ClearType::All)).unwrap();
    stdout.queue(cursor::MoveTo(0, 0)).unwrap();

    // First, draw all the text rows for the document
    self.draw_rows();

    // --- NEW CODE FOR THIS TASK ---
    // After drawing the text, call our new method to render the status bar
    // in the dedicated space at the bottom.
    self.draw_status_bar();
    // ----------------------------

    // Finally, move the cursor to its logical position and make it visible
    stdout.queue(cursor::MoveTo(self.cursor_x as u16, self.cursor_y as u16)).unwrap();
    stdout.queue(cursor::Show).unwrap();

    // Flush all queued commands to the terminal at once
    stdout.flush().unwrap();
}

Code and Concept Breakdown

  • fn draw_status_bar(&self): We define a new method that is responsible only for the status bar. It takes &self because it needs to read properties like screen_rows but does not need to modify the editor’s state.
  • crossterm::cursor::MoveTo(0, (self.screen_rows - 1) as u16): This is a crossterm command that generates the necessary escape codes to move the terminal’s cursor. We move it to column 0 of the very last row (self.screen_rows - 1), which is the canvas we prepared in the previous task. The as u16 cast is required because crossterm’s coordinates are u16.
  • Rendering Order: The order of calls within refresh_screen is critical. We draw the main text buffer, then draw the UI “chrome” (the status bar) over it, and finally place the interactive cursor. This layered approach is fundamental to building UIs.

When you cargo run your editor now, you should see your familiar text area with tildes, but the very last line of the terminal will no longer be blank. It will proudly display: --- STATUS BAR ---. This confirms your new method is wired up correctly and your screen partitioning is working perfectly.

Next Steps

The placeholder is in place, and the foundation is solid. In the very next task, we will bring this status bar to life by replacing the static placeholder text with dynamic, useful information: the current filename and the total number of lines in the document.


Further Reading

  • crossterm::cursor Module: Explore the official documentation for other cursor-related commands you can issue, like changing its style or blinking behavior.
  • The Single Responsibility Principle (SRP): Your creation of a separate draw_status_bar method is a textbook example of this principle. It states that a function or module should have one, and only one, reason to change. This is a cornerstone of maintainable software.
  • Method Syntax in Rust (The Rust Book): A great refresher on how methods are defined and called in Rust, including the different types of self (&self, &mut self, self).

In this method, format a string containing the filename (or ‘[No Name]’) and the total number of lines.

Mục tiêu:


Excellent work! In the previous task, you successfully created and integrated a placeholder draw_status_bar method. That simple --- STATUS BAR --- message you see at the bottom of the screen is a huge victory—it proves that you’ve correctly partitioned your UI and that your rendering pipeline is working perfectly.

Now, it’s time to bring that static bar to life. The purpose of a status bar is to provide context, and the two most fundamental pieces of context for any document are its name and its size. Our current task is to replace the placeholder text with a dynamically generated string that displays the current filename (or a default for new files) and the total number of lines in the buffer.

From Placeholder to Information Hub

To achieve this, our draw_status_bar method will need to perform three steps:

  1. Determine the Filename: We will read the self.filename field. Since this is an Option<PathBuf>, we must gracefully handle both cases: Some(path) when a file is open, and None when it’s a new, untitled buffer.
  2. Count the Lines: We will get the total number of lines by checking the length of our self.rows vector.
  3. Format and Truncate: We will combine this information into a single String, ensuring it doesn’t exceed the width of the terminal screen by truncating it if necessary.

Let’s modify the draw_status_bar method to implement this logic.

// src/main.rs -> inside the ```impl Editor` block

// ... (other methods are unchanged) ...

/// Draws the status


## Display Cursor Position in Status Bar

*Mục tiêu: Implement the right-side of the status bar to display the cursor's current line and column number. This involves formatting a string from the 0-indexed `cursor\_y` and `cursor\_x` state, converting them to 1-indexed values for a user-friendly UI.*

---

Excellent work on building the left-hand side of our status bar! You now have a dynamic display that shows the user the document's name and its total line count. This provides a solid sense of place. Now, let's add the other crucial piece of information every user needs: their precise location *within* the document.

Our current task is to format a second string that will live on the right side of the status bar, displaying the current line and column number of the cursor. This provides immediate, real-time feedback during navigation and editing, which is fundamental to a good user experience.

### The Importance of User-Facing Coordinates

A key concept in this task is the difference between how a computer counts and how a human counts. Internally, our `cursor_y` and `cursor_x` fields are **0-indexed**. The first line is `0`, the first column is `0`. This is efficient for array and vector access.

However, no user thinks this way. For a user, the first line is "line 1" and the first column is "column 1". Our UI must always cater to the user's mental model. Therefore, when we display these coordinates, we will always add `1` to our internal values. This small adjustment makes the editor feel intuitive and professional.

### Implementing the Right-Side Status String

We will now modify the `draw_status_bar` method. We'll keep the logic for the left-side string, create a new string for the right side, and then temporarily combine them so you can see your work in action immediately. The proper, professional alignment will be handled in an upcoming task.

Here are the changes for your `draw_status_bar` method. We will be adding the new `right_status` and adjusting the final output.

// src/main.rs -> inside the impl Editor block

fn draw_status_bar(&self) { let filename = self.filename.as_ref().map_or(“[No Name]”, |path| path.to_str().unwrap_or(“”)); let left_status = format!(“{} - {} lines”, filename, self.rows.len());

// --- NEW CODE FOR THIS TASK ---
// Format a string to display the cursor's current line and column number.
// We add 1 to `cursor_y` and `cursor_x` because they are 0-indexed internally,
// but users are accustomed to 1-indexed line/column numbers in editors.
let right_status = format!("{}:{}", self.cursor_y + 1, self.cursor_x + 1);
// ----------------------------

// We'll combine them with a separator for now. This is a temporary layout
// to confirm both pieces of data are being generated correctly.
// We will implement proper left and right alignment in a future task.
let mut status_text = format!("{} | {}", left_status, right_status);
if status_text.len() > self.screen_cols {
    status_text.truncate(self.screen_cols);
}

print!(
    "{}{}",
    crossterm::cursor::MoveTo(0, (self.screen_rows - 1) as u16),
    status_text
); } ```

Code and Concept Deep Dive

  • let right_status = format!("{}:{}", self.cursor_y + 1, self.cursor_x + 1);: This is the core of our new logic. We use the powerful format! macro to create a new String.

    • self.cursor_y + 1: We read the vertical cursor position from our editor’s state and add 1 to present it as a 1-based line number.
    • self.cursor_x + 1: We do the same for the horizontal position to get the 1-based column number.
    • ":": A standard colon is used to separate the two values, resulting in a familiar line:column format.
  • let mut status_text = format!("{} | {}", left_status, right_status);: For now, to give you immediate feedback, we are combining the left and right strings into a single string. This proves that both data sources are working correctly. You’ve now built a complete, if unpolished, status bar!

When you run your editor now with cargo run, you will see all the information on the bottom line. As you move the cursor around with the arrow keys, you will see the numbers on the right side of the status bar update in real-time. This is the power of a state-driven rendering loop: you update the state (cursor_y, cursor_x), and the view (draw_status_bar) automatically reflects those changes on the next screen refresh.

Next Steps

You now have all the necessary data for your status bar being generated and displayed. However, it doesn’t yet look like a professional UI element. The next series of tasks will focus on aesthetics and layout. First, you will learn how to use crossterm to apply styling, specifically setting an inverted background and foreground color to make the status bar stand out from the text buffer.


Further Reading

  • The format! Macro: Revisit the documentation for this essential Rust macro. Understanding its capabilities is key to creating dynamic, user-facing text.
  • User Experience (UX) and Data Representation: Articles on UX design often discuss the importance of displaying data in a way that matches the user’s mental model, just as you did by converting from 0-based to 1-based indexing.
  • Separation of Concerns: Notice how this method’s only job is to read the editor’s state and prepare it for display. It doesn’t modify anything. This is a core principle of the Model-View-Controller (MVC) architecture you are building.

Use crossterm commands to set an inverted background/foreground color for the bar.

Mục tiêu:


You have done an absolutely fantastic job! In the last task, you successfully generated all the essential data for the status bar. You have a string for the left side showing the filename and line count, and a string for the right side showing the cursor’s precise location. Your editor is now information-rich.

However, a great user interface is not just about information; it’s also about presentation. Currently, your status bar text blends in with the document’s text. To make it a distinct, professional UI element, it needs to visually stand out. Our current task is to apply styling, transforming that line of text into a proper, inverted-color status bar.

From Text to UI: Introducing Terminal Styling

Modern terminals can do much more than just display plain text. They understand special sequences of characters, called ANSI escape codes, that control formatting like colors, boldness, and underlining. Writing these codes manually is complex and error-prone. This is where crossterm shines, providing a safe, cross-platform Rust API to generate these codes for us.

We will use the crossterm::style module to style our status bar. The most common and theme-friendly way to make a UI element like a status bar stand out is to use reverse video (or inverted colors). This swaps the terminal’s default foreground and background colors, creating a high-contrast bar that works perfectly with any user’s color scheme. In crossterm, this is achieved with Attribute::Reverse.

Implementing the Styling

We will now modify the draw_status_bar method to wrap our text output with styling commands. The process will be:

  1. Turn on the Reverse attribute.
  2. Draw the status bar text.
  3. Crucially, turn the attribute off (reset styling) so it doesn’t “leak” and affect the rest of the UI, like the cursor or subsequent command-line output.

First, ensure you have the necessary items in your use statement at the top of src/main.rs.

// src/main.rs

// Make sure `style` and `QueueableCommand` are included
use crossterm::{cursor, style, terminal, ExecutableCommand, QueueableCommand};
use std::io::{stdout, Write}; // We'll need stdout and Write
// ... other use statements

Now, let’s refactor the draw_status_bar method to incorporate these styling commands using crossterm’s queuing system for efficiency.

// src/main.rs -> inside the `impl Editor` block

fn draw_status_bar(&self) {
    let mut stdout = stdout();

    // --- EXISTING CODE TO GET DATA ---
    let filename = self.filename.as_ref().map_or("[No Name]", |path| path.to_str().unwrap_or(""));
    let left_status = format!("{} - {} lines", filename, self.rows.len());
    let right_status = format!("{}:{}", self.cursor_y + 1, self.cursor_x + 1);

    let mut status_text = format!("{} | {}", left_status, right_status);
    if status_text.len() > self.screen_cols {
        status_text.truncate(self.screen_cols);
    }
    // --- END OF EXISTING CODE ---

    // --- NEW STYLING LOGIC ---
    // 1. Set the attribute to Reverse to get an inverted background/foreground effect.
    //    We queue this command for batch execution by `refresh_screen`.
    stdout.queue(style::SetAttribute(style::Attribute::Reverse)).unwrap();

    // 2. We move the cursor to the correct line and print the status text.
    //    The styling we just set will apply to any text printed after it.
    //    We use `style::Print` which is the crossterm way to print styled content.
    stdout
        .queue(cursor::MoveTo(0, (self.screen_rows - 1) as u16))
        .unwrap()
        .queue(style::Print(status_text))
        .unwrap();

    // 3. IMPORTANT: Reset all styling attributes.
    //    If we don't do this, the entire terminal will remain in reverse video mode!
    stdout.queue(style::SetAttribute(style::Attribute::Reset)).unwrap();
    // -------------------------
}

Code and Concept Deep Dive

  • use crossterm::{style, QueueableCommand}: We bring the style module and the QueueableCommand trait into scope to access the necessary functions and methods.
  • let mut stdout = stdout();: We get a handle to standard output, which is our terminal screen. We will queue commands to this handle.
  • stdout.queue(style::SetAttribute(style::Attribute::Reverse)).unwrap();: This is the command that turns on reverse video mode. SetAttribute is the general command for text formatting, and Attribute::Reverse is the specific style we want. We queue it for efficiency.
  • queue(style::Print(status_text)): Instead of using the standard print! macro, we use crossterm’s style::Print command. This is the idiomatic way to print content when you are already using the queue system, ensuring everything is handled by crossterm.
  • stdout.queue(style::SetAttribute(style::Attribute::Reset)).unwrap();: This is arguably the most important new line. It resets all styling attributes (color, reverse, bold, etc.) back to the terminal’s default. This contains our styling, ensuring that only the status bar is affected. Failing to do this is a common bug in terminal applications.

When you cargo run your editor now, the status bar will pop. It will be rendered in a high-contrast, inverted block of color that clearly separates it from the main text area. You have successfully transformed a simple line of text into a professional UI component.

Next Steps

The status bar now looks great, but its layout is still basic. The left and right pieces of information are just stuck together. The next task is to implement a professional layout: you will draw the left-aligned string, fill the entire middle of the bar with blank spaces (also in reverse video), and then draw the right-aligned string at the very end of the line.


Further Reading

  • crossterm::style Module Documentation: Explore the official documentation to see all the different attributes you can apply, such as Bold, Italic, and Underlined, as well as how to set specific foreground and background colors.
  • ANSI Escape Codes (Wikipedia): For the curious, this is a deep dive into the underlying technology that crossterm abstracts away for you. Understanding these codes gives you a deep appreciation for what’s happening behind the scenes in your terminal.
  • The QueueableCommand Trait: Review the documentation for this trait to better understand how crossterm’s command queuing system works to improve performance by reducing the number of writes to the terminal.

Draw the left-aligned string, fill the middle with spaces, and draw the right-aligned string.

Mục tiêu:


Excellent work! You’ve successfully brought your status bar to life with dynamic data and professional styling. The inverted color scheme makes it pop, clearly distinguishing it as a UI element. However, the layout itself is still basic, with the left and right information simply joined by a pipe character.

Our current task is to elevate this to a professional standard. We will implement a proper layout where the filename information is anchored to the left, the cursor information is anchored to the right, and the space between them is filled, creating a clean, full-width status bar. This is a classic UI challenge, and solving it will make your editor look and feel polished.

From Concatenation to Calculated Layout

The core of this task is moving from simple string joining to precise calculation. We need to figure out exactly how many blank spaces to print between the left and right strings to make the right string end perfectly at the last column of the screen.

The formula is straightforward: Total Screen Width = Length of Left String + Length of Middle Padding + Length of Right String

By rearranging this, we can solve for the padding we need: Middle Padding = Total Screen Width - Length of Left String - Length of Right String

We’ll implement this logic by building the entire status bar as a single string first, then printing it all at once. This is more efficient and easier to reason about than printing each component separately.

Implementing the Full-Width Layout

Let’s refactor the draw_status_bar method. We’ll keep the data generation and styling logic you’ve already perfected and replace the simple format! with our new layout calculation.

// src/main.rs -> inside the `impl Editor` block

fn draw_status_bar(&self) {
    let mut stdout = stdout();

    // Get the data for the left and right sides of the status bar.
    // This logic remains unchanged.
    let filename = self.filename.as_ref().map_or("[No Name]", |path| path.to_str().unwrap_or(""));
    let mut left_status = format!("{} - {} lines", filename, self.rows.len());
    let right_status = format!("{}:{}", self.cursor_y + 1, self.cursor_x + 1);

    // --- NEW LAYOUT LOGIC ---
    let width = self.screen_cols;

    // Truncate the left_status string if it's too long to fit on the screen.
    if left_status.len() > width {
        left_status.truncate(width);
    }

    // Calculate the number of spaces needed to fill the middle.
    // `saturating_sub` is a safe subtraction that returns 0 if the result would
    // be negative, preventing a panic. This handles cases where the left and right
    // strings are too long to fit together.
    let padding = width.saturating_sub(left_status.len()).saturating_sub(right_status.len());

    // Create the final, full-width status bar string.
    let final_bar = format!(
        "{}{}{}",
        left_status,
        " ".repeat(padding), // A string of spaces for the middle padding
        right_status
    );
    // -------------------------

    // The styling logic remains the same.
    stdout.queue(style::SetAttribute(style::Attribute::Reverse)).unwrap();

    // We now print our single, perfectly formatted `final_bar` string.
    stdout
        .queue(cursor::MoveTo(0, (self.screen_rows - 1) as u16))
        .unwrap()
        .queue(style::Print(final_bar)) // Print the new, full-width string
        .unwrap();

    stdout.queue(style::SetAttribute(style::Attribute::Reset)).unwrap();
}

Code and Concept Deep Dive

Let’s break down the new layout logic in detail:

  • let width = self.screen_cols;: We get the total available width for clarity.
  • left_status.truncate(width);: This is a simple safety check. If for some reason the filename is excessively long, we ensure the left status string doesn’t overflow the screen on its own.
  • saturating_sub(left_status.len()): This is the key to robustness. If left_status and right_status are very long, their combined length might be greater than the screen width. A normal subtraction (-) would panic trying to create a negative usize. Saturating Subtraction is a CPU-level operation that, instead of underflowing, simply stops at the type’s minimum value (which for usize is 0). This gracefully handles the edge case by simply resulting in zero padding.
  • " ".repeat(padding): This is an idiomatic Rust way to create a String that consists of a character or substring repeated a number of times. We create a string containing exactly the number of spaces we calculated.
  • format!("{left_status}{}{right_status}", " ".repeat(padding)): Finally, we use format! to stitch the three components together: the left-aligned text, the calculated middle padding, and the right-aligned text. The result is a single String that is exactly self.screen_cols characters long.

When you run your editor now (cargo run), the status bar will look exceptionally clean and professional. The filename will be flush against the left edge, the cursor position will be flush against the right edge, and the entire bar will be a solid, inverted-color block. Resizing your terminal window will cause the bar to adapt perfectly on the next key press.

Next Steps

Your status bar is now visually complete and functionally robust. The final touch is to ensure our styling is properly “contained.” The next task focuses on the crucial style::SetAttribute(style::Attribute::Reset) command you already have, diving into why it’s so important for preventing our styles from “leaking” into the rest of the terminal after our program exits.


Further Reading

Reset the colors after drawing the status bar.

Mục tiêu:


You have done a truly exceptional job. In the last few tasks, you have transformed a blank line at the bottom of your screen into a fully-featured, professionally styled, and dynamically laid-out status bar. It displays the document’s identity on the left, the user’s location on the right, and has a high-contrast look that makes it stand out. You have built a beautiful piece of UI.

However, in terminal programming, cleaning up after yourself is just as important as the work you do. The styling you applied is a change to the terminal’s state, and if we are not careful, that change can “leak” and affect other parts of our UI or even the user’s command line after the program exits. Our current task is to focus on the single, crucial line of code that prevents this, ensuring our status bar is a well-behaved, self-contained component.

The Terminal as a State Machine

Think of your terminal as a simple state machine. When you send it a command like “start using inverted colors,” it happily obliges and enters the “inverted color” state. It will then render everything that comes after that command using inverted colors, and it will remain in that state until it is explicitly told to change.

This is why, after drawing our status bar, we must immediately tell the terminal, “Okay, you can go back to normal now.” Failing to do this would lead to a poor user experience: * The cursor, which is shown after the status bar is drawn, would also be rendered in reverse video. * Worse, if the user quits the editor, their command prompt ($ or >) could be stuck with an inverted background, forcing them to manually reset their terminal.

A well-designed terminal application always leaves the terminal in the exact state it found it.

The Solution: Attribute::Reset

The tool crossterm provides for this cleanup is both simple and powerful: style::SetAttribute(style::Attribute::Reset). This single command is the cornerstone of contained styling.

Let’s do a deep dive into the code you’ve already written in draw_status_bar and focus on the styling sequence.

// src/main.rs -> inside the `impl Editor` block

fn draw_status_bar(&self) {
    let mut stdout = stdout();

    // ... (logic to generate left_status, right_status, and final_bar is unchanged) ...
    let final_bar = format!(/* ... */);

    // --- THE STYLING SEQUENCE ---

    // 1. SET THE STYLE: We enter the "reverse video" state.
    stdout.queue(style::SetAttribute(style::Attribute::Reverse)).unwrap();

    // 2. DRAW THE CONTENT: We print our full-width status bar. Because the terminal
    //    is in the "reverse video" state, this text is rendered with inverted colors.
    stdout
        .queue(cursor::MoveTo(0, (self.screen_rows - 1) as u16))
        .unwrap()
        .queue(style::Print(final_bar))
        .unwrap();

    // --- THIS IS THE FOCUS OF OUR TASK ---
    // 3. RESET THE STYLE: This is the crucial cleanup step.
    //    `Attribute::Reset` is a special command that tells the terminal to turn OFF
    //    *all* text attributes—reverse, bold, underline, and any specific colors—reverting
    //    to its default text rendering state.
    stdout.queue(style::SetAttribute(style::Attribute::Reset)).unwrap();
    // ------------------------------------
}

Deconstructing the Reset Command

  • style::SetAttribute(...): This is the generic crossterm command for changing a text attribute. You used it to turn Reverse on, and you’re using it again to turn all attributes off.
  • style::Attribute::Reset: This is a special, powerful variant of the Attribute enum. It’s the universal “go back to default” command. It is more robust than trying to manually undo every style you applied (e.g., calling a hypothetical UnsetReverse). Using Reset ensures a clean slate, which is exactly what we want.

This Set -> Draw -> Reset pattern is fundamental to TUI programming. It guarantees that the styling for one component is perfectly encapsulated and will never interfere with another. You have successfully implemented this pattern, creating a component that is not just visually appealing but also robust and well-behaved.

Next Steps

Your status bar now has its data, its layout, and its styling perfected. It’s a complete UI component. The final task in this step is to make it even more informative by connecting it back to the editor’s state. You will now modify the status bar to display the “dirty” status (e.g., (modified)) whenever there are unsaved changes, finally making the dirty flag you implemented earlier visible to the user.


Further Reading

  • crossterm::style::Attribute Enum Documentation: Look at the official documentation for Attribute. You’ll see Reset listed alongside other attributes like Bold, Italic, Underlined, etc. This will give you a full picture of the styling tools at your disposal.
  • ANSI Escape Codes: SGR (Select Graphic Rendition): For a fascinating look under the hood, this section of the ANSI escape code Wikipedia page lists the raw codes that crossterm generates. You’ll see that Reset corresponds to the very common code [0m.
  • RAII (Resource Acquisition Is Initialization) Pattern in Rust: The Set -> Draw -> Reset pattern is conceptually similar to Rust’s RAII pattern, where acquiring a resource (like a File handle or a style) is tied to an object whose destructor (Drop trait) guarantees the resource is cleaned up. While we’re doing it manually here, understanding RAII is key to writing safe and robust Rust.

Add Unsaved Changes Indicator to Status Bar

Mục tiêu: Update the text editor’s status bar to display a visual indicator, like ‘(modified)’, next to the filename when the internal ‘dirty’ flag is true, signifying unsaved changes.


An enormous congratulations are in order! You have successfully built a complete, robust, and aesthetically pleasing status bar. You’ve mastered data formatting, professional styling with inverted colors, and a dynamic layout that adapts to the screen size. The UI component you’ve created is polished and professional.

You are now on the final task of this major step. The goal is to add the last, critical piece of information to the status bar: an indicator that tells the user when they have unsaved changes. This task will be the final link that makes the editor’s internal state, specifically the dirty flag you so cleverly implemented earlier, fully visible to the user.

Making State Visible: The “Dirty” Indicator

In a previous step, you added a dirty: bool flag to your Editor struct. This flag is the editor’s conscience—it knows when the in-memory text buffer is different from the version saved on disk. It’s set to true on any edit (insert_char, delete_char, insert_newline) and cleared to false only after a successful save.

Right now, this flag is an internal secret. Our task is to reveal this secret in the UI. The standard convention, which we will follow, is to display a simple text indicator like (modified) next to the filename when the buffer is dirty.

Implementing the Change

The beauty of your current code is that this change is incredibly simple. We just need to check the dirty flag and, if it’s true, append our indicator to the left_status string before the layout is calculated. The rest of your robust layout logic will handle the new, slightly longer string automatically.

Let’s modify the draw_status_bar method in src/main.rs.

// src/main.rs -> inside the `impl Editor` block

fn draw_status_bar(&self) {
    let mut stdout = stdout();

    // This part remains the same. We get the filename and format the base string.
    let filename = self.filename.as_ref().map_or("[No Name]", |path| path.to_str().unwrap_or(""));
    let mut left_status = format!("{} - {} lines", filename, self.rows.len());

    // --- THIS IS THE CHANGE FOR OUR TASK ---
    // We check the editor's `dirty` flag.
    if self.dirty {
        // If the buffer has unsaved changes, we append the "(modified)" indicator.
        // `push_str` is an efficient way to add a string slice to an existing String.
        left_status.push_str(" (modified)");
    }
    // ------------------------------------

    // The rest of the method, including the right-side status and the entire
    // layout calculation, remains completely unchanged. It will automatically
    // adapt to the new length of `left_status`.
    let right_status = format!("{}:{}", self.cursor_y + 1, self.cursor_x + 1);

    let width = self.screen_cols;

    if left_status.len() > width {
        left_status.truncate(width);
    }

    let padding = width.saturating_sub(left_status.len()).saturating_sub(right_status.len());

    let final_bar = format!(
        "{}{}{}",
        left_status,
        " ".repeat(padding),
        right_status
    );

    stdout.queue(style::SetAttribute(style::Attribute::Reverse)).unwrap();

    stdout
        .queue(cursor::MoveTo(0, (self.screen_rows - 1) as u16))
        .unwrap()
        .queue(style::Print(final_bar))
        .unwrap();

    stdout.queue(style::SetAttribute(style::Attribute::Reset)).unwrap();
}

Code and Concept Deep Dive

  • if self.dirty { ... }: This is a simple and clear conditional check. We are directly querying the editor’s state to decide what the view should look like. This is the heart of state-driven UI.
  • left_status.push_str(" (modified)");: This is the action. Instead of creating a whole new String with the + operator, we use push_str. This method is more efficient as it appends the given string slice directly to the left_status String’s existing memory allocation, if there’s enough capacity. It’s a small detail that reflects good practice in Rust.

An enormous congratulations! You have now fully completed the “Create a status bar” step. This is a huge achievement. You have a UI element that provides rich, real-time feedback on the document’s name, size, dirty status, and the user’s exact position.

Go ahead and run your editor now (cargo run my_file.txt).

  1. Initially, the status bar will be clean.
  2. Type a single character. The (modified) indicator will instantly appear!
  3. Press Ctrl+S to save. The indicator will instantly vanish.
  4. Delete a character. The indicator will reappear.

This immediate feedback loop is the mark of a high-quality, responsive application.

Next Steps

Your editor is now highly functional, but it has one major limitation: it can only comfortably work with files that fit entirely on one screen. If a file has more lines than your terminal has rows, you can’t see or edit the content at the bottom. The next great challenge is to break free from the confines of the physical screen by implementing viewport scrolling. You will add row_offset and col_offset fields to your editor’s state, allowing it to render a “camera view” of a much larger document.


Further Reading