Inline rendering
Most fullscreen apps take over the terminal with the alternate screen. Many tools, prompts, progress bars, pickers, and REPLs want to draw a live region right where the cursor is and leave the scrollback intact. That is inline mode, and it is the uncurses default.
The idea
A Program owns the terminal session, input, and terminal modes. Its Screen
is the renderer you reach with program.screen_mut(). After init(), that
screen starts inline with the cursor visible: it owns a block of rows starting at
the cursor and draws there, in the normal buffer. You decide how many rows it
owns with resize, and you can grow or shrink that block as your content
changes. Nothing scrolls into history unless you ask for it.
use uncurses::buffer::Bounded;
use uncurses::program::Program;
use uncurses::style::Style;
use uncurses::text::TextSurface;
fn main() -> std::io::Result<()> {
let mut program = Program::stdio()?;
program.init()?; // inline, cursor visible, no alternate screen
// Claim one content row plus one trailing blank row right here at the cursor.
let width = program.screen().width();
let screen = program.screen_mut();
screen.resize((width, 2));
screen.set_str((0, 0), "working...", Style::new());
screen.render()?;
program.finish()
}Inline mode leaves the alternate screen and the cursor as they are.
enter_alt_screen and hide_cursor are opt-in, fullscreen-style choices.
Fullscreen is a render property
Inline versus fullscreen is the screen’s fullscreen render property.
Inline means relative addressing in a band in the normal buffer. Fullscreen means
absolute addressing over the whole viewport, which is what you want after
switching to the alternate screen buffer.
Use program.enter_alt_screen() and program.exit_alt_screen() to switch
buffers. They emit DECSET 1049 or DECRST 1049, flush, and set the screen’s
fullscreen property to match. Calling program.screen_mut().set_fullscreen(..)
directly only retargets how frames are addressed. It emits no terminal mode and
switches no buffers, so use it only when you are driving the terminal bytes
yourself.
Growing the region
Inline regions are not fixed. Call resize again whenever your content changes
height, and the renderer reflows the block in place. A prompt that grows as the
user types resizes as the line count changes: keep the width at
program.screen().width() and vary only the height.
resize re-establishes the region, so the next render repaints it. Call it when
the height actually changes rather than on every keystroke:
let screen = program.screen_mut();
let height = lines.len() as u16;
if screen.height() != height {
let width = screen.width();
screen.resize((width, height));
}To follow the terminal instead of your own content, use
autoresize:
it reads the size the operating system already knows, keeps your inline height,
and skips reports that leave the cell grid unchanged.
The renderer keeps the block anchored in the normal buffer, so growing from two rows to ten expands the managed area instead of taking over the whole terminal.
Placing the caret
An inline prompt keeps the cursor visible, and you usually want it sitting on the
character the user is editing, not wherever the last cell write happened to land.
Stage it with set_cursor_position and every render parks the cursor there at
the end of the frame. Drawing and cursor coordinates are (x, y), zero-based;
terminal row and column reports are one-based.
let screen = program.screen_mut();
screen.set_str((0, 0), &line, Style::new());
screen.set_cursor_position((caret_col, 0));
screen.render()?;It is sticky, so set it when the caret moves and later frames keep the cursor
there on their own. Call clear_cursor_position to stop steering it. Visibility
stays separate: program.show_cursor() and program.hide_cursor() decide
whether the caret is drawn, while set_cursor_position only decides where it
rests.
Committing to scrollback
Sometimes you want a line to leave the live region and become permanent history,
the way a shell prints a command’s output above the next prompt. That is
insert_above on Screen: it writes content into the scrollback above your
inline block, then keeps drawing the block below it.
program.screen_mut().insert_above("compiled in 1.2s")?;It flushes immediately and leaves your live block untouched, so the committed line lands in scrollback and the block keeps drawing right below it.
A multiline prompt that commits its buffer on Ctrl-D does exactly this: render
the editable block inline, and on commit push the finished text up with
insert_above and clear the block for the next entry.
flowchart TB
scroll["scrollback (committed lines via insert_above)"]
scroll --> live["live inline block (resized as it grows)"]
live --> prompt["cursor / next input"]
Teardown
Finishing an inline session resets the modes the Program emitted, leaves the
last frame in place, and returns the cursor to the shell.
See the inline_input example for a complete multiline inline prompt with
editing, paste, and insert_above commits.