libghostty-vt 0.0.0
Loading...
Searching...
No Matches
Scrollback

Scrollback is the history that has scrolled off the top of the screen, kept so you can scroll back and read it.

It's also where a terminal's memory goes. Most of what a terminal holds is its screen contents and scrollback, and a program can write forever. So you need to decide how much history to keep, and if you're running a lot of terminals, what to do about the memory it takes.

The default limit is small, so nearly every terminal wants to set its own. The scrollbar and the compression sections are only for those who need them. Most programs never need compression.

Limits

Scrollback has two limits, and both are set with ghostty_terminal_set(). If you set both, the first one reached is used.

Option Default SIZE_MAX NULL
GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES 10,000 bytes No limit Restores the default
GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES No limit No limit No limit
// Keep up to 10 MB of history.
size_t max_bytes = 10 * 1024 * 1024;
&max_bytes);
GHOSTTY_API GhosttyResult ghostty_terminal_set(GhosttyTerminal terminal, GhosttyTerminalOption option, const void *value)
@ GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_BYTES
Definition terminal.h:2339

They're useful together, because each one depends on things you don't control. The byte limit is reached with fewer lines if the program uses more styles, and the line limit is reached sooner with a narrower terminal.

Both limits are estimates. History lives in pages, the smallest unit of grid space we allocate, and we only prune whole pages. A page is about 400 KB at the time of writing, so the byte limit is accurate to about that much, and the default of 10,000 bytes is one page in practice. The line limit almost always keeps more lines than you asked for, usually somewhere from dozens to a hundred or so.

Lowering a limit removes eligible complete pages right away. A byte limit of zero disables scrollback and erases the history you have.

Read the limits back with GHOSTTY_TERMINAL_DATA_SCROLLBACK_MAX_BYTES and GHOSTTY_TERMINAL_DATA_SCROLLBACK_MAX_LINES. They return GHOSTTY_NO_VALUE when the limit is unlimited, and they always report the primary screen's value, even while the alternate screen is active.

Scrolling the viewport

The viewport is the part of the terminal you're showing. Move it with ghostty_terminal_scroll_viewport(), which takes a tagged union:

Tag Value field What it does
GHOSTTY_TERMINAL_SCROLL_VIEWPORT_TOP Scroll to the top of the scrollback
GHOSTTY_TERMINAL_SCROLL_VIEWPORT_BOTTOM Scroll to the bottom (the active area)
GHOSTTY_TERMINAL_SCROLL_VIEWPORT_DELTA delta Scroll by a number of rows, negative for up
GHOSTTY_TERMINAL_SCROLL_VIEWPORT_ROW row Scroll so that this row is the first one visible
GHOSTTY_TERMINAL_SCROLL_VIEWPORT_DELTA_PROMPT delta_prompt Scroll by a number of prompts, negative for backward
.value = { .delta = -5 },
};
ghostty_terminal_scroll_viewport(terminal, behavior); // up 5 rows
GHOSTTY_API GhosttyResult ghostty_terminal_scroll_viewport(GhosttyTerminal terminal, GhosttyTerminalScrollViewport behavior)
@ GHOSTTY_TERMINAL_SCROLL_VIEWPORT_DELTA
Definition terminal.h:107

Row 0 is the top of the scrollback. The row is clamped so the viewport never scrolls past the top of the active area. With no scrollback, such as on the alternate screen, the viewport stays on the active area.

Scrolling by prompts needs semantic prompt markers from the shell. Without a matching prompt it does nothing.

GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE tells you if the viewport is following the active area, or if someone has scrolled up into history.

The scrollbar

Read GHOSTTY_TERMINAL_DATA_SCROLLBAR to get a GhosttyTerminalScrollbar, which has the three numbers you need to draw one: total rows in the scrollable area, the offset of the viewport into them, and the len of the visible area.

There's no notification when these change, and that's on purpose. Poll once per frame or once per write batch and compare the result with the last one. This is what Ghostty's own renderer does. It's cheap, because we keep the total up to date as the terminal changes and cache the offset. The one exception is the first read after the viewport moves somewhere that isn't an absolute row, which can cost O(pages).

The offset is the same row space as GHOSTTY_TERMINAL_SCROLL_VIEWPORT_ROW, so when someone drags your scrollbar you can scroll straight to the position you drew.

// Someone dragged the scrollbar halfway down.
.value = { .row = (size_t)((bar.total - bar.len) / 2) },
};
GHOSTTY_API GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal, GhosttyTerminalData data, void *out)
@ GHOSTTY_TERMINAL_SCROLL_VIEWPORT_ROW
Definition terminal.h:122
@ GHOSTTY_TERMINAL_DATA_SCROLLBAR
Definition terminal.h:2734

If you only want counts, GHOSTTY_TERMINAL_DATA_TOTAL_ROWS is the rows in the active screen including scrollback, and GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS is the rows in scrollback.

Resizing

By default, a resize can pull rows out of scrollback and back into the active area. Growing the rows reveals scrollback if the cursor is on the bottom row, and a column reflow that needs fewer rows reveals it too.

Most programs want this and can skip this section. But if your pty keeps its own screen buffer without scrollback, it can't pull rows back, and it will disagree with the terminal about the screen after a resize. Windows ConPTY is the case that needs this. Set GHOSTTY_TERMINAL_OPT_RESIZE_PULL_SCROLLBACK to false:

bool pull_scrollback = false;
&pull_scrollback);
@ GHOSTTY_TERMINAL_OPT_RESIZE_PULL_SCROLLBACK
Definition terminal.h:2548

Now growing the rows always appends blank rows at the bottom, and a column reflow keeps the top of the active area on the same content. A line that's fully in scrollback stays there, but a soft-wrapped line with at least one row still in the active area can still unwrap back into view. The setting survives a full reset (RIS), and passing NULL restores the default of true.

Memory use

Read GHOSTTY_TERMINAL_DATA_MEMORY_USAGE to see how much memory a terminal holds. This is for applications that host many terminals and have to stay inside a budget, for example by compressing or closing whichever ones hold the most.

GhosttyTerminalMemoryUsage is a sized struct, so initialize it with GHOSTTY_INIT_SIZED(). If size is too small, the read returns GHOSTTY_INVALID_VALUE and leaves the struct alone. The read never decompresses scrollback, but it does look at every page, so don't do it after every write.

Budget against the resident bytes. That's the physical memory the pages use right now. Virtual bytes are the address space reserved for pages.

Each screen has its own fields, with a primary_ or alternate_ prefix, so add them together for the terminal's total. The primary screen holds shell output and all of the scrollback. The alternate screen is used by full-screen programs such as text editors, and its fields are zero until a program first switches to it.

Colors, styles and hyperlinks are stored in pages, so they're already in the page figures. Images are stored separately and have their own fields. Small things outside of pages, such as the window title and internal bookkeeping, aren't counted. They're small next to the pages once a terminal has any scrollback.

On macOS, the system takes back memory freed by compression lazily, when something else needs it. Until then, the memory the system reports for your process (its RSS) can be higher than the resident figures here.

// Return the bytes of memory a terminal currently holds. A server hosting
// many terminals can compare this figure across terminals to decide which
// one to compress or shut down first.
static uint64_t terminal_resident_bytes(GhosttyTerminal terminal) {
// GHOSTTY_INIT_SIZED sets the size field, which the call requires.
terminal,
&usage);
assert(result == GHOSTTY_SUCCESS);
// Count both screens and any images. The alternate screen reports zero
// until a program such as a full-screen editor first switches to it.
}

Full program: c-vt-compression/src/main.c

Compression

Old scrollback is mostly sitting there, so we can compress it. This changes how the history is stored and nothing else. The contents and the scrollback limit stay the same, and reading compressed history restores it transparently.

You drive it. libghostty-vt doesn't create a timer or a background thread, so nothing is compressed until you ask. The shape is an idle timer: restart it when the terminal changes, and when it fires, compress a step at a time until there's nothing left to do.

ghostty_terminal_compression_activity() returns an opaque token. Only compare it for equality, because it can wrap, and a change in either direction means the same thing. It changes only when compression-relevant state changes, so restart your idle timer when it does.

uint64_t compression_activity;
terminal,
&compression_activity);
assert(result == GHOSTTY_SUCCESS);
// Terminal mutations may change the token. When it changes, restart the
// application's idle timer rather than compressing on the output path.
const char *line = "repeated and compressible terminal history\r\n";
for (size_t i = 0; i < 4000; i++) {
terminal,
(const uint8_t *)line,
strlen(line));
}
uint64_t new_activity;
result = ghostty_terminal_compression_activity(terminal, &new_activity);
assert(result == GHOSTTY_SUCCESS);
if (new_activity != compression_activity) {
compression_activity = new_activity;
// Restart the application's compression idle timer here.
}

Full program: c-vt-compression/src/main.c

When the timer fires, call ghostty_terminal_compress() with GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL, which does a bounded amount of work. There are three results:

// Perform one step after the application's idle timer fires. Returning true
// asks the application to schedule another step while the terminal is idle.
static bool compression_idle_step(GhosttyTerminal terminal) {
terminal,
&compression_result);
assert(result == GHOSTTY_SUCCESS);
switch (compression_result) {
return true;
return false;
default:
assert(false);
return false;
}
}

Full program: c-vt-compression/src/main.c

Complete means the pass finished, not that every page was compressed. Compression is opportunistic. A page can be unprofitable to compress, or hit an allocation or reclamation failure.

GHOSTTY_TERMINAL_COMPRESSION_MODE_FULL does one synchronous scan of every eligible page instead. It can stall on large scrollback.

ghostty_terminal_compress() isn't thread-safe with anything else on the same terminal, so serialize it with your writes, rendering, searches and other terminal access.

Compression lowers the resident figure but not the virtual one, because each page's space stays reserved for decompression.