diff options
Diffstat (limited to 'src/termkey/driver-ti.c')
-rw-r--r-- | src/termkey/driver-ti.c | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/src/termkey/driver-ti.c b/src/termkey/driver-ti.c new file mode 100644 index 0000000000..1f8ee10808 --- /dev/null +++ b/src/termkey/driver-ti.c @@ -0,0 +1,654 @@ +// we want strdup() +#define _XOPEN_SOURCE 600 + +#include "termkey.h" +#include "termkey-internal.h" + +#ifdef HAVE_UNIBILIUM +# include <unibilium.h> +#else +# include <curses.h> +# include <term.h> + +/* curses.h has just polluted our namespace. We want this back */ +# undef buttons +#endif + +#include <ctype.h> +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#ifndef _WIN32 +# include <unistd.h> +#else +# include <io.h> +#endif +#include <sys/types.h> +#include <sys/stat.h> + +#define streq(a,b) (!strcmp(a,b)) + +#define MAX_FUNCNAME 9 + +static struct { + const char *funcname; + TermKeyType type; + TermKeySym sym; + int mods; +} funcs[] = +{ + /* THIS LIST MUST REMAIN SORTED! */ + { "backspace", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BACKSPACE, 0 }, + { "begin", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 0 }, + { "beg", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 0 }, + { "btab", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_TAB, TERMKEY_KEYMOD_SHIFT }, + { "cancel", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CANCEL, 0 }, + { "clear", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CLEAR, 0 }, + { "close", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_CLOSE, 0 }, + { "command", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_COMMAND, 0 }, + { "copy", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_COPY, 0 }, + { "dc", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DELETE, 0 }, + { "down", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DOWN, 0 }, + { "end", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_END, 0 }, + { "enter", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_ENTER, 0 }, + { "exit", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_EXIT, 0 }, + { "find", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_FIND, 0 }, + { "help", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HELP, 0 }, + { "home", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_HOME, 0 }, + { "ic", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_INSERT, 0 }, + { "left", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_LEFT, 0 }, + { "mark", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MARK, 0 }, + { "message", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MESSAGE, 0 }, + { "move", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MOVE, 0 }, + { "next", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, // Not quite, but it's the best we can do + { "npage", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, + { "open", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPEN, 0 }, + { "options", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPTIONS, 0 }, + { "ppage", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, + { "previous", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, // Not quite, but it's the best we can do + { "print", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PRINT, 0 }, + { "redo", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REDO, 0 }, + { "reference", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFERENCE, 0 }, + { "refresh", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFRESH, 0 }, + { "replace", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REPLACE, 0 }, + { "restart", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RESTART, 0 }, + { "resume", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RESUME, 0 }, + { "right", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_RIGHT, 0 }, + { "save", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SAVE, 0 }, + { "select", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SELECT, 0 }, + { "suspend", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SUSPEND, 0 }, + { "undo", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UNDO, 0 }, + { "up", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UP, 0 }, + { NULL }, +}; + +#ifdef HAVE_UNIBILIUM +static enum unibi_string unibi_lookup_str(const char *name) +{ + for(enum unibi_string ret = unibi_string_begin_+1; ret < unibi_string_end_; ret++) + if(streq(unibi_name_str(ret), name)) + return ret; + + return -1; +} + +static const char *unibi_get_str_by_name(const unibi_term *ut, const char *name) +{ + enum unibi_string idx = unibi_lookup_str(name); + if(idx == (enum unibi_string)-1) + return NULL; + + return unibi_get_str(ut, idx); +} +#endif + +/* To be efficient at lookups, we store the byte sequence => keyinfo mapping + * in a trie. This avoids a slow linear search through a flat list of + * sequences. Because it is likely most nodes will be very sparse, we optimise + * vector to store an extent map after the database is loaded. + */ + +typedef enum { + TYPE_KEY, + TYPE_ARR, +} trie_nodetype; + +struct trie_node { + trie_nodetype type; +}; + +struct trie_node_key { + trie_nodetype type; + struct keyinfo key; +}; + +struct trie_node_arr { + trie_nodetype type; + unsigned char min, max; /* INCLUSIVE endpoints of the extent range */ + struct trie_node *arr[]; /* dynamic size at allocation time */ +}; + +typedef struct { + TermKey *tk; + +#ifdef HAVE_UNIBILIUM + unibi_term *unibi; /* only valid until first 'start' call */ +#else + char *term; /* only valid until first 'start' call */ +#endif + + struct trie_node *root; + + char *start_string; + char *stop_string; +} TermKeyTI; + +static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node); + +static struct trie_node *new_node_key(TermKeyType type, TermKeySym sym, int modmask, int modset) +{ + struct trie_node_key *n = malloc(sizeof(*n)); + if(!n) + return NULL; + + n->type = TYPE_KEY; + + n->key.type = type; + n->key.sym = sym; + n->key.modifier_mask = modmask; + n->key.modifier_set = modset; + + return (struct trie_node*)n; +} + +static struct trie_node *new_node_arr(unsigned char min, unsigned char max) +{ + struct trie_node_arr *n = malloc(sizeof(*n) + ((int)max-min+1) * sizeof(n->arr[0])); + if(!n) + return NULL; + + n->type = TYPE_ARR; + n->min = min; n->max = max; + + int i; + for(i = min; i <= max; i++) + n->arr[i-min] = NULL; + + return (struct trie_node*)n; +} + +static struct trie_node *lookup_next(struct trie_node *n, unsigned char b) +{ + switch(n->type) { + case TYPE_KEY: + fprintf(stderr, "ABORT: lookup_next within a TYPE_KEY node\n"); + abort(); + case TYPE_ARR: + { + struct trie_node_arr *nar = (struct trie_node_arr*)n; + if(b < nar->min || b > nar->max) + return NULL; + return nar->arr[b - nar->min]; + } + } + + return NULL; // Never reached but keeps compiler happy +} + +static void free_trie(struct trie_node *n) +{ + switch(n->type) { + case TYPE_KEY: + break; + case TYPE_ARR: + { + struct trie_node_arr *nar = (struct trie_node_arr*)n; + int i; + for(i = nar->min; i <= nar->max; i++) + if(nar->arr[i - nar->min]) + free_trie(nar->arr[i - nar->min]); + break; + } + } + + free(n); +} + +static struct trie_node *compress_trie(struct trie_node *n) +{ + if(!n) + return NULL; + + switch(n->type) { + case TYPE_KEY: + return n; + case TYPE_ARR: + { + struct trie_node_arr *nar = (struct trie_node_arr*)n; + unsigned char min, max; + // Find the real bounds + for(min = 0; !nar->arr[min]; min++) + if(min == 255 && !nar->arr[min]) { + free(nar); + return new_node_arr(1, 0); + } + + for(max = 0xff; !nar->arr[max]; max--) + ; + + struct trie_node_arr *new = (struct trie_node_arr*)new_node_arr(min, max); + int i; + for(i = min; i <= max; i++) + new->arr[i - min] = compress_trie(nar->arr[i]); + + free(nar); + return (struct trie_node*)new; + } + } + + return n; +} + +static bool try_load_terminfo_key(TermKeyTI *ti, const char *name, struct keyinfo *info) +{ + const char *value = NULL; + +#ifdef HAVE_UNIBILIUM + if(ti->unibi) + value = unibi_get_str_by_name(ti->unibi, name); +#else + if(ti->term) + value = tigetstr(name); +#endif + + if(ti->tk->ti_getstr_hook) + value = (ti->tk->ti_getstr_hook)(name, value, ti->tk->ti_getstr_hook_data); + + if(!value || value == (char*)-1 || !value[0]) + return false; + + struct trie_node *node = new_node_key(info->type, info->sym, info->modifier_mask, info->modifier_set); + insert_seq(ti, value, node); + + return true; +} + +static int load_terminfo(TermKeyTI *ti) +{ + int i; + +#ifdef HAVE_UNIBILIUM + unibi_term *unibi = ti->unibi; +#else + { + int err; + + /* Have to cast away the const. But it's OK - we know terminfo won't really + * modify term */ + if(setupterm((char*)ti->term, 1, &err) != OK) + return 0; + } +#endif + + ti->root = new_node_arr(0, 0xff); + if(!ti->root) + return 0; + + /* First the regular key strings + */ + for(i = 0; funcs[i].funcname; i++) { + char name[MAX_FUNCNAME + 5 + 1]; + + sprintf(name, "key_%s", funcs[i].funcname); + if(!try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = funcs[i].type, + .sym = funcs[i].sym, + .modifier_mask = funcs[i].mods, + .modifier_set = funcs[i].mods, + })) + continue; + + /* Maybe it has a shifted version */ + sprintf(name, "key_s%s", funcs[i].funcname); + try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = funcs[i].type, + .sym = funcs[i].sym, + .modifier_mask = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, + .modifier_set = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, + }); + } + + /* Now the F<digit> keys + */ + for(i = 1; i < 255; i++) { + char name[9]; + sprintf(name, "key_f%d", i); + if(!try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = TERMKEY_TYPE_FUNCTION, + .sym = i, + .modifier_mask = 0, + .modifier_set = 0, + })) + break; + } + + /* Finally mouse mode */ + { + const char *value = NULL; + +#ifdef HAVE_UNIBILIUM + if(ti->unibi) + value = unibi_get_str_by_name(ti->unibi, "key_mouse"); +#else + if(ti->term) + value = tigetstr("key_mouse"); +#endif + + if(ti->tk->ti_getstr_hook) + value = (ti->tk->ti_getstr_hook)("key_mouse", value, ti->tk->ti_getstr_hook_data); + + /* Some terminfos (e.g. xterm-1006) claim a different key_mouse that won't + * give X10 encoding. We'll only accept this if it's exactly "\e[M" + */ + if(value && streq(value, "\x1b[M")) { + struct trie_node *node = new_node_key(TERMKEY_TYPE_MOUSE, 0, 0, 0); + insert_seq(ti, value, node); + } + } + + /* Take copies of these terminfo strings, in case we build multiple termkey + * instances for multiple different termtypes, and it's different by the + * time we want to use it + */ +#ifdef HAVE_UNIBILIUM + const char *keypad_xmit = unibi ? + unibi_get_str(unibi, unibi_keypad_xmit) : + NULL; +#endif + + if(keypad_xmit) + ti->start_string = strdup(keypad_xmit); + else + ti->start_string = NULL; + +#ifdef HAVE_UNIBILIUM + const char *keypad_local = unibi ? + unibi_get_str(unibi, unibi_keypad_local) : + NULL; +#endif + + if(keypad_local) + ti->stop_string = strdup(keypad_local); + else + ti->stop_string = NULL; + +#ifdef HAVE_UNIBILIUM + if(unibi) + unibi_destroy(unibi); + + ti->unibi = NULL; +#else + if(ti->term) + free(ti->term); + + ti->term = NULL; +#endif + + ti->root = compress_trie(ti->root); + + return 1; +} + +static void *new_driver(TermKey *tk, const char *term) +{ + TermKeyTI *ti = malloc(sizeof *ti); + if(!ti) + return NULL; + + ti->tk = tk; + ti->root = NULL; + ti->start_string = NULL; + ti->stop_string = NULL; + +#ifdef HAVE_UNIBILIUM + ti->unibi = unibi_from_term(term); + int saved_errno = errno; + if(!ti->unibi && saved_errno != ENOENT) { + free(ti); + return NULL; + } + /* ti->unibi may be NULL if errno == ENOENT. That means the terminal wasn't + * known. Lets keep going because if we get getstr hook that might invent + * new strings for us + */ +#else + { + int err; + + ti->term = NULL; + + /* Have to cast away the const. But it's OK - we know terminfo won't really + * modify term */ + if(setupterm((char*)term, 1, &err) == OK) + ti->term = strdup(term); + } +#endif + + return ti; +} + +static int start_driver(TermKey *tk, void *info) +{ + TermKeyTI *ti = info; + struct stat statbuf; + char *start_string; + size_t len; + + if(!ti->root) + load_terminfo(ti); + + start_string = ti->start_string; + + if(tk->fd == -1 || !start_string) + return 1; + + /* The terminfo database will contain keys in application cursor key mode. + * We may need to enable that mode + */ + + /* There's no point trying to write() to a pipe */ + if(fstat(tk->fd, &statbuf) == -1) + return 0; + +#ifndef _WIN32 + if(S_ISFIFO(statbuf.st_mode)) + return 1; +#endif + + // Can't call putp or tputs because they suck and don't give us fd control + len = strlen(start_string); + while(len) { + size_t written = write(tk->fd, start_string, len); + if(written == -1) + return 0; + start_string += written; + len -= written; + } + return 1; +} + +static int stop_driver(TermKey *tk, void *info) +{ + TermKeyTI *ti = info; + struct stat statbuf; + char *stop_string = ti->stop_string; + size_t len; + + if(tk->fd == -1 || !stop_string) + return 1; + + /* There's no point trying to write() to a pipe */ + if(fstat(tk->fd, &statbuf) == -1) + return 0; + +#ifndef _WIN32 + if(S_ISFIFO(statbuf.st_mode)) + return 1; +#endif + + /* The terminfo database will contain keys in application cursor key mode. + * We may need to enable that mode + */ + + // Can't call putp or tputs because they suck and don't give us fd control + len = strlen(stop_string); + while(len) { + size_t written = write(tk->fd, stop_string, len); + if(written == -1) + return 0; + stop_string += written; + len -= written; + } + return 1; +} + +static void free_driver(void *info) +{ + TermKeyTI *ti = info; + + free_trie(ti->root); + + if(ti->start_string) + free(ti->start_string); + + if(ti->stop_string) + free(ti->stop_string); + +#ifdef HAVE_UNIBILIUM + if(ti->unibi) + unibi_destroy(ti->unibi); +#else + if(ti->term) + free(ti->term); +#endif + + free(ti); +} + +#define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) + +static TermKeyResult peekkey(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) +{ + TermKeyTI *ti = info; + + if(tk->buffcount == 0) + return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + + struct trie_node *p = ti->root; + + unsigned int pos = 0; + while(pos < tk->buffcount) { + p = lookup_next(p, CHARAT(pos)); + if(!p) + break; + + pos++; + + if(p->type != TYPE_KEY) + continue; + + struct trie_node_key *nk = (struct trie_node_key*)p; + if(nk->key.type == TERMKEY_TYPE_MOUSE) { + tk->buffstart += pos; + tk->buffcount -= pos; + + TermKeyResult mouse_result = (*tk->method.peekkey_mouse)(tk, key, nbytep); + + tk->buffstart -= pos; + tk->buffcount += pos; + + if(mouse_result == TERMKEY_RES_KEY) + *nbytep += pos; + + return mouse_result; + } + + key->type = nk->key.type; + key->code.sym = nk->key.sym; + key->modifiers = nk->key.modifier_set; + *nbytep = pos; + return TERMKEY_RES_KEY; + } + + // If p is not NULL then we hadn't walked off the end yet, so we have a + // partial match + if(p && !force) + return TERMKEY_RES_AGAIN; + + return TERMKEY_RES_NONE; +} + +static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node) +{ + int pos = 0; + struct trie_node *p = ti->root; + + // Unsigned because we'll be using it as an array subscript + unsigned char b; + + while((b = seq[pos])) { + struct trie_node *next = lookup_next(p, b); + if(!next) + break; + p = next; + pos++; + } + + while((b = seq[pos])) { + struct trie_node *next; + if(seq[pos+1]) + // Intermediate node + next = new_node_arr(0, 0xff); + else + // Final key node + next = node; + + if(!next) + return 0; + + switch(p->type) { + case TYPE_ARR: + { + struct trie_node_arr *nar = (struct trie_node_arr*)p; + if(b < nar->min || b > nar->max) { + fprintf(stderr, "ASSERT FAIL: Trie insert at 0x%02x is outside of extent bounds (0x%02x..0x%02x)\n", + b, nar->min, nar->max); + abort(); + } + nar->arr[b - nar->min] = next; + p = next; + break; + } + case TYPE_KEY: + fprintf(stderr, "ASSERT FAIL: Tried to insert child node in TYPE_KEY\n"); + abort(); + } + + pos++; + } + + return 1; +} + +struct TermKeyDriver termkey_driver_ti = { + .name = "terminfo", + + .new_driver = new_driver, + .free_driver = free_driver, + + .start_driver = start_driver, + .stop_driver = stop_driver, + + .peekkey = peekkey, +}; |