// lesson: main-loop
The Event Loop, Signals, and Cleanup
Every piece is on the bench. The event loop is the engine block they bolt onto โ and it's small, because you've already built everything hard about it:
enable raw mode; enter the alternate screen;
while (running) {
editor_scroll(e); /* enforce the invariant */
editor_render(e, &frame); /* model -> one buffer */
write_all(STDOUT_FILENO, frame...); /* -> one write */
wait for input (poll, with a timeout);
read; decode key events; dispatch each;
}
leave the alternate screen; restore termios;
Render, wait, dispatch, repeat. vim, less, htop, tmux: this loop. Three topics deserve real attention before you wire it: interruptions, crashes, and where the logic should live.
SIGWINCH and the rules of signal handlers
When the user drags the terminal's corner, the kernel sends your
process SIGWINCH (window change). You must re-query the size
(ioctl(fd, TIOCGWINSZ, &winsize)), resize the editor's viewport,
and repaint. The temptation is to do all that in the signal
handler. Don't โ a signal handler interrupts your program at an
arbitrary instruction. Mid-malloc, mid-printf, mid anything. Call
anything non-reentrant from the handler and you corrupt whatever the
interrupted code was in the middle of; POSIX blesses only a short
list of "async-signal-safe" functions (write is on it; malloc,
printf, and friends are not).
The professional pattern makes the handler trivial and moves the work to the loop:
static volatile sig_atomic_t g_resized = 0;
static void on_winch(int sig) { (void)sig; g_resized = 1; }
/* in the loop, at the top: */
if (g_resized) { g_resized = 0; requery_size(e); /* repaint */ }
volatile sig_atomic_t is the one type C guarantees is safe to
write from a handler and read from normal code. And notice the loop
is already shaped to notice the flag promptly: the signal
interrupts the blocked poll (that EINTR you handled properly in
wait_readable โ this is why), the loop comes around, sees the
flag, repaints at the new size.
Dying well
Your program owns the user's terminal and their unsaved text; it must exit cleanly even when things go wrong.
- Normal exits: leave the alternate screen, show the cursor,
restore termios. Registering the restore with
atexit()gives everyexit()path the cleanup for free. - Errors: the classic
die()helper โ restore terminal first, thenperror+exit(1), so the error message prints onto a sane screen instead of into raw-mode garbage. - Ctrl+C: raw mode disabled ISIG, so it's just a byte (0x03) โ your dispatch decides what it means. (Ours ignores it; Ctrl+Q is quit.)
- Even on
SIGSEGV, a crash-handler thatwrite()s the restore-terminal escape sequence before dying spares the user a wrecked shell (remember:writeis signal-safe; this is the same trick as the resize flag, one rung more desperate).
Dispatch is policy; keep it pure
The last design decision, and the one this challenge grades: what happens when a key arrives? Resist writing it inline in the loop. Make it a function โ
enum ed_action { ED_CONTINUE, ED_QUIT, ED_SAVE, ED_FIND };
enum ed_action editor_handle_key(struct editor *e, struct key_event k);
โ that mutates the editor and reports what the loop should do,
touching no I/O itself. Saving involves a filesystem; quitting
involves the loop's control flow; so the function returns intent
(ED_SAVE, ED_QUIT) and the loop executes it. The payoff is the
one you've collected throughout the course: the entire keyboard
personality of the editor becomes a value-in, value-out function the
tests (and you) can drive without a terminal.
One piece of dispatch state earns its keep: quit confirmation.
Ctrl+Q with unsaved changes shouldn't kill the buffer on the spot;
the convention (kilo's, among others) is press-again-to-confirm:
first Ctrl+Q on a dirty buffer arms quit_pending and continues;
a second consecutive Ctrl+Q quits; any other key disarms it. It's
a two-state state machine โ after the VT parser, barely worth the
name โ but it's the difference between an editor and a data-loss
device.
โบ Key Dispatch
30 ptsThe starter provides a working editing core (line buffer, movement,
all from your earlier challenges โ reference versions included so
this challenge stands alone). You write editor_handle_key:
| key | effect | returns |
|---|---|---|
printable KEY_CHAR (incl. tab) |
insert, dirty = 1 |
ED_CONTINUE |
KEY_ENTER |
split line, dirty = 1 |
ED_CONTINUE |
KEY_BACKSPACE / KEY_DELETE |
the usual, dirty = 1 |
ED_CONTINUE |
| arrows / Home / End / PgUp / PgDn | editor_move |
ED_CONTINUE |
| Ctrl+S | โ | ED_SAVE |
| Ctrl+F | โ | ED_FIND |
| Ctrl+Q | quit-confirm logic below | ED_QUIT / ED_CONTINUE |
| anything else | ignored | ED_CONTINUE |
Quit-confirm: Ctrl+Q returns ED_QUIT immediately if the buffer is
clean or quit_pending is set; otherwise it sets quit_pending = 1 and returns ED_CONTINUE. Every key other than Ctrl+Q
resets quit_pending to 0.
Log in to submit a solution and earn points.