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
After init(), a 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::screen::Screen;
use uncurses::style::Style;
use uncurses::text::TextSurface;
fn main() -> std::io::Result<()> {
let mut screen = Screen::stdio()?;
screen.init()?; // inline, cursor visible, no alternate screen
// Claim one content row plus one trailing blank row right here at the cursor.
let w = screen.width();
screen.resize((w, 2));
screen.set_str((0, 0), "working...", Style::new());
screen.render()?;
screen.finish()
}There is no enter_alt_screen or hide_cursor here. Those are opt-in,
fullscreen-style choices.
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 is just a resize per keystroke:
let w = screen.width();
screen.resize((w, lines.len() as u16));Keep the width at screen.width() and vary only the height. 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.
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:
show_cursor and hide_cursor decide whether the caret is drawn, while
set_cursor_position only decides where it rests. See
placing the cursor
for the full picture.
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: it writes content into the scrollback above your inline block,
then keeps drawing the block below it.
screen.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 terminal modes, 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.