Hello, terminal
Here is the smallest complete uncurses program. It prints a line, waits for you
to press q, and hands the terminal back cleanly.
use uncurses::buffer::Bounded;
use uncurses::event::{Event, Key};
use uncurses::screen::Screen;
use uncurses::style::Style;
use uncurses::text::TextSurface;
fn main() -> std::io::Result<()> {
let mut screen = Screen::stdio()?;
screen.init()?;
let w = screen.width();
screen.resize((w, 2));
screen.set_str((0, 0), "Hello! Press q to quit.", Style::new());
screen.render()?;
let q: Key = "q".parse().unwrap();
loop {
let ev = screen.read_event()?;
screen.observe_event(&ev)?;
if matches!(ev, Event::KeyPress(k) if k == q) {
break;
}
}
screen.finish()
}Run it, see your line, press q, and you are back at the shell prompt. The
inline rows are reset, and your earlier scrollback stays intact. That is the
whole contract.
Line by line
Open the screen.
let mut screen = Screen::stdio()?;
screen.init()?;Screen::stdio() wires the screen to standard input and output. init() enters
raw mode and starts capability discovery. A session is bracketed by init() and
finish(); before init(), construction does not change terminal modes.
After init(), the screen starts inline: it draws in the normal buffer, right
where your cursor already is, and it leaves the cursor visible. The alternate
screen and a hidden cursor are opt-in, which we will get to below.
Claim some space.
let w = screen.width();
screen.resize((w, 2));Inline, the screen owns however many rows you ask for. Here we take the full width and make it two rows tall: one for the text, and one left empty below it. That trailing blank row means when the program finishes, the shell prompt comes back on a fresh line of its own instead of butting up against your last line.
Draw, then show.
screen.set_str((0, 0), "Hello! Press q to quit.", Style::new());
screen.render()?;set_str paints text at an x/y position into an in-memory frame. Nothing
reaches the terminal until render(), which diffs the new frame against what is
already on screen and writes only the difference. Painting cannot fail; only
render() talks to the terminal, so only render() returns a Result.
Wait for input.
let q: Key = "q".parse().unwrap();
loop {
let ev = screen.read_event()?;
screen.observe_event(&ev)?;
if matches!(ev, Event::KeyPress(k) if k == q) {
break;
}
}read_event() blocks until something happens: a keypress, a resize, a paste.
Passing each event to observe_event() is optional; it lets the screen keep
terminal discovery and resize state current, and skipping it still reads fine.
Here we loop until that event is the q key. Keys parse from strings, so "q",
"ctrl+c", and "f1" all just work.
Put the terminal back.
screen.finish()One call. finish() drains pending capability-query replies, resets tracked
modes and the managed area, restores the terminal’s prior state, and consumes the
screen so you cannot use it by accident afterward. Arrange for finish() to run
before your app returns, including from error paths.
Going fullscreen
The inline program above shares the screen with your shell. To take over the
whole terminal instead, the way an editor or a dashboard does, opt into the
alternate screen and hide the cursor right after init():
use uncurses::event::{Event, Key};
use uncurses::screen::Screen;
use uncurses::style::Style;
use uncurses::text::TextSurface;
fn main() -> std::io::Result<()> {
let mut screen = Screen::stdio()?;
screen.init()?;
screen.enter_alt_screen()?;
screen.hide_cursor()?;
screen.set_str((0, 0), "Fullscreen. Press q to quit.", Style::new());
screen.render()?;
let q: Key = "q".parse().unwrap();
loop {
let ev = screen.read_event()?;
screen.observe_event(&ev)?;
if matches!(ev, Event::KeyPress(k) if k == q) {
break;
}
}
screen.finish()
}Two differences from the inline version. First, enter_alt_screen() switches to
the terminal’s alternate buffer, so your drawing does not scroll into the
shell’s history and the original screen comes back untouched on finish().
Second, there is no manual resize: right after init(), the screen is already
sized to the terminal. hide_cursor() just keeps the blinking caret out of your
layout.
Everything else is the same, finish() included. It resets tracked modes, puts
the cursor back, and leaves the alternate screen for you.
Next steps
You have met Screen, the front-door entry point. It is not the only one. The
next page maps out the layers
and when to reach for each.