// lesson: window-size-and-signals

How Big Is the Screen?

build_frame needs screen_rows and screen_cols. The terminal knows; the question is how to ask. There are two answers, and a robust editor implements both.

The ioctl, and the escape-sequence fallback

The kernel tracks each tty's dimensions and hands them over through an ioctl:

#include <sys/ioctl.h>

winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
    rows = ws.ws_row;
    cols = ws.ws_col;
}

TIOCGWINSZ โ€” TIOCtl Get WINdow SiZe. It can fail (some serial links, some odd environments report 0ร—0), and the traditional fallback is a lovely hack built from two escape sequences: send ESC [ 999 ; 999 H (cursor addressing clamps at the screen edge, so the cursor lands at the true bottom-right), then send ESC [ 6 n โ€” DSR, Device Status Report, argument 6: "report the active cursor position". The terminal writes back onto your stdin a Cursor Position Report:

ESC [ 24 ; 80 R        โ† the terminal typed this into your input

Parse the two numbers and you have the screen size. Note what just happened: the input stream your key decoder reads can also contain replies from the terminal. (That's also how bracketed-paste markers arrive, and why decoders that choke on unexpected CSI input are a liability โ€” your Lesson 2 decoder already swallows unknown sequences.)

SIGWINCH: the size changes under you

When the user drags the terminal window, every process in its foreground process group receives SIGWINCH (WINdow CHange โ€” one of the few signals that's an information broadcast, not a demand). The handler discipline from Lesson 1 applies doubly here: a signal handler can fire between any two instructions, so it must only do things that are async-signal-safe. The standard pattern is a flag:

volatile sig_atomic_t g_resized = 0;
void on_winch(int) { g_resized = 1; }   // the entire handler
...
// in the main loop, between keystrokes:
if (g_resized) { g_resized = 0; requery_size(); clamp_and_redraw(); }

volatile sig_atomic_t is the only type the C and C++ standards bless for this. Conveniently, a pending read() returns โˆ’1/EINTR when a signal arrives (install the handler without SA_RESTART), so a resize also wakes your input loop immediately instead of waiting for the next keystroke. After re-querying the size, the editor must clamp: the cursor and scroll offsets that were valid at 80ร—24 may be far outside a 40ร—12 window โ€” that clamping function is exactly the viewport logic you build in a later lesson.

The graded piece of this lesson is the parser for the cursor position report, which is subtler than it looks: it reads attacker-adjacent input (any program can write bytes to your terminal), so it must reject garbage rather than parse it optimistically. std::optional is the right return type โ€” "a position, or nothing" โ€” making the failure case impossible to ignore silently, unlike an int* out-param or a magic โˆ’1ร—โˆ’1.

โ€บ Parse the Cursor Position Report

10 pts

Implement parse_cursor_report(s) returning std::optional<std::pair<int, int>> โ€” {rows, cols} โ€” for a complete, exact report:

  • The string must be exactly ESC [ <digits> ; <digits> R โ€” nothing before, nothing after.
  • Both numbers must be 1โ€“4 digits (no terminal is 10,000 columns wide; the cap also sidesteps integer overflow on hostile input) and โ‰ฅ 1.
  • Anything else โ€” missing prefix, missing ;, empty digits, trailing bytes, a lowercase r โ€” returns std::nullopt.

Log in to submit a solution and earn points.