Search code examples
pythonterminalncursescurses

Python Curses Terminal Resize Issue


What's the correct way to print a line to the bottom of a terminal window that can handle resizing?

import curses
from curses import wrapper

def main(stdscr):
    inp = 0
    y,x = stdscr.getmaxyx()
    stdscr.clear()
    stdscr.nodelay(1)
    while inp != 48 and inp != 27:
        stdscr.addnstr(y-1,0, 'I AM KILL TERMINAL WHEN RESIZE AAAAAAAH', x)
        inp = stdscr.getch()

wrapper(main)

Once I resize the terminal to less columns then the length of the string it tries to wrap onto the next line and errors out. I can't see anything in the documentation about disabling wrapping.

I've tried to update my max y,x values before the addstr function.

while inp != 48 and inp != 27:

        if (y,x) != stdscr.getmaxyx():
            y,x = stdscr.getmaxyx()

        stdscr.addnstr(y-1,0, 'I AM KILL TERMINAL WHEN RESIZE AAAAAAAH', x)
        inp = stdscr.getch()

I've also tried capturing SIGWINCH

while inp != 48 and inp != 27:

        def resize_handler(signum, frame):
            stdscr.erase()
            stdscr.refresh()
            termsize = shutil.get_terminal_size()
            curses.resizeterm(termsize[1],termsize[0])
            y,x = stdscr.getmaxyx()

        signal.signal(signal.SIGWINCH, resize_handler)

        stdscr.addnstr(y-1,0, 'I AM KILL TERMINAL WHEN RESIZE AAAAAAAH', x)
        inp = stdscr.getch()

But neither of these seem to capture the terminal update early enough.


Solution

  • The correct way to handle SIGWINCH for the given example is to wrap the stdscr.addnstr andstdscr.getch calls in a block of code which redraws the text (limiting the number of characters to the existing terminal size), doing this as often as the terminal is resized.

    The problem is that the stdscr.getch call (actually the C function in ncurses doing the work) is interrupted. That does the refresh which is performed in the loop in the first example. ncurses' stdscr.getch should be returning a KEY_RESIZE from the stdscr.getch call, which applications can use to tell when to repaint things. (This works except for OpenBSD which omits the feature for non-technical reasons).

    Establishing a signal handle prevents ncurses from telling the application that the terminal has resized. Reading the second example, it appears that the ncurses library will still be waiting for input, already having done the refresh that puts the addnstr text on the screen (from before the first resize).

    The discussion Curses and resizing windows shows one pitfall: if the application is not going to read characters, it will never see KEY_RESIZE. But your example doesn't do that. I'd discard the signal handler (besides getting in the way, it uses signal-unsafe functions which can break python), and change the first example to something like this:

    import curses
    from curses import wrapper
    
    def main(stdscr):
        inp = 0
        y,x = stdscr.getmaxyx()
        stdscr.clear()
        stdscr.nodelay(1)
        while inp != 48 and inp != 27:
            while True:
                try:
                    stdscr.addnstr(y-1,0, 'I AM KILL TERMINAL WHEN RESIZE AAAAAAAH', x)
                except curses.error:
                    pass
                inp = stdscr.getch()
                if inp != curses.KEY_RESIZE:
                    break
                stdscr.erase()
                y,x = stdscr.getmaxyx()
    
    wrapper(main)