LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   foolproof yet correct handling of keyboard input (https://www.linuxquestions.org/questions/programming-9/foolproof-yet-correct-handling-of-keyboard-input-4175719673/)

jr_bob_dobbs 12-11-2022 05:33 PM

foolproof yet correct handling of keyboard input
 
Some time ago I started a project to write a text editor. The specification I came up with ahead of time was that it would use a reduced subset of the keys of nano and that it would use ncurses.

Window resize events, tab handling, word wrap and all originally specified functions are complete. The editor works and I use it every day. I'm not asking how to write a text editor. Already did it. :)

There is just one nagging problem that I've yet to fix. Keyboard handling is not always correct. Keys from the keypad are not always correct. Backspace is sometimes ignored, or is sometimes interpreted as if it were the delete key. Home and end keys are sometimes ignored. The delete key sometimes is ignored. In an attempt to find consistency, I've tried my editor in the console, in xterm and a few other terminals. I've run it in screen and not in screen. The only consistency I've noted is that my editor, if started in the Linux console or xterm will correctly handle input keys ... unless one is in screen in xterm, detaches, exits xorg and then reattaches in the console.

How I originally wrote keyboard handling, and everything else, was to read the man pages of ncurses and its functions.

I begin to realize that keyboard handling is a bit complex. The kernel has drivers to read the various keyboards (One could in a USB keyboard in addition to a laptop's built-in keyboard). Each terminal takes the key-presses, and then passes them to the editor. Wait, if one is in xorg (or other x-windowing system) then I guess xorg reads keys from the kernel, then passes them to the window manager which in tern passes them to the terminal and then eventually to my editor?

I looked at the source code for nano, to see how it handles keyboard input, and then had to walk away and have a lie-down. :p

Is there some sort of guide or tutorial to correctly, yet in a way that always works, handle key-presses?

GazL 12-11-2022 06:27 PM

A lot of the ncurses info out there on the web is woefully out of date.

I can provide an example of a small demo program I wrote several years ago while trying to get to grips with the 'wide' ncurses library. Perhaps that will give you some ideas.

keypress.h:
Code:

#ifndef KEYPRESS_H
#define KEYPRESS_H

#include <wchar.h>

#define KEYDATASIZE 8  /* Max characters to allow in an input escape sequence */

typedef struct keypress {
    int type;
    wchar_t data[KEYDATASIZE];
} keypress_t;

int get_keypress(keypress_t *k);

/*
 *  If ncurses keypad() mode is enabled and a defined key is received
 *  then k.type will be set to KEY_CODE_YES and k.data[0] will
 *  contain the keycode value. The function will return 1.
 *
 *  If ncurses keypad() mode is not active, or if an unrecognised
 *  escape sequence is received, k.type will be set to OK and
 *  k.data[] will contain a null terminated string of wchars.
 *  The return value will be the number of wchars stored.
 * 
 *  If the escape sequence is longer than KEYDATASIZE - 1
 *  it will be truncated.
*/

#endif /* KEYPRESS_H */

keypress.c:
Code:

/* keypress.c */

#include <wchar.h>
#include <curses.h>

#include "keypress.h"

int get_keypress(keypress_t *k)
{
    wint_t wch = L'\0';
    size_t n = 0;
    int ret = 0;
    int keypad_on = 0;
   
    if ( !k )
        return 0;
   
    for (int i = 0 ; i < KEYDATASIZE ; i++)
        k->data[i] = L'\0';
   
    ret = get_wch(&wch);
    if ( KEY_CODE_YES == ret )
    {
        k->type = ret;
        k->data[n++] = wch;
    }
    else if ( OK != ret )
    {
        k->data[n] = L'\0';
        return n;
    }
    else if ( /* ESC */ L'\033' == wch )
    {
        k->type = ret;
        nodelay(stdscr,TRUE);
        keypad_on = is_keypad(stdscr);
        if ( keypad_on )
            keypad(stdscr, FALSE);
        while ( ret == OK )
        {
            if ( n < KEYDATASIZE - 1 )
                k->data[n++] = wch;
           
            ret = get_wch(&wch);
        }
        nodelay(stdscr, FALSE);
        if ( keypad_on )
            keypad(stdscr,TRUE);
    }
    else
    {
        k->type = ret;
        k->data[n++] = wch;
    }
    k->data[n] = L'\0';
    return n;
}

keypress-example.c:
Code:

/* keypress-example.c */

#include <locale.h>  /* setlocale() */
#include <stdlib.h>  /* setenv(), exit() */
#include <stdio.h>  /* perror() */
#include <string.h>  /* strcmp() */
#include <wchar.h>
#include <wctype.h>  /* iswprint() */
#include <signal.h>  /* sigaction() */
#include <curses.h>

#include "keypress.h"  /* get_keypress() */

static int running = 1;
static int opt_keypad = 0;


void sigterm_handler(int sig)
{
    running = 0;
    return;
}


void update_screen(keypress_t *key)
{
    if ( !key )
        return;

    move(8, 1);   
    if ( KEY_CODE_YES == key->type )
    {
        printw("Keycode octal( %04o )", key->data[0]);
        switch ( key->data[0] )
        {
        case KEY_BACKSPACE:
            printw(": KEY_BACKSPACE ( Backspace )");
            break;
        case KEY_DC:
            printw(": KEY_DC ( Delete Character )");
            break;
        }
    }
    else if ( OK == key->type )
    {
        if ( iswprint(key->data[0]) )
            printw("Unicode( 0x%04x ): '%lc'", key->data[0], key->data[0]);
        else if ( key->data[0] == L'\033' && wcslen(key->data) > 1)
            printw("Escape Sequence: %ls", key->data);
        else
            printw("Non printable/control character( 0x%04x ): %s",
                  key->data[0], key_name(key->data[0]));
    }
    clrtoeol();
    refresh();
       
    return;
}


void draw_screen()
{
    clear();
    attr_on(A_BOLD, NULL);
    mvprintw(1, 1, "NCurses Wide-Input Handling Demonstration");
    attr_off(A_BOLD, NULL);
    if ( opt_keypad )
    {
        mvprintw(3, 1, "Keypad() is enabled.");
        mvprintw(4, 1, "See ");
        attr_on(A_UNDERLINE, NULL);
        printw("/usr/include/ncurses/curses.h");
        attr_off(A_UNDERLINE, NULL);
        printw(" for list of pre-defined keycode symbols.");
    }
    mvhline_set(6, 1, NULL, COLS - 2);
    mvhline_set(10, 1, NULL, COLS - 2);
    mvprintw(12, 1, "Press a key ('q' to exit, 'k' to toggle keypad() mode).");
    refresh();

    return;
}


void display_loop()
{
    keypress_t key;
   
    draw_screen();
    while ( running )
    {
        get_keypress(&key);
        if ( KEY_CODE_YES == key.type && KEY_RESIZE == key.data[0] )
            draw_screen();
        else if ( 0 == wcscmp(key.data, L"q") )
            running = 0;
        else if ( 0 == wcscmp(key.data, L"k") ) {
            opt_keypad = ~opt_keypad;
            keypad(stdscr, opt_keypad);
            draw_screen();
        } else
            update_screen(&key);
    }

    return;
}


int main(int argc, char *argv[])
{
   
    struct sigaction sa;   

    if ( !setlocale(LC_ALL, "") )
    {
        perror("setlocale");
        exit(EXIT_FAILURE);
    }

   
    /* PARSE ARGS */

    for ( int i = 1 ; i < argc ; i++ )
    {
        if ( strcmp("--keypad", argv[i]) == 0 )
            opt_keypad = 1;
    }

   
    /* Setup a signal handler for SIGINT/SIGTERM so that we can exit
      gracefully rather than leaving the screen in a mess. */
    sa.sa_handler = sigterm_handler; 
    if ( -1 == sigaction(SIGTERM, &sa, NULL) )
        perror("sigaction: ");
    if ( -1 == sigaction(SIGINT, &sa, NULL) )
        perror("sigaction: ");


    /* NCURSES INITIALISATION */
   
    setenv("ESCDELAY","200", FALSE);  /* improve keypad() responsiveness. */
   
    initscr();
    scrollok(stdscr, TRUE);
    if ( opt_keypad )
        keypad(stdscr, TRUE);
    cbreak();
    noecho();
    curs_set(0);
   

    /* MAIN LOOP */

    display_loop();
   

    /* NCURSES CLEANUP */

    clear();
    refresh();
    endwin();

    return EXIT_SUCCESS;
}

Makefile:
Code:

# Makefile for keypress: An ncurses input demonstration

CFLAGS = -Wall -O2
CPPFLAGS =
NCFLAGS := $(shell pkg-config --cflags ncursesw)
LDLIBS := $(shell pkg-config --libs ncursesw)

PREFIX=/usr/local


keypress-example: keypress-example.o keypress.o
        $(CC) -o $@ $^ $(LDLIBS)

%.o: %.c
        $(CC) -c -o $@ $(CFLAGS) $(CPPFLAGS) $(NCFLAGS) $<

keypress-example.o: keypress-example.c keypress.h
keypress.o: keypress.c keypress.h

all: clean keypress-example

clean:
        rm -f  *.o

distclean: clean
        rm -f keypress-example \#*\# *~ TAGS

install: keypress-example
        mkdir -p $(DESTDIR)$(PREFIX)/bin
        cp keypress-example $(DESTDIR)$(PREFIX)/bin

tags:
        etags *.c *.h

edit: tags
        emacs *.c *.h Makefile &

.PHONY: clean distclean all install tags edit

################################################################# End. #

Unfortunately and as you've already discovered, due to the nature of terminal input and the differences between terminals, it is a somewhat messy topic.

My advice would be to always use the ncursesw "wide" library and get_wch(), and to use keypad(, TRUE) mode to make life easier.

jr_bob_dobbs 12-12-2022 05:10 PM

That was an interesting program. At first it refused to compile but adding
Code:

#ifndef keypad
  #include <curses.h>
#endif

near the top of the keypress.h file fixed it.

It's revealed what I already suspected: each term has different ideas as to what many keys are. Thank you. I will read up on the wide thing and see how that goes.

EdGr 12-12-2022 05:26 PM

I think that you asked for this problem by using ncurses when you didn't actually want a terminal emulator.

If you write a GUI program instead, you will receive input events exactly as X generated them. No terminals involved. I recommend GTK 3. It passes the keypress events as-is.
Ed

GazL 12-12-2022 05:35 PM

That's weird. It compiles fine as is on both my CRUX and Slackware boxes.
There's nothing curses specific in keypress.h that would need the include, so I'm not sure what that is all about.

This is what the make output looks like on my system:
Code:

$ make
cc -c -o keypress-example.o -Wall -O2  -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=600  keypress-example.c
cc -c -o keypress.o -Wall -O2  -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=600  keypress.c
cc -o keypress-example keypress-example.o keypress.o -lncursesw


And, yes, most the terminals tend to do their own thing. That's where keypad mode helps a little, though it does rely on the $TERM and terminfo entries being correct, which isn't always the case.

To be honest, it's probably best to forget that the function keys exist and just stick to the normal keys and cursor movement.

P.S. I've not encountered an escape sequence longer than 7 yet, but there might be some out there, so perhaps giving KEYDATASIZE a little more headroom might not be a bad idea.

jr_bob_dobbs 12-20-2022 05:27 PM

Quote:

Originally Posted by EdGr (Post 6397501)
I think that you asked for this problem by using ncurses when you didn't actually want a terminal emulator.

Request for clarification: I wrote a text editor, not a terminal emulator.
I had to use ncurses, since my editor runs in the console as well, when x-windows is not running.

Quote:

If you write a GUI program instead, you will receive input events exactly as X generated them.
If that were true, Qemu wouldn't be eating colon keys ;) But this gets off-topic.

An update. I've tested nano on more terminals and ... wow, things go wonky for nano as well. So for now I've been using Gazl's most excellent program to suss out the key codes for the terminals that I *do* use and, like nano apparently does, will ignore other terminals. I figure if my editor works in the console, xterm and (to be fixed still) the OpenBSD console I'll stop there.

EdGr 12-20-2022 05:50 PM

Quote:

Originally Posted by jr_bob_dobbs (Post 6399235)
Request for clarification: I wrote a text editor, not a terminal emulator.
I had to use ncurses, since my editor runs in the console as well, when x-windows is not running.

Yes, the downside of writing a GUI program is that X is required.

Your text editor lives higher in the software stack than the terminal emulation being done by ncurses, VTE, and the console. Your program is seeing the result. There may being a way to read the keyboard directly, but I have not tried to do that.
Ed

GazL 12-21-2022 04:24 AM

Just being pedantic, but ncurses' goal is "terminal abstraction" not "terminal emulation".

hazel 12-21-2022 05:32 AM

I wonder if this is related to the need to patch kbd when building LFS to get consistent handling of backspace and delete.
Quote:

After patching, the backspace key generates the character with code 127, and the delete key generates a well-known escape sequence.

NevemTeve 12-21-2022 06:07 AM

This BackSpace/Delete problem is only 50+ years old, so there is no point in being impatient.
My terminfo database says:
Code:

infocmp linux | egrep 'kbs|kdch1'
        kb2=\E[G, kbs=^?, kcbt=\E[Z, kcub1=\E[D, kcud1=\E[B,
        kcuf1=\E[C, kcuu1=\E[A, kdch1=\E[3~, kend=\E[4~,

That's quite okay. Now the problem is with xterm: original terminfo database says kbs=\b (or kbs=^H), linux-patched terminfo-database says kbs=\177 (or kbs=^?)

GazL 12-21-2022 06:30 AM

For backspace/delete to do the right thing, 4 things must agree.
  1. the sequence the terminal/emulator generates when the key is pressed.
  2. the erase value of the tty/pty. (check with: stty -a | grep --color '\berase =')
  3. the kbs= value of the terminfo. (check with: infocmp | grep --color '\bkbs='
  4. the program must either use ncurses keypad(, TRUE), or query the stty erase value before attempting to parse any keyboard input.
Traditionally xterm has used BS=^H. Some distro have been known to patch this, and that causes interoperability issues when sshing to hosts whose terminfo entry for xterm may still specify the traditional value: ^H.

Most other terminal emulators follow the linux console and use BS=DEL. I can't remember what the OpenBSD console uses.


P.S. we can blame emacs for all this BS=DEL nonsense!

jr_bob_dobbs 12-23-2022 12:41 PM

Thank you all for the replies. So much information!

p.s. I still remember back in the day, using CP/M and memorizing that ^h (8) was backspace and 127 was delete. The keyboard had a backspace key and that generated a ^h. I'd probably memorized half the ascii table back then. :D Right, let me stop rambling. Thanks again. :)


All times are GMT -5. The time now is 04:57 AM.