The multi-threaded approach I mentioned in the comments, that has a separate thread to fetch and enqueue keys, being designed not to drop keys, isn't trivial. It requires some C skills and some UNIX knowledge. I implemented a working skeleton that runs, so you can see what's involved.
To test this, save the file as, let's say, dispatch.c
$ cc -o dispatch dispatch.c
$ ./dispatch
Sample output:
$ ./dispatch
Key 'a' pressed...
... Thread T3 pulled key 'a' from queue
... Thread T1 pulled key 'a' from queue
... Thread T2 pulled key 'a' from queue
Key 'b' pressed...
... Thread T2 pulled key 'b' from queue
... Thread T1 pulled key 'b' from queue
Key 'c' pressed...
... Thread T3 pulled key 'c' from queue
... Thread T1 pulled key 'c' from queue
Key 'd' pressed...
... Thread T2 pulled key 'd' from queue
... Thread T3 pulled key 'd' from queue
Key 'z' pressed...
... Thread T2 pulled key 'z' from queue
... Thread T1 pulled key 'z' from queue
... Thread T3 pulled key 'z' from queue
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <termios.h>
#include <sys/types.h>
typedef struct keyQueue {
struct keyQueue *next;
char key;
} keyQueue_t;
typedef struct ThreadInfo {
pthread_t tid; /* thread id */
pthread_mutex_t kqmutex; /* protects key queue from race condition between threads */
keyQueue_t kqhead; /* input keys queued to this thread */
char *keys; /* keys this thread responds to */
char *name; /* name of this thread */
} threadInfo_t;
static struct termios origtc, newtc;
threadInfo_t threads[] = {
{ 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '\0' }, "abcez", "Thread T1" },
{ 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '\0' }, "abdfz", "Thread T2" },
{ 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '\0' }, "acdgz", "Thread T3" }
};
void *service(void *arg) {
char key;
threadInfo_t *t = &threads[(int)arg]; // get pointer to thread
for(;;) {
pthread_mutex_lock(&t->kqmutex); // lock other threads out while we tamper
key = '\0'; // initialize key to NULL
if (t->kqhead.next != NULL) { // Anything queued up for us?
keyQueue_t *kq = t->kqhead.next; // if so get ptr to key pkt
key = kq->key; // fetch key from pkt
t->kqhead.next = kq->next; // Point to next key in queue (or NULL if no more queued up).
free(kq);
}
pthread_mutex_unlock(&t->kqmutex); // unlock key queue
if (key != '\0') { // if we got a key, log it
printf("... %s pulled key '%c' from queue\n", t->name, key);
}
// ⇓ usleep() probably more practical as 1-sec too long for most cases
sleep(1); // sleep so we don't loop too fast eating CPU
}
return NULL;
}
int main() {
/* Fire up threads */
for (long i = 0; i < sizeof (threads) / sizeof (threadInfo_t); i++) {
if (pthread_create(&threads[i].tid, NULL, service, (void *)i) < 0) {
perror("pthread_create()");
exit(-1);
}
}
tcgetattr(0, &origtc); // get orig tty settings
newtc = origtc; // copy them
newtc.c_lflag &= ~ICANON; // put in '1 key mode'
newtc.c_lflag &= ~ECHO; // turn off echo
for(;;) {
tcsetattr(0, TCSANOW, &newtc); // echo off 1-key read mode
char c = getchar(); // get single key immed.
tcsetattr(0, TCSANOW, &origtc); // settings back to normal
printf("Key '%c' pressed...\n", c); // show user what we got
for (int i = 0; i < sizeof (threads) / sizeof (threadInfo_t); i++) {
threadInfo_t *t = &threads[i]; // get shorthand ptr to thread
if (strchr(t->keys, c) != NULL) { // this thread listens for this key
pthread_mutex_lock(&t->kqmutex); // lock other threads out while we tamper
keyQueue_t *kq = calloc(sizeof (struct keyQueue), 1); // allocate pkt
kq->key = c; // stash key there
keyQueue_t *kptr = &t->kqhead; // get pointer to queue head
while(kptr->next != NULL) // find first empty slot
kptr = kptr->next;
kptr->next = kq; // enqueue key packet to thread
pthread_mutex_unlock(&t->kqmutex); // unlock key queue
}
}
}
}
This code starts three threads, t1, t2, t3, which each have a 'key queue' structure on them, as well as a char *
field keys
. keys
is a string containing the characters (keys) the thread is 'interested in'.
The keyboard keys listed in the string are duplicated in the threads string so that one key can be consumed by more than one thread in some cases. For example, all the threads listen to 'a' and 'z', two threads listen to 'b', another two to 'c', another pair of threads are interested in 'd', and finally 'e', 'f', and 'g' have only one thread listening, respectively.
The main loop reads keys without echo and captures keys immediately (e.g. without the user having to hit return). When a key is entered, it loops through the thread info to find out which threads are interested in the pressed key and enqueues the key (in a packet) to the respective thread(s).
The threads are in their own loop, sleeping one second in between loops. When they wake up they check their queue to see if there are any keys queued. If there are they pull it from the queue and say they pulled that key from the queue.
Because of the delay in each thread's polling/work loop (e.g. before the threads wake up and check their respective queues), there's time for you to enter multiple things on the keyboard to get queued up to the threads, and then the threads will dequeue them the enqueue keys one at a time at 1 second intervals.
In real life the program would use a much shorter sleep, but would put something in there to keep each thread from needlessly hogging a lot of CPU time.
Kind of fun to run it and see it in action.
*Note: calloc()
is used instead of malloc()
because unlike malloc()
, calloc()
initializes the memory returned to all 0's. It's a nice trick.