// lesson: scrolling-region
Scrolling and Scrollback
Print a line at the bottom of a full terminal and everything glides up one row. It's so familiar it doesn't look like a feature โ but someone has to implement it, and in your emulator that someone is you.
What scrolling actually is
When a linefeed happens on the last row, the terminal doesn't move the cursor down (there's no down); it moves the content up:
- Row 0 disappears (hold that thought).
- Rows 1..Nโ1 shift up by one.
- The bottom row becomes blank.
- The cursor stays exactly where it was โ bottom row, same column.
With a flat rows ร cols grid, the shift is one honest memmove:
memmove(&cells[0], /* dst: row 0 */
&cells[cols], /* src: row 1 */
(size_t)(rows - 1) * cols * sizeof(cell));
/* then blank the last row */
memmove, not memcpy โ the regions overlap, and overlapping
memcpy is undefined behavior that works right up until the day the
optimizer vectorizes it differently. (Real emulators dodge the copy
entirely by keeping a ring of row pointers and just rotating the
"which row is row 0" index. A worthwhile optimization exactly when
profiling says so โ cat on a large file scrolls thousands of times
per second โ and a distraction before that. We'll do the memmove.)
Scrollback: where row 0 goes to live
That disappearing top row is the terminal's other beloved feature:
scrollback โ the history you reach with Shift+PgUp. The screen
grid stays exactly rows ร cols; scrolled-off lines move to a
separate buffer that only ever grows at one end and gets trimmed at
the other. First-in, first-out, bounded: a ring buffer.
Fix a capacity of cap lines and keep three numbers alongside the
storage:
head โ the slot the NEXT line will be written into
len โ how many lines are currently stored (saturates at cap)
Writing line after line: slot head, then head = (head+1) % cap,
and len climbs until it hits cap and stays there โ at which
point each new line silently lands on the slot of the oldest one.
That's the whole eviction policy. No shifting, no allocation per
line, O(1) always.
Reading is where rings earn their reputation for off-by-one bugs, so
derive it once, carefully. "The n-th most recent line" (n = 0 is the
newest) was written n+1 writes before the next write, i.e. at slot
(head - 1 - n + cap) % cap for n in [0, len)
The + cap guards C's % against negative operands (in C, -1 % 5
is -1, not 4 โ a fact that has ruined many evenings).
One more design note: notice the screen grid and the scrollback
never share memory. When a real emulator shows scrollback, it
renders from history while the grid keeps living underneath โ
which is also why full-screen apps switch to the alternate
screen (ESC[?1049h): the alt screen has no scrollback, so vim
scrolling a file doesn't shred your shell history. Two grids, one
history buffer, sharp boundaries.
โบ Scroll with Scrollback
25 ptsA character-grid terminal (styles omitted to keep the focus on motion) with three operations:
tg_scroll_up(t)โ push the top row into the scrollback ring (evicting the oldest line if full), shift the grid up, blank the bottom row. Cursor unchanged.tg_linefeed(t)โ LF semantics on a scrolling screen: if the cursor is above the last row, move down one (column unchanged); on the last row,tg_scroll_up(cursor stays put, column unchanged).tg_scrollback_get(t, n, out)โ copy the n-th most recent scrolled line (n = 0 newest) intooutas a NUL-terminated string of exactlycolscharacters. Return 0, or -1 ifnis out of range.
The starter provides init/free and a put-string helper; the tests scroll far past capacity and read history back in exact order.
Log in to submit a solution and earn points.