A prompt that reads

Everything a shell does starts with one loop: print a prompt, read a line, do something with it. Milestone one is that loop, honest and unadorned.

use std::io::{self, Write};

fn main() {
    loop {
        print!("tiny$ ");
        io::stdout().flush().expect("flush failed");

        let mut line = String::new();
        match io::stdin().read_line(&mut line) {
            Ok(0) => break, // EOF: ctrl-d ends the session
            Ok(_) => println!("you typed: {}", line.trim_end()),
            Err(err) => eprintln!("read error: {err}"),
        }
    }
}

read_line returning Ok(0) is how end-of-input shows up — that is your ctrl-d exit. Everything else just echoes for now; the next milestone starts making sense of the line.