// 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:

  1. Row 0 disappears (hold that thought).
  2. Rows 1..Nโˆ’1 shift up by one.
  3. The bottom row becomes blank.
  4. 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 pts

A 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) into out as a NUL-terminated string of exactly cols characters. Return 0, or -1 if n is 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.