aboutsummaryrefslogtreecommitdiff
path: root/src/termkey/driver-ti.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/termkey/driver-ti.c')
-rw-r--r--src/termkey/driver-ti.c654
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,
+};