c++linux

How can I detect if multiple keyboard keys are being pressed at the same time in C++, in Linux, from the terminal?


I have some code which currently checks if the character "b" is being pressed:

#include <iostream>
#include <termios.h>
#include <unistd.h>

using namespace std;

char getKeyPress() {
    struct termios oldt, newt;
    char ch;

    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
    ch = getchar();
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    return ch;
}

int main() {
    cout << "Press 'b' to see if it's detected. Press 'q' to quit." << endl;

    while (true) {
        char key = getKeyPress();

        if (key == 'b') {
            cout << "'b' is currently pressed." << endl;
        }

        if (key == 'q') {
            cout << "Exiting..." << endl;
            break;
        }
    }

    return 0;
}

However in my real code base, I need to check for multiple characters being pressed. How could I extend this code to check if "b" or "n" are being pressed, and if both "b" and "n" are being pressed at the same time?


Solution

  • You need to change the terminal mode to raw or medium-raw whitch than sends mousedown-mouseup events, and then convert the events back to letters/symbols/keys

    1. header files (I might've missed some or written to much see: header file more than enough of them)
        #include <sys/ioctl.h>
        #include <unistd.h>
        #include <termios.h>
        #include <stdio.h>
        #include <linux/kd.h>
        #include <linux/keyboard.h>
        #include <stdlib.h>
        #include <fcntl.h>
        #include <errno.h>
    
    1. function to get the file descriptor of the current console
    static const char *conspath[] = {
        "/proc/self/fd/0",
        "/dev/tty",
        "/dev/tty0",
        "/dev/vc/0",
        "/dev/systty",
        "/dev/console",
        NULL
    };
    
    /*
     * getfd.c
     *
     * Get an fd for use with kbd/console ioctls.
     * We try several things because opening /dev/console will fail
     * if someone else used X (which does a chown on /dev/console).
     */
    
    static int
    is_a_console(int fd)
    {
        char arg;
    
        arg = 0;
        return (isatty(fd) && ioctl(fd, KDGKBTYPE, &arg) == 0 && ((arg == KB_101) || (arg == KB_84)));
    }
    
    static int
    open_a_console(const char *fnam)
    {
        int fd;
    
        /*
         * For setkbdmode we need write permissions
         * and for getkbdmode we need read permissions
         * so open with READ-WRITE
         */
        fd = open(fnam, O_RDWR);
        if (fd < 0)
            return -1;
        return fd;
    }
    
    int
    getfd(const char *fnam)
    {
        int fd, i;
    
        if (fnam) {
            if ((fd = open_a_console(fnam)) >= 0) {
                if (is_a_console(fd))
                    return fd;
                close(fd);
            }
            return -1;
        }
    
        for (i = 0; conspath[i]; i++) {
            if ((fd = open_a_console(conspath[i])) >= 0) {
                if (is_a_console(fd))
                    return fd;
                close(fd);
            }
        }
    
        for (fd = 0; fd < 3; fd++)
            if (is_a_console(fd))
                return fd;
    
        return -1;
    } 
    
    1. Init: store old KBD_MODE and set it to K_MEDUIMRAW, than save old termios mode change the it to unbuffered input (not waiting for enter)
    int fd = getfd(NULL); // might need root permissions or sid bit in premissions (sudo chown root ./prog && sudo chmod u+s ./prog)
    if (!fd) exit 0x12;
    int old_kbdmode;
    if (ioctl(fd, KDGKBMODE, &old_kbdmode)) {
       throw("ioctl KDGKBMODE error");
    }
    
    int mode = K_MEDIUMRAW;
    if ((err = ioctl(fd, KDSKBMODE, mode))) {
    //* note - this might not work on ubuntu in a shared object file.
    //* In a case like this just create a setkbdmode binary file
    //* and use it from the dll
      close(fd);
      printf("ioctl KDSKBMODE error %d\n", errno);
      return -1;
    }
    
    termios old_termios;
    tcgetattr(STDIN_FILENO,&old_termios);
    termios term_ios = old_termios;
    term_ios.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &term_ios);
    
    tcgetattr(fd,&old_fdterm);
    term_ios = old_fdterm;
    term_ios.c_lflag &= ~(ICANON | ECHO | ISIG);
    term_ios.c_iflag = 0;
    term_ios.c_cc[VMIN] = 0xff;
    term_ios.c_cc[VTIME] = 1;
    tcsetattr(fd, TCSANOW, &term_ios);
    
    1. get keyboard layout mapping to convert keycodes to letters/numbers/symblols/function keys

    3.1(optional but usefull): define an enum class with keys for easeier usage: At the bottom of this file (just copy it)

    3.2

    // if you skipped 3.1 use [unsigned short] instead of [enum Key]
    enum Key key_chart[MAX_NR_KEYMAPS][KEYBOARD_MAX]; // probably [255][255]
    for (auto t = 0; t < MAX_NR_KEYMAPS; t++) {
        if (t > UCHAR_MAX) {
           exit(18);
        }
        for (auto i = 0; i < NR_KEYS; i++) {
           if (i > UCHAR_MAX) {
               exit(18);
           }
    
           struct kbentry ke;
           ke.kb_table = (unsigned char) t;
           ke.kb_index = (unsigned char) i;
           ke.kb_value = 0;
    
           if (ioctl(fd, KDGKBENT, (unsigned long)&ke)) {
              exit(19);
           }
    
           if (!i && ke.kb_value == K_NOSUCHMAP)
              break;
                        
           if (KTYP(ke.kb_value) == KT_LETTER) // weird kernel thing to show whitch writable characters can be acted by a capslock (just ignore it)
              ke.kb_value = K(KT_LATIN, KVAL(ke.kb_value));
    
           if (ke.kb_value == UINT16_MAX) {
               // ? idk mabe just push back UNDEFIEND
               exit(20);
           }
           key_chart[t][i] = static_cast<enum Key>(ke.kb_value);
       }
    }
    
    
    1. Update Loop (I advise to make a function ex. HandleKeyboard, and if you do, remember to keep your variables global) 4.1 parse input function
        inline constexpr int parse_input(const char * buf, int n) {
            int out = 0;
                
                for (int i = 0; i < n; i++) {
                    out = buf[i] & 0x7f; // set keycode
                    out += buf[i] & 0x80 ? 0x100 : 0x000; // add bit 0 if press bit 1 if release
                }
            return out;
        };
    

    4.2 Handle Keyboard

    std::bitset<KEYBOARD_MAX> key_states(0);
    int key_hit = -1;
    int key_released = -1;
    void HandleKeyboard(void) {
            int bytes, parsed, len;
            char buf[1];
    
            key_hit = -1;
            key_released = -1;
            int old_hit = -1;
    
            ioctl(fd, FIONREAD, &bytes);
            if (!bytes) return;
    
     ReadKeyboardAction:
            len = read(fd, buf, sizeof(buf)); bytes -= len;
            // TODO -> parse multi-byte sequences
            //(not sure if they even exist - they don't in the en-us layout)
    
            if (len <= 0) {
                if (len < 0) {
                   fprintf(stderr, "\nread error: %d\n", errno);
                   exit(15);
                } 
                return;
            }
    
            parsed = parse_input(buf,len);
    
            if ( (!key_states[parsed % KEYBOARD_MAX]) && (!(parsed / KEYBOARD_MAX)) ) key_hit = parsed % KEYBOARD_MAX;
            if ( key_states[parsed % KEYBOARD_MAX] && (parsed / KEYBOARD_MAX) ) key_released = parsed % KEYBOARD_MAX;
            key_states[parsed % KEYBOARD_MAX] = !(parsed / KEYBOARD_MAX);
    
            
            if (bytes > 0) goto ReadKeyboardAction;
    
    
      • Checking if a key is down/was just pressed or released:
    bool IsKeyDown(enum Key key) {
        for (auto i = 0; i < KEYBOARD_MAX; ++i)
            if (key_states[i] && key_chart[0][i] == key)
                return true;
        return false;
    }
    enum Key KeyPressed(void) {
            if (key_hit < 0) return Key::NONE;
            return key_chart[0][key_hit];
        }
    
    enum Key KeyReleased(void) {
        if (key_released < 0) return Key::NONE;
        return key_chart[0][key_released];
    }
    
      • Very important - end of program reset settings back to normal - if you don't the terminal might end up unsusable (it will if youre using linux without a graphical environment (raw console))
    void Fin(void) {
       tcsetattr(fd,TCSANOW,&old_fdterm);
       tcsetattr(STDIN_FILENO,TCSANOW,&old_termios);
       ioctl(fd, KDSKBMODE, old_kbdmode)
       close(fd);
    }
    

    This mean's that you should alway run this at exit:

    // run this right after init
                atexit(Fin);
                at_quick_exit(Fin);
    
                signal(SIGHUP, quick_exit);
                signal(SIGINT, quick_exit);
                signal(SIGQUIT, quick_exit);
                signal(SIGILL, quick_exit);
                signal(SIGTRAP, quick_exit);
                signal(SIGABRT, quick_exit);
                signal(SIGIOT, quick_exit);
                signal(SIGFPE, quick_exit);
                signal(SIGKILL, quick_exit);
                signal(SIGUSR1, quick_exit);
                signal(SIGSEGV, quick_exit);
                signal(SIGUSR2, quick_exit);
                signal(SIGPIPE, quick_exit);
                signal(SIGTERM, quick_exit);
            #ifdef SIGSTKFLT
                signal(SIGSTKFLT, quick_exit);
            #endif
                signal(SIGCHLD, quick_exit);
                signal(SIGCONT, quick_exit);
                signal(SIGSTOP, quick_exit);
                signal(SIGTSTP, quick_exit);
                signal(SIGTTIN, quick_exit);
                signal(SIGTTOU, quick_exit);
    

    this ensures that if Ctrl+C, pkill, segmantetion fault, floating point error or anything really kill it, it won't make the machine have to shutdow

    Example program using this:

    #include "Keyboard.hpp" // the above utilities
    #define IsCtrlDown() (IsKeyDown(Key::CTRL) || IsKeyDown(Key::CTRLL) || IsKeyDown(Key::CTRLR))
    #include <iostream>
    
    using namespace std;
    
    int main() {
        Init();
        while(true) {
            HandleKeyboard();
            if (KeyPressed() == Key::q && IsCtrlDown()) break;
            if (IsKeyDown(Key::k)) cout << "Key K is down\n";
            if (IsKeyReleased(Key::l) cout << "The L ended" << endl;
        }
        cout << "Goodbye\n" << flush;
        return 0;
    }
    

    If you want to know how to get toggled keys (capslock, numlock, scroll lock) than ask in a comment - I won't include it now 'cause this is already really long

    I've got a program that uses this - you can look at it's source code here on github - the specific code for handling keyboard is in Console.cpp (It's multi-platform so you have to find the section #ifdef __linux__) [function Console::Init and Console::HandleKeyboard]