Search code examples
ncursespython-curses

Where does curses inject KEY_RESIZE on endwin and refresh


Run this Python code and resize window. It will get a KEY_RESIZE and exit.

import curses
import signal

stdscr = None

def handler(num, _):
    curses.endwin()
    stdscr.refresh()

stdscr = curses.initscr()
curses.cbreak()
stdscr.keypad(1)
stdscr.refresh()
signal.signal(signal.SIGWINCH, handler)
while True:
    ch = stdscr.getch()
    if ch == curses.KEY_RESIZE: break
curses.endwin()

Where is this KEY_RESIZE injected?


I also tested with C code:

#include <ncurses.h>
#include <signal.h>

WINDOW *stdscr = NULL;

void handler(int num) {
    endwin();
    wrefresh(stdscr);
}

int main()
{
    stdscr = initscr();
    cbreak();
    keypad(stdscr, 1);
    wrefresh(stdscr);
    signal(SIGWINCH, handler);
    while (1) {
        int ch = wgetch(stdscr);
        if (ch == KEY_RESIZE) break;
    }
    endwin();
    return 0;
}

Run it and resize it, then press a key, it will get a KEY_RESIZE exit. Why do we have to press a key to get a KEY_RESIZE in C code, which is not necessary in Python code?


Solution

  • That's by design... since curses doesn't have anything like an event-loop, it has to tell the application that a SIGWINCH was detected, and that the curses library has updated its data structures to work with the new terminal size. (Having a signal handler in the application that an application could use to tell the curses library would not work better, and having the curses library do this is simpler, anyway).

    The initscr and getch manual pages mention this SIGWINCH feature.

    As for "where" it does this: that's in _nc_update_screensize, which checks a flag set by the signal handler, and is called from several places including doupdate (which refresh and getch call). That will inject a KEY_RESIZE whether or not there was actually a SIGWINCH, if the screensize has changed.

    Now... it's possible to have signal handlers chain, by calling the original handler from newly-established handlers. (Calling signal in a C program returns the current handler's address). ncurses will only add its handler at initialization time, so one (unlikely) possibility is that the python code is perhaps reusing the underlying handler when it adds its own.

    However, there's a larger problem in the examples: they are making curses calls in the signal handlers. That's unsafe in C (the reason why ncurses' signal handlers only set a flag). Perhaps in python those handlers are wrapped -- or just due to timing you're getting unexpected behavior.