Search code examples
cncursesportabilitycurses

How portable is the use of getch outside curses mode?


I'm working on a terminal program to recognize individual key presses, including keypad keys, but I'd rather not do it in curses/program mode if possible. Rather than reinvent the wheel using terminfo and some sort of mapping or tree structure for fast keypad key matching, I figured I might just leverage curses and use tcgetattr() and tcsetattr() to do what I want outside curses mode while still using curses I/O functions to do the translation of keypad keys for me. Much to my surprise, this works (Linux, ncurses 6.1.20180127):

/**
 * Most error checking elided for brevity.
 */
#include <stdio.h>     // printf
#include <string.h>    // memcpy

#include <curses.h>
#include <termios.h>   // tcgetattr, tcsetattr

int main(void)
{
    struct termios curr, new_shell_mode;
    int c, fd;
    SCREEN *sp;
    FILE *ttyf;

    /*
     * Initialize desired abilities in curses.
     * This unfortunately clears the screen, so
     * a refresh() is required, followed by
     * endwin().
     */
    ttyf = fopen("/dev/tty", "r+b");
    fd = fileno(ttyf);
    sp = newterm(NULL, ttyf, ttyf);
    raw();
    noecho();
    nonl();
    keypad(stdscr, TRUE);
    refresh();
    // Save the current curses mode TTY attributes for later use.
    tcgetattr(fd, &curr);
    endwin();

    /*
     * Set the shell/non-curses mode TTY attributes to
     * match those of program/curses mode (3 attempts).
     */
    memcpy(&new_shell_mode, &curr, sizeof curr);
    for (c = 0; c < 3; c++) {
        tcsetattr(fd, TCSADRAIN, &new_shell_mode);
        tcgetattr(fd, &curr);
        if (0 == memcmp(&new_shell_mode, &curr, sizeof curr))
            break;
    }
    // If new shell mode could fully be set, get a key press.
    if (c != 3)
        c = getch();
    reset_shell_mode();
    delscreen(sp);
    fclose(ttyf);
    printf("%02X\n", c);
    return 0;
}

However, given that I've exited curses mode, is it actually safe/portable to still use getch() in the manner shown?

Or do I need to take the more difficult path of using setupterm() to load the terminfo DB and loop through the strnames array, calling tigetstr() for each, plus set my own termios flags manually and deal with reading the keypress myself?

Nothing in the XSI Curses spec seems to forbid this, provided stdscr remains valid, which seems to be either until the program exits or delwin() is called, I can continue using it, and since stdscr is connected to my ttyf file, which is the terminal, I can use it to get a keypress without resorting to handling everything myself.


Solution

  • You initialized curses using newterm, and it doesn't matter that you called endwin: curses will resume full-screen mode if it does a refresh as a side-effect of calling getch.

    That's not just ncurses, but any curses implementation (except for long-obsolete BSD versions from the 1980s). X/Open Curses notes

    If the current or specified window is not a pad, and it has been moved or modified since the last refresh operation, then it will be refreshed before another character is read.

    In your example, nothing was "moved or modified". But getch checks. (There's probably nothing to be gained by the endwin/termios stuff, since newterm doesn't do a clear-screen until the first refresh).