Vibe Engines
YouTube

Lesson 3.5 · 11 min

Reading a line

Chatterbox has been talking to a canned list. This lesson plugs in the keyboard: it reads a line you type, answers it, and goes back for the next, until you type /quit.

Adds to ChatterboxChatterbox reads what you type.

Where input comes from

The line use std::io; at the top brings Rust's input and output tools into reach, so you can write io::stdin() instead of the full path.

A program has an input stream, called standard input or stdin. When you run a program in a terminal, whatever you type goes there. To pull one line out of it you call read_line.

The in-browser runner on this page has no keyboard to read, so this lesson works in two halves. First the real program, which you run in your own terminal. Then the same thinking as a function over text, which you can test here.

Read one line, show it
use std::io;
 
fn main() {
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
println!("you typed: {}", line.trim());
}

Four new pieces, and you only need the idea of each for now.

  1. String::new() makes an empty piece of text that can grow. It is let mut because read_line is going to write into it.
  2. &mut line means "here, you may change this". The &mut is lending the text out to be changed. Chapter 4 explains lending in full; for now, read it as "write your answer here".
  3. .unwrap() means "if reading failed, stop the program". Chapter 7 replaces it with something kinder.
  4. .trim() removes whitespace at both ends. read_line keeps the Enter key you pressed as a newline at the end of the text, so without trim a typed /quit would arrive as /quit plus a newline, and would match no arm.

That last point is worth checking by eye. Here is a real run with the input piped in:

Real run, with the input piped in (cargo run -q hides the build chatter)
$ echo "hello there" | cargo run -q
you typed: hello there

Reading until there is nothing left

read_line also returns how many bytes it read. When the input has ended (you pressed Ctrl-D, or a pipe ran dry) that number is 0. A chat loop has to notice, or it spins forever on empty reads. So the loop has two ways out: the user types /quit, or the input ends.

Before the loop, one more important piece: line.clear() at the top of each turn. read_line adds to what is there, it does not replace it.

src/main.rs: Chatterbox v0.3, finished
use std::io;
 
fn reply(msg: &str) -> String {
match msg {
"" => String::from("(nothing to say)"),
"/help" => String::from("commands: /help, /quit"),
other => format!("you: {other}"),
}
}
 
fn main() {
println!("Chatterbox v0.3. Type /help or /quit.");
let mut line = String::new();
let mut message_count = 0;
 
loop {
line.clear();
let bytes = io::stdin().read_line(&mut line).unwrap();
if bytes == 0 {
break;
}
let message = line.trim();
if message == "/quit" {
println!("bye!");
break;
}
println!("{}", reply(message));
message_count += 1;
}
println!("messages so far: {message_count}");
}

That is the whole program, and it is Chatterbox v0.3: it loops, reads, decides with match, and answers with a function. A session looks like this:

Five lines in (one of them blank), the same program out
$ printf 'hi\n/help\n\nhow are you\n/quit\n' | cargo run -q
Chatterbox v0.3. Type /help or /quit.
you: hi
commands: /help, /quit
(nothing to say)
you: how are you
bye!
messages so far: 4
Try itYour turn: a chat session as text

Finish run_chat. For each line of input: trim it, stop at /quit (adding bye! first), otherwise add reply(line). Every output line ends with a newline.

Finish run_chat. For each line of input: trim it, stop at /quit (adding bye! first), otherwise add reply(line). Every output line ends with a newline.

fn reply(msg: &str) -> String {
match msg {
"" => String::from("(nothing to say)"),
"/help" => String::from("commands: /help, /quit"),
other => format!("you: {other}"),
}
}
 
fn run_chat(input: &str) -> String {
let mut out = String::new();
for line in input.lines() {
let message = line.trim();
// TODO: /quit: add "bye!\n" and stop. Otherwise add reply(message) and a newline.
}
out
}
 
fn main() {
print!("{}", run_chat("hi\n/help\n/quit\nnever seen\n"));
}

Hidden tests (4): echoes_and_helps no_input_no_output quit_stops_the_chat whitespace_is_trimmed

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

What you have now

Chapter 3 is done and Chatterbox is a real, if shy, chat program. It reacts to what you type, it knows two commands, and its logic is in a function you can test. Everything it says is text it holds for a moment and forgets. In chapter 4 we look at the idea Rust is built around, ownership: who is responsible for a piece of text while it moves between main and reply.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
Why call trim() on what read_line gave you?Reveal
The text still ends with the newline from pressing Enter, so "/quit" would not match "/quit" plus a newline.
What does read_line returning 0 mean?Reveal
The input has ended, so the loop should stop.
Why does the exercise take a &str of lines instead of reading the keyboard?Reveal
A function over text can be tested and run anywhere, including the Playground, which has no keyboard. The loop is the same.

That is lesson 3.5. Tick it off so the course remembers where you are.

Learn Rust by building a chat app · Lesson 3.5 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026