// 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 ptsImplement 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 lowercaserโ returnsstd::nullopt.
Log in to submit a solution and earn points.