diff options
-rw-r--r-- | Makefile | 17 | ||||
-rw-r--r-- | demo.c | 47 | ||||
-rw-r--r-- | termkey.c | 438 | ||||
-rw-r--r-- | termkey.h | 113 |
4 files changed, 615 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f0afb15 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +CCFLAGS=-Wall -Iinclude -std=c99 +LDFLAGS= + +CCFLAGS+=$(shell pkg-config --cflags glib-2.0) +LDFLAGS+=$(shell pkg-config --libs glib-2.0) + +all: demo + +demo: termkey.o demo.c + gcc $(CCFLAGS) $(LDFLAGS) -o $@ $^ + +termkey.o: termkey.c + gcc $(CCFLAGS) -o $@ -c $^ + +.PHONY: clean +clean: + rm -f *.o demo @@ -0,0 +1,47 @@ +#include <stdio.h> +#include <stdlib.h> +#include <termios.h> +#include <unistd.h> + +#include "termkey.h" + +int main(int argc, char *argv[]) { + struct termios termios; + + if(tcgetattr(0, &termios)) { + perror("ioctl(TCIOGETS)"); + exit(1); + } + + int old_lflag = termios.c_lflag; + termios.c_lflag &= ~(ICANON|ECHO); + + tcsetattr(0, TCSANOW, &termios); + + termkey_t *tk = termkey_new(0, TERMKEY_FLAG_CONVERTKP); + + termkey_key key; + + while(termkey_waitkey(tk, &key) && key.code != TERMKEY_SYM_EOF) { + if(key.flags & TERMKEY_KEYFLAG_SPECIAL) + printf("Key %s%s%s%s (code %d)\n", + key.modifiers & TERMKEY_KEYMOD_SHIFT ? "S-" : "", + key.modifiers & TERMKEY_KEYMOD_ALT ? "A-" : "", + key.modifiers & TERMKEY_KEYMOD_CTRL ? "C-" : "", + termkey_describe_sym(key.code), + key.code); + else + printf("Key %s%s%s%s (U+%04X)\n", + key.modifiers & TERMKEY_KEYMOD_SHIFT ? "S-" : "", + key.modifiers & TERMKEY_KEYMOD_ALT ? "A-" : "", + key.modifiers & TERMKEY_KEYMOD_CTRL ? "C-" : "", + key.utf8, + key.code); + + if(key.modifiers & TERMKEY_KEYMOD_CTRL && key.code == 'C') + break; + } + + termios.c_lflag = old_lflag; + tcsetattr(0, TCSANOW, &termios); +} diff --git a/termkey.c b/termkey.c new file mode 100644 index 0000000..ef72cbf --- /dev/null +++ b/termkey.c @@ -0,0 +1,438 @@ +#include "termkey.h" + +#include <unistd.h> +#include <string.h> + +// Use GLib to implement for now because I am lazy. Rewrite sometime +#include <glib.h> +#include <stdio.h> + +struct termkey { + int fd; + int flags; + unsigned char *buffer; + size_t buffvalid; + size_t buffsize; + + char is_closed; +}; + +termkey_t *termkey_new_full(int fd, int flags, size_t buffsize) +{ + termkey_t *tk = g_new0(struct termkey, 1); + + tk->fd = fd; + tk->flags = flags; + + tk->buffer = g_malloc0(buffsize); + tk->buffvalid = 0; + tk->buffsize = buffsize; + + tk->is_closed = 0; + + return tk; +} + +termkey_t *termkey_new(int fd, int flags) +{ + return termkey_new_full(fd, flags, 256); +} + +static inline void eatbytes(termkey_t *tk, size_t count) +{ + memmove(tk->buffer, tk->buffer + count, tk->buffvalid - count); + tk->buffvalid -= count; +} + +static int getkey_csi(termkey_t *tk, size_t introlen, termkey_key *key) +{ + size_t csi_end = introlen; + + while(csi_end < tk->buffvalid) { + if(tk->buffer[csi_end] >= 0x40 && tk->buffer[csi_end] < 0x80) + break; + csi_end++; + } + + if(csi_end >= tk->buffvalid) { + key->code = TERMKEY_SYM_NONE; + return key->code; + } + + unsigned char cmd = tk->buffer[csi_end]; + int arg1 = -1; + int arg2 = -1; + + size_t p = introlen; + + // Now attempt to parse out up to the first two 1;2; separated arguments + if(tk->buffer[p] >= '0' && tk->buffer[p] <= '9') { + arg1 = 0; + while(p < csi_end && tk->buffer[p] != ';') { + arg1 = (arg1 * 10) + tk->buffer[p] - '0'; + p++; + } + + if(tk->buffer[p] == ';') + p++; + + if(tk->buffer[p] >= '0' && tk->buffer[p] <= '9') { + arg2 = 0; + while(p < csi_end && tk->buffer[p] != ';') { + arg2 = (arg2 * 10) + tk->buffer[p] - '0'; + p++; + } + } + } + + key->code = TERMKEY_SYM_UNKNOWN; + + switch(cmd) { + case 'A': key->code = TERMKEY_SYM_UP; break; + case 'B': key->code = TERMKEY_SYM_DOWN; break; + case 'C': key->code = TERMKEY_SYM_RIGHT; break; + case 'D': key->code = TERMKEY_SYM_LEFT; break; + case 'E': key->code = TERMKEY_SYM_BEGIN; break; + case 'F': key->code = TERMKEY_SYM_END; break; + case 'H': key->code = TERMKEY_SYM_HOME; break; + case 'P': key->code = TERMKEY_SYM_F1; break; + case 'Q': key->code = TERMKEY_SYM_F2; break; + case 'R': key->code = TERMKEY_SYM_F3; break; + case 'S': key->code = TERMKEY_SYM_F4; break; + case '~': + switch(arg1) { + case 1: key->code = TERMKEY_SYM_HOME; break; + case 2: key->code = TERMKEY_SYM_INSERT; break; + case 3: key->code = TERMKEY_SYM_DELETE; break; + case 4: key->code = TERMKEY_SYM_END; break; + case 5: key->code = TERMKEY_SYM_PAGEUP; break; + case 6: key->code = TERMKEY_SYM_PAGEDOWN; break; + case 15: key->code = TERMKEY_SYM_F5; break; + case 17: key->code = TERMKEY_SYM_F6; break; + case 18: key->code = TERMKEY_SYM_F7; break; + case 19: key->code = TERMKEY_SYM_F8; break; + case 20: key->code = TERMKEY_SYM_F9; break; + case 21: key->code = TERMKEY_SYM_F10; break; + case 23: key->code = TERMKEY_SYM_F11; break; + case 24: key->code = TERMKEY_SYM_F12; break; + + default: + fprintf(stderr, "CSI function key %d\n", arg1); + } + break; + + default: + fprintf(stderr, "CSI arg1=%d arg2=%d cmd=%c\n", arg1, arg2, cmd); + } + + key->modifiers = arg2 == -1 ? 0 : arg2 - 1; + key->flags = TERMKEY_KEYFLAG_SPECIAL; + + eatbytes(tk, csi_end + 1); + + return key->code; +} + +static int getkey_ss3(termkey_t *tk, size_t introlen, termkey_key *key) +{ + if(introlen + 1 < tk->buffvalid) { + key->code = TERMKEY_SYM_NONE; + return key->code; + } + + unsigned char cmd = tk->buffer[introlen]; + + key->code = TERMKEY_SYM_UNKNOWN; + + switch(cmd) { + case 'A': key->code = TERMKEY_SYM_UP; break; + case 'B': key->code = TERMKEY_SYM_DOWN; break; + case 'C': key->code = TERMKEY_SYM_RIGHT; break; + case 'D': key->code = TERMKEY_SYM_LEFT; break; + case 'F': key->code = TERMKEY_SYM_END; break; + case 'H': key->code = TERMKEY_SYM_HOME; break; + case 'M': key->code = TERMKEY_SYM_KPENTER; break; + case 'P': key->code = TERMKEY_SYM_F1; break; + case 'Q': key->code = TERMKEY_SYM_F2; break; + case 'R': key->code = TERMKEY_SYM_F3; break; + case 'S': key->code = TERMKEY_SYM_F4; break; + case 'X': key->code = TERMKEY_SYM_KPEQUALS; break; + case 'j': key->code = TERMKEY_SYM_KPMULT; break; + case 'k': key->code = TERMKEY_SYM_KPPLUS; break; + case 'l': key->code = TERMKEY_SYM_KPCOMMA; break; + case 'm': key->code = TERMKEY_SYM_KPMINUS; break; + case 'n': key->code = TERMKEY_SYM_KPPERIOD; break; + case 'o': key->code = TERMKEY_SYM_KPDIV; break; + case 'p': key->code = TERMKEY_SYM_KP0; break; + case 'q': key->code = TERMKEY_SYM_KP1; break; + case 'r': key->code = TERMKEY_SYM_KP2; break; + case 's': key->code = TERMKEY_SYM_KP3; break; + case 't': key->code = TERMKEY_SYM_KP4; break; + case 'u': key->code = TERMKEY_SYM_KP5; break; + case 'v': key->code = TERMKEY_SYM_KP6; break; + case 'w': key->code = TERMKEY_SYM_KP7; break; + case 'x': key->code = TERMKEY_SYM_KP8; break; + case 'y': key->code = TERMKEY_SYM_KP9; break; + + default: + fprintf(stderr, "SS3 %c (0x%02x)\n", cmd, cmd); + break; + } + + key->modifiers = 0; + key->flags = TERMKEY_KEYFLAG_SPECIAL; + + if(tk->flags & TERMKEY_FLAG_CONVERTKP) { + char is_kp = 1; + switch(key->code) { + case TERMKEY_SYM_KP0: key->code = '0'; break; + case TERMKEY_SYM_KP1: key->code = '1'; break; + case TERMKEY_SYM_KP2: key->code = '2'; break; + case TERMKEY_SYM_KP3: key->code = '3'; break; + case TERMKEY_SYM_KP4: key->code = '4'; break; + case TERMKEY_SYM_KP5: key->code = '5'; break; + case TERMKEY_SYM_KP6: key->code = '6'; break; + case TERMKEY_SYM_KP7: key->code = '7'; break; + case TERMKEY_SYM_KP8: key->code = '8'; break; + case TERMKEY_SYM_KP9: key->code = '9'; break; + case TERMKEY_SYM_KPPLUS: key->code = '+'; break; + case TERMKEY_SYM_KPMINUS: key->code = '-'; break; + case TERMKEY_SYM_KPMULT: key->code = '*'; break; + case TERMKEY_SYM_KPDIV: key->code = '/'; break; + case TERMKEY_SYM_KPCOMMA: key->code = ','; break; + case TERMKEY_SYM_KPPERIOD: key->code = '.'; break; + case TERMKEY_SYM_KPEQUALS: key->code = '='; break; + + default: + is_kp = 0; + break; + } + + if(is_kp) { + key->flags = 0; + + key->utf8[0] = key->code; + key->utf8[1] = 0; + } + } + + eatbytes(tk, introlen + 1); + + return key->code; +} + +int termkey_getkey(termkey_t *tk, termkey_key *key) +{ + if(tk->buffvalid == 0) { + key->code = tk->is_closed ? TERMKEY_SYM_EOF : TERMKEY_SYM_NONE; + return key->code; + } + + // Now we're sure at least 1 byte is valid + unsigned char b0 = tk->buffer[0]; + + if(b0 == 0x1b) { + if(tk->buffvalid == 1) { + key->code = TERMKEY_SYM_ESCAPE; + key->modifiers = 0; + key->flags = TERMKEY_KEYFLAG_SPECIAL; + + eatbytes(tk, 1); + + return key->code; + } + + unsigned char b1 = tk->buffer[1]; + if(b1 == '[') + return getkey_csi(tk, 2, key); + + if(b1 == 'O') + return getkey_ss3(tk, 2, key); + + tk->buffer++; + + int metakey = termkey_getkey(tk, key); + if(metakey != TERMKEY_SYM_NONE) { + key->modifiers |= TERMKEY_KEYMOD_ALT; + tk->buffer--; + eatbytes(tk, 1); + } + + return key->code; + } + else if(b0 < 0x20) { + // Control key + + key->code = TERMKEY_SYM_UNKNOWN; + + if(!(tk->flags & TERMKEY_FLAG_NOINTERPRET)) { + // Try to interpret C0 codes that have nice names + switch(b0) { + case 0x08: key->code = TERMKEY_SYM_BACKSPACE; break; + case 0x09: key->code = TERMKEY_SYM_TAB; break; + case 0x0a: key->code = TERMKEY_SYM_ENTER; break; + } + } + + if(key->code == TERMKEY_SYM_UNKNOWN) { + key->code = b0 + 0x40; + key->modifiers = TERMKEY_KEYMOD_CTRL; + key->flags = 0; + + key->utf8[0] = key->code; + key->utf8[1] = 0; + } + else { + key->modifiers = 0; + key->flags = TERMKEY_KEYFLAG_SPECIAL; + } + + eatbytes(tk, 1); + + return key->code; + } + else if(b0 == 0x20 && !(tk->flags & TERMKEY_FLAG_NOINTERPRET)) { + key->code = TERMKEY_SYM_SPACE; + key->modifiers = 0; + key->flags = TERMKEY_KEYFLAG_SPECIAL; + + eatbytes(tk, 1); + + return key->code; + } + else if(b0 == 0x7f && !(tk->flags & TERMKEY_FLAG_NOINTERPRET)) { + key->code = TERMKEY_SYM_DEL; + key->modifiers = 0; + key->flags = TERMKEY_KEYFLAG_SPECIAL; + + eatbytes(tk, 1); + + return key->code; + } + else if(b0 >= 0x20 && b0 < 0x80) { + // ASCII lowbyte range + key->code = b0; + key->modifiers = 0; + key->flags = 0; + + key->utf8[0] = key->code; + key->utf8[1] = 0; + + eatbytes(tk, 1); + + return key->code; + } + + fprintf(stderr, "TODO - tk->buffer[0] == 0x%02x\n", tk->buffer[0]); + return TERMKEY_SYM_NONE; +} + +int termkey_waitkey(termkey_t *tk, termkey_key *key) +{ + int ret; + while((ret = termkey_getkey(tk, key)) == TERMKEY_SYM_NONE) { + termkey_advisereadable(tk); + } + + return ret; +} + +void termkey_pushinput(termkey_t *tk, unsigned char *input, size_t inputlen) +{ + if(tk->buffvalid + inputlen > tk->buffsize) { + fprintf(stderr, "TODO! Extend input buffer!\n"); + exit(0); + } + + // Not strcpy just in case of NUL bytes + memcpy(tk->buffer + tk->buffvalid, input, inputlen); + tk->buffvalid += inputlen; +} + +void termkey_advisereadable(termkey_t *tk) +{ + unsigned char buffer[64]; // Smaller than the default size + size_t len = read(tk->fd, buffer, sizeof buffer); + + if(len == -1) + tk->is_closed = 1; + else + termkey_pushinput(tk, buffer, len); +} + +// Must be kept synchronised with enum termkey_sym_e in termkey.h +const char *keynames[] = { + "None", + + // Special names in C0 + "Backspace", + "Tab", + "Enter", + "Escape", + + // Special names in G0 + "Space", + "DEL", + + // CSI keys + "Up", + "Down", + "Left", + "Right", + "Begin", + + // CSI function keys + "Insert", + "Delete", + "Home", + "End", + "PageUp", + "PageDown", + + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + + // Numeric keypad special keys + "KP0", + "KP1", + "KP2", + "KP3", + "KP4", + "KP5", + "KP6", + "KP7", + "KP8", + "KP9", + "KPEnter", + "KPPlus", + "KPMinus", + "KPMult", + "KPDiv", + "KPComma", + "KPPeriod", + "KPEquals", +}; + +const char *termkey_describe_sym(int code) +{ + if(code == -1) + return "EOF"; + + if(code == -2) + return "UNKNOWN"; + + if(code < sizeof(keynames)/sizeof(keynames[0])) + return keynames[code]; + + return "UNKNOWN"; +} diff --git a/termkey.h b/termkey.h new file mode 100644 index 0000000..a99e428 --- /dev/null +++ b/termkey.h @@ -0,0 +1,113 @@ +#ifndef __TERMKEY_H__ +#define __TERMKEY_H__ + +#include <stdint.h> +#include <stdlib.h> + +typedef enum { + TERMKEY_SYM_EOF = -1, // Stream closed + TERMKEY_SYM_UNKNOWN = -2, + TERMKEY_SYM_NONE = 0, // Did not find a key + + // Special names in C0 + TERMKEY_SYM_BACKSPACE, + TERMKEY_SYM_TAB, + TERMKEY_SYM_ENTER, + TERMKEY_SYM_ESCAPE, + + // Special names in G0 + TERMKEY_SYM_SPACE, + TERMKEY_SYM_DEL, + + // CSI keys + TERMKEY_SYM_UP, + TERMKEY_SYM_DOWN, + TERMKEY_SYM_LEFT, + TERMKEY_SYM_RIGHT, + TERMKEY_SYM_BEGIN, + + // CSI function keys + TERMKEY_SYM_INSERT, + TERMKEY_SYM_DELETE, + TERMKEY_SYM_HOME, + TERMKEY_SYM_END, + TERMKEY_SYM_PAGEUP, + TERMKEY_SYM_PAGEDOWN, + + TERMKEY_SYM_F1, + TERMKEY_SYM_F2, + TERMKEY_SYM_F3, + TERMKEY_SYM_F4, + TERMKEY_SYM_F5, + TERMKEY_SYM_F6, + TERMKEY_SYM_F7, + TERMKEY_SYM_F8, + TERMKEY_SYM_F9, + TERMKEY_SYM_F10, + TERMKEY_SYM_F11, + TERMKEY_SYM_F12, + + // Numeric keypad special keys + TERMKEY_SYM_KP0, + TERMKEY_SYM_KP1, + TERMKEY_SYM_KP2, + TERMKEY_SYM_KP3, + TERMKEY_SYM_KP4, + TERMKEY_SYM_KP5, + TERMKEY_SYM_KP6, + TERMKEY_SYM_KP7, + TERMKEY_SYM_KP8, + TERMKEY_SYM_KP9, + TERMKEY_SYM_KPENTER, + TERMKEY_SYM_KPPLUS, + TERMKEY_SYM_KPMINUS, + TERMKEY_SYM_KPMULT, + TERMKEY_SYM_KPDIV, + TERMKEY_SYM_KPCOMMA, + TERMKEY_SYM_KPPERIOD, + TERMKEY_SYM_KPEQUALS, + + // et cetera ad nauseum +} termkey_sym_e; + +enum { + TERMKEY_KEYFLAG_SPECIAL = 0x01, // 'code' is a special keycode, not a unicode codepoint +}; + +enum { + TERMKEY_KEYMOD_SHIFT = 0x01, + TERMKEY_KEYMOD_ALT = 0x02, + TERMKEY_KEYMOD_CTRL = 0x04, +}; + +typedef struct { + int modifiers; + int code; + int flags; + + /* Any Unicode character can be UTF-8 encoded in no more than 5 bytes, plus + * terminating NUL */ + char utf8[6]; +} termkey_key; + +typedef struct termkey termkey_t; + +enum { + TERMKEY_FLAG_NOINTERPRET = 0x01, // Do not interpret C0//G1 codes if possible + TERMKEY_FLAG_CONVERTKP = 0x02, // Convert KP codes to regular keypresses +}; + +termkey_t *termkey_new(int fd, int flags); +void termkey_free(termkey_t *tk); + +int termkey_getkey(termkey_t *tk, termkey_key *key); +int termkey_waitkey(termkey_t *tk, termkey_key *key); + +void termkey_pushinput(termkey_t *tk, unsigned char *input, size_t inputlen); + +void termkey_advisereadable(termkey_t *tk); + + +const char *termkey_describe_sym(int code); + +#endif |