commit eb57aa4c696fee5049ea3731f97513fc405e159b
Author: Bruce Hill <bruce@bruce-hill.com>
Date:   Mon May 20 19:28:47 2019 -0700

    Initial commit

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a3005d6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,40 @@
+PREFIX=
+CC=cc
+CFLAGS=-O0 -std=gnu99 -g
+LIBS=-ltermbox
+NAME=bb
+
+all: $(NAME)
+
+clean:
+	rm $(NAME)
+
+$(NAME): $(NAME).c
+	$(CC) $(NAME).c $(LIBS) $(CFLAGS) -o $(NAME)
+
+test: $(NAME)
+	./$(NAME) test.xml
+	
+install: $(NAME)
+	@prefix="$(PREFIX)"; \
+	if [[ ! $$prefix ]]; then \
+		read -p $$'\033[1mWhere do you want to install? (default: /usr/local) \033[0m' prefix; \
+	fi; \
+	if [[ ! $$prefix ]]; then \
+		prefix="/usr/local"; \
+	fi; \
+	mkdir -pv $$prefix/bin $$prefix/share/man/man1 \
+	&& cp -v $(NAME) $$prefix/bin/ \
+	&& cp -v doc/$(NAME).1 $$prefix/share/man/man1/
+
+uninstall:
+	@prefix="$(PREFIX)"; \
+	if [[ ! $$prefix ]]; then \
+		read -p $$'\033[1mWhere do you want to uninstall from? (default: /usr/local) \033[0m' prefix; \
+	fi; \
+	if [[ ! $$prefix ]]; then \
+		prefix="/usr/local"; \
+	fi; \
+	echo "Deleting..."; \
+	rm -rvf $$prefix/bin/$(NAME) $$prefix/share/man/man1/$(NAME).1
+
diff --git a/bb.c b/bb.c
new file mode 100644
index 0000000..be3497f
--- /dev/null
+++ b/bb.c
@@ -0,0 +1,636 @@
+/*
+ * Bruce's Browser (bb)
+ * Copyright 2019 Bruce Hill
+ * Released under the MIT license
+ */
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/dir.h>
+#include <sys/errno.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "keys.h"
+
+#define MAX(a,b) ((a) < (b) ? (b) : (a))
+#define MIN(a,b) ((a) > (b) ? (b) : (a))
+#define MAX_PATH 4096
+#define writez(fd, str) write(fd, str, strlen(str))
+
+static const int SCROLLOFF = 5;
+
+static struct termios orig_termios;
+static int termfd;
+static int width, height;
+static int mouse_x, mouse_y;
+
+static void update_term_size(void)
+{
+    struct winsize sz = {0};
+    ioctl(termfd, TIOCGWINSZ, &sz);
+    width = sz.ws_col;
+    height = sz.ws_row;
+}
+
+static void init_term()
+{
+    termfd = open("/dev/tty", O_RDWR);
+    tcgetattr(termfd, &orig_termios);
+    struct termios tios;
+    memcpy(&tios, &orig_termios, sizeof(tios));
+    tios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
+                           | INLCR | IGNCR | ICRNL | IXON);
+    tios.c_oflag &= ~OPOST;
+    tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+    tios.c_cflag &= ~(CSIZE | PARENB);
+    tios.c_cflag |= CS8;
+    tios.c_cc[VMIN] = 0;
+    tios.c_cc[VTIME] = 0;
+    tcsetattr(termfd, TCSAFLUSH, &tios);
+    // xterm-specific:
+    writez(termfd, "\033[?1049h");
+    update_term_size();
+    // Initiate mouse tracking:
+    writez(termfd, "\e[?1000h\e[?1002h\e[?1015h\e[?1006h");
+}
+
+static void close_term()
+{
+    // xterm-specific:
+    writez(termfd, "\033[?1049l");
+    tcsetattr(termfd, TCSAFLUSH, &orig_termios);
+    close(termfd);
+}
+
+static void err(const char *msg, ...)
+{
+    close_term();
+    va_list args;
+    va_start(args, msg);
+    fprintf(stderr, msg, args);
+    va_end(args);
+    if (errno)
+        fprintf(stderr, "\n%s", strerror(errno));
+    fprintf(stderr, "\n");
+    _exit(1);
+}
+
+static pid_t run_cmd(int *readable_fd, int *writable_fd, const char *cmd, ...)
+{
+    int fd[2];
+    pid_t child;
+    pipe(fd);
+    if ((child = fork())) {
+        if (child == -1)
+            err("Failed to fork");
+        if (writable_fd) *writable_fd = fd[0];
+        else close(fd[0]);
+        if (readable_fd) *readable_fd = fd[1];
+        else close(fd[1]);
+    } else {
+        close(fd[0]);
+        if (writable_fd)
+            dup2(fd[1], STDIN_FILENO);
+        if (readable_fd)
+            dup2(fd[0], STDOUT_FILENO);
+        char *formatted_cmd;
+        va_list args;
+        va_start(args, cmd);
+        vasprintf(&formatted_cmd, cmd, args);
+        va_end(args);
+        if (formatted_cmd)
+            execlp("sh", "sh", "-c", formatted_cmd);
+        err("Failed to execute command: %s", formatted_cmd);
+        _exit(0);
+    }
+    return child;
+}
+
+static void term_move(int x, int y)
+{
+    static char buf[32] = {0};
+    int len = snprintf(buf, sizeof(buf), "\e[%d;%dH", y+1, x+1);
+    if (len > 0)
+        write(termfd, buf, len);
+}
+
+static int term_write(const char *str, ...)
+{
+    char buf[1024] = {0};
+    va_list args;
+    va_start(args, str);
+    int len = snprintf(buf, sizeof(buf), str, args);
+    va_end(args);
+    if (len > 0)
+        write(termfd, buf, len);
+    return len;
+
+    /*
+    char *formatted = NULL;
+    va_list args;
+    va_start(args, str);
+    int ret = asprintf(&formatted, str, args);
+    va_end(args);
+    if (!formatted)
+        err("failed to allocate for string format");
+
+    write(termfd, formatted, ret);
+    free(formatted);
+    return ret;
+    */
+}
+
+typedef struct {
+    struct dirent entry;
+    const char *path;
+    int selected : 1;
+} entry_t;
+
+static void render(const char *path, entry_t *files, size_t nfiles, int cursor, int scroll, size_t nselected)
+{
+    writez(termfd, "\e[2J\e[0;1m"); // Clear, reset color + bold
+    term_move(0,0);
+    writez(termfd, path);
+    writez(termfd, "\e[0m"); // Reset color
+
+    char fullpath[MAX_PATH];
+    size_t pathlen = strlen(path);
+    strncpy(fullpath, path, pathlen + 1);
+    for (int i = scroll; i < scroll + height - 3 && i < nfiles; i++) {
+        int x = 0;
+        int y = i - scroll + 1;
+        term_move(x, y);
+
+        // Selection box:
+        if (files[i].selected)
+            writez(termfd, "\e[43m \e[0m"); // Yellow BG
+        else
+            writez(termfd, " ");
+
+        if (i != cursor && files[i].entry.d_type & DT_DIR) {
+            writez(termfd, "\e[34m"); // Blue FG
+        }
+        if (i == cursor) {
+            writez(termfd, "\e[7m"); // Reverse color
+        }
+
+        // Filesize:
+        struct stat info;
+        fullpath[pathlen] = '/';
+        strncpy(fullpath + pathlen + 1, files[i].entry.d_name, files[i].entry.d_namlen);
+        fullpath[pathlen + 1 + files[i].entry.d_namlen] = '\0';
+        lstat(fullpath, &info);
+
+        int j = 0;
+        const char* units[] = {"B", "K", "M", "G", "T", "P", "E", "Z", "Y"};
+        int bytes = info.st_size;
+        while (bytes > 1024) {
+            bytes /= 1024;
+            j++;
+        }
+        //term_write("%10.*f%s  ", j, bytes, units[j]);
+
+        // Date:
+        char buf[64];
+        strftime(buf, sizeof(buf), "%l:%M%p %b %e %Y", localtime(&(info.st_mtime)));
+        writez(termfd, buf);
+        writez(termfd, "  ");
+
+        // Name:
+        write(termfd, files[i].entry.d_name, files[i].entry.d_namlen);
+
+        if (files[i].entry.d_type & DT_DIR) {
+            writez(termfd, "/");
+        }
+        if (files[i].entry.d_type == DT_LNK) {
+            char linkpath[MAX_PATH] = {0};
+            ssize_t pathlen;
+            if ((pathlen = readlink(files[i].entry.d_name, linkpath, sizeof(linkpath))) < 0)
+                err("readlink() failed");
+            writez(termfd, "\e[36m -> "); // Cyan FG
+            write(termfd, linkpath, pathlen);
+        }
+        writez(termfd, "\e[0m"); // Reset color and attributes
+    }
+
+    term_move(0, height - 1);
+    char buf[32] = {0};
+    int len = snprintf(buf, sizeof(buf), "%lu selected", nselected);
+    write(termfd, buf, len);
+}
+
+static int compare_alpha(const void *v1, const void *v2)
+{
+    const entry_t *f1 = (const entry_t*)v1, *f2 = (const entry_t*)v2;
+    int diff;
+    diff = (f1->entry.d_type & DT_DIR) - (f2->entry.d_type & DT_DIR);
+    if (diff) return -diff;
+    const char *p1 = f1->entry.d_name, *p2 = f2->entry.d_name;
+    while (*p1 && *p2) {
+        int diff = (*p1 - *p2);
+        if ('0' <= *p1 && *p1 <= '9' && '0' <= *p2 && *p2 <= '9') {
+            long n1 = strtol(p1, (char**)&p1, 10);
+            long n2 = strtol(p2, (char**)&p2, 10);
+            diff = ((p1 - f1->entry.d_name) - (p2 - f2->entry.d_name)) || (n1 - n2);
+            if (diff) return diff;
+        } else if (diff) {
+            return diff;
+        } else {
+            ++p1, ++p2;
+        }
+    }
+    return *p1 - *p2;
+}
+
+static void write_selection(int fd, entry_t *selected, size_t nselected, size_t ndeselected)
+{
+    for (int i = 0; i < nselected + ndeselected; i++) {
+        if (!selected[i].selected)
+            continue;
+
+        const char *p = selected[i].path;
+        while (*p) {
+            const char *p2 = strchr(p, '\n');
+            if (!p2) p2 = p + strlen(p) + 1;
+            write(fd, p, p2 - p);
+            if (*p2 == '\n')
+                write(fd, "\\", 1);
+            p = p2;
+        }
+
+        write(fd, "/", 1);
+
+        p = selected[i].entry.d_name;
+        while (*p) {
+            const char *p2 = strchr(p, '\n');
+            if (!p2) p2 = p + strlen(p) + 1;
+            write(fd, p, p2 - p);
+            if (*p2 == '\n')
+                write(fd, "\\", 1);
+            p = p2;
+        }
+        write(fd, "\n", 1);
+    }
+}
+
+static int term_get_event()
+{
+    char c;
+    if (read(termfd, &c, 1) != 1)
+        return -1;
+    switch (c) {
+        case '\x1b':
+            if (read(termfd, &c, 1) != 1)
+                return -1;
+            switch (c) {
+                case '[':
+                    if (read(termfd, &c, 1) != 1)
+                        return -1;
+                    switch (c) {
+                        case 'H': return KEY_HOME;
+                        case 'F': return KEY_END;
+                        case 'M':
+                            {
+                                char buf[7] = {0};
+                                if (read(termfd, buf, 6) != 6)
+                                    return -1;
+                                unsigned char buttons, x, y;
+                                if (sscanf(buf, "%c%c%c", &buttons, &x, &y) != 3)
+                                    return -1;
+
+                                mouse_x = (int)x - 32, mouse_y = (int)y - 32;
+                                switch (buttons) {
+                                    case 0: return KEY_MOUSE_LEFT;
+                                    case 1: return KEY_MOUSE_RIGHT;
+                                    case 2: return KEY_MOUSE_MIDDLE;
+                                    case 3: return KEY_MOUSE_RELEASE;
+                                    default: return -1;
+                                }
+                            }
+                            break;
+                        default:
+                            break;
+                    }
+                    return '\x1b';
+                default:
+                    return '\x1b';
+            }
+            break;
+        default:
+            return c;
+    }
+    return -1;
+}
+
+static void explore(const char *path)
+{
+    static entry_t *selected;
+    static size_t nselected = 0, ndeselected = 0, selectedcapacity = 0;
+    char to_select[MAX_PATH] = {0};
+    char _path[MAX_PATH];
+  tail_call:;
+    DIR *dir = opendir(path);
+    if (!dir)
+        err("Couldn't open dir: %s", path);
+    if (chdir(path) != 0)
+        err("Couldn't chdir into %s", path);
+    struct dirent *dp;
+
+    size_t filecap = 0, nfiles = 0;
+    entry_t *files = NULL;
+
+    // Hash inode -> inode with linear probing
+    size_t hashsize = 2 * nselected;
+    ino_t *selecthash = calloc(hashsize, sizeof(ino_t));
+    if (!selecthash)
+        err("Failed to allocate %d spaces for selecthash", hashsize);
+    for (int i = nselected + ndeselected - 1; i >= 0; i--) {
+        if (!selected[i].selected) continue;
+        ino_t inode = selected[i].entry.d_ino;
+        int probe = ((int)inode) % hashsize;
+        while (selecthash[probe])
+            probe = (probe + 1) % hashsize;
+        selecthash[probe] = inode;
+    }
+
+    while ((dp = readdir(dir)) != NULL) {
+        if (dp->d_name[0] == '.' && dp->d_name[1] == '\0')
+            continue;
+        if (nfiles >= filecap) {
+            filecap += 100;
+            if ((files = realloc(files, sizeof(entry_t)*filecap)) == NULL)
+                err("Alloc fail");
+        }
+        int selected = 0;
+        if (nselected) {
+            for (int probe = ((int)dp->d_ino) % hashsize; selecthash[probe]; probe = (probe + 1) % hashsize) {
+                if (selecthash[probe] == dp->d_ino) {
+                    selected = 1;
+                    break;
+                }
+            }
+        }
+        entry_t file = {*dp, path, selected};
+        files[nfiles++] = file;
+    }
+    free(selecthash);
+    if (nfiles == 0) {
+        err("No files found (not even '..')");
+    }
+    qsort(files, nfiles, sizeof(entry_t), compare_alpha);
+    closedir(dir);
+
+    int cursor = 0;
+    int scroll = 0;
+
+    if (to_select[0]) {
+        for (int i = 0; i < nfiles; i++) {
+            if (strcmp(to_select, files[i].entry.d_name) == 0) {
+                cursor = i;
+                break;
+            }
+        }
+    }
+
+    struct timespec lastclick;
+    clock_gettime(CLOCK_MONOTONIC, &lastclick);
+    int picked, scrolloff;
+
+    while (1) {
+      redraw:
+        render(path, files, nfiles, cursor, scroll, nselected);
+      skip_redraw:
+        scrolloff = MIN(SCROLLOFF, height/2);
+        //sleep(2);
+        //if (1) goto done;
+        switch (term_get_event()) {
+            case KEY_MOUSE_LEFT: {
+                struct timespec clicktime;
+                clock_gettime(CLOCK_MONOTONIC, &clicktime);
+                double dt_ms = 1e3*(double)(clicktime.tv_sec - lastclick.tv_sec);
+                dt_ms += 1e-6*(double)(clicktime.tv_nsec - lastclick.tv_nsec);
+                lastclick = clicktime;
+                if (mouse_y > 0 && scroll + (mouse_y - 1) < nfiles) {
+                    int clicked = scroll + (mouse_y - 1);
+                    if (dt_ms > 200) {
+                    // Single click
+                        if (mouse_x == 0) {
+                            // Toggle
+                            picked = clicked;
+                            goto toggle;
+                        } else {
+                            cursor = clicked;
+                        }
+                    } else {
+                        // Double click
+                        picked = clicked;
+                        goto open_file;
+                    }
+                }
+            }
+            case KEY_ESC: case 'q': case 'Q':
+                goto done;
+            case KEY_CTRL_D:
+                cursor = MIN(nfiles - 1, cursor + (height - 3) / 2);
+                if (nfiles <= height - 3)
+                    goto redraw;
+                scroll += (height - 3)/2;
+                if (scroll > nfiles - (height - 3))
+                    scroll = nfiles - (height - 3);
+                goto redraw;
+            case KEY_CTRL_U:
+                cursor = MAX(0, cursor - (height - 3) / 2);
+                if (nfiles <= height - 3)
+                    goto redraw;
+                scroll -= (height - 3)/2;
+                if (scroll < 0)
+                    scroll = 0;
+                goto redraw;
+            case ' ': case '\r':
+                picked = cursor;
+              toggle:
+                files[picked].selected ^= 1;
+                if (files[picked].selected) {
+                    if (nselected + ndeselected + 1 > selectedcapacity) {
+                        selectedcapacity += 100;
+                        selected = realloc(selected, selectedcapacity);
+                    }
+                    selected[nselected++] = files[picked];
+                } else {
+                    // Find and destroy
+                    for (int i = nselected + ndeselected - 1; i >= 0; i--) {
+                        if (!selected[i].selected) continue;
+                        if (selected[i].entry.d_ino == files[picked].entry.d_ino) {
+                            selected[i].selected = 0;
+                            --nselected;
+                            // Leave a hole to clean up later
+                            if (i == nselected + ndeselected) {
+                                goto redraw;
+                            }
+                            ++ndeselected;
+                            goto found_it;
+                        }
+                    }
+                    err("Didn't find selection");
+                  found_it:
+                    // Coalesce removals:
+                    if (selectedcapacity > nselected + 100) {
+                        entry_t *first = &selected[0];
+                        entry_t *last = &selected[nselected + ndeselected - 1];
+                        entry_t *p = first;
+                        while (first != last) {
+                            if (first->selected) {
+                                *p = *first;
+                                ++p;
+                            }
+                            ++first;
+                        }
+                        ndeselected = 0;
+
+                        selectedcapacity = nselected + 100;
+                        selected = realloc(selected, selectedcapacity);
+                    }
+                }
+                goto redraw;
+            case 'j':
+                if (cursor >= nfiles - 1)
+                    goto skip_redraw;
+                ++cursor;
+                if (cursor > scroll + height - 4 - scrolloff && scroll < nfiles - (height - 3)) {
+                    ++scroll;
+                }
+                goto redraw;
+            case 'k':
+                if (cursor <= 0)
+                    goto skip_redraw;
+                --cursor;
+                if (cursor < scroll + scrolloff && scroll > 0) {
+                    --scroll;
+                }
+                goto redraw;
+            case 'J':
+                if (cursor < nfiles - 1) {
+                    ++cursor;
+                    files[cursor].selected = files[cursor - 1].selected;
+                }
+                goto redraw;
+            case 'K':
+                if (cursor > 0) {
+                    --cursor;
+                    files[cursor].selected = files[cursor + 1].selected;
+                }
+                goto redraw;
+            case 'h':
+                if (strcmp(path, "/") != 0) {
+                    char *p = strrchr(path, '/');
+                    if (p) strcpy(to_select, p+1);
+                    else to_select[0] = '\0';
+                    char tmp[MAX_PATH];
+                    strcpy(tmp, path);
+                    strcat(tmp, "/");
+                    strcat(tmp, "..");
+                    if (!realpath(tmp, _path))
+                        err("realpath failed");
+                    path = _path;
+                    goto tail_call;
+                }
+                break;
+            case 'l':
+                picked = cursor;
+              open_file:
+                {
+                    int is_dir = files[picked].entry.d_type & DT_DIR;
+                    if (files[picked].entry.d_type == DT_LNK) {
+                        char linkpath[MAX_PATH];
+                        if (readlink(files[picked].entry.d_name, linkpath, sizeof(linkpath)) < 0)
+                            err("readlink() failed");
+                        DIR *dir = opendir(linkpath);
+                        if (dir) {
+                            is_dir = 1;
+                            if (closedir(dir) < 0)
+                                err("Failed to close directory: %s", linkpath);
+                        }
+                    }
+                    if (is_dir) {
+                        if (strcmp(files[picked].entry.d_name, "..") == 0) {
+                            char *p = strrchr(path, '/');
+                            if (p) strcpy(to_select, p+1);
+                            else to_select[0] = '\0';
+                        } else to_select[0] = '\0';
+                        char tmp[MAX_PATH];
+                        strcpy(tmp, path);
+                        strcat(tmp, "/");
+                        strcat(tmp, files[picked].entry.d_name);
+                        if (!realpath(tmp, _path))
+                            err("realpath failed");
+                        path = _path;
+                        goto tail_call;
+                    } else {
+                        char *name = files[picked].entry.d_name;
+                        close_term();
+                        pid_t child = run_cmd(NULL, NULL,
+#ifdef __APPLE__
+                                "if file -bI %s | grep '^text/'; then $EDITOR %s; else open %s; fi",
+#else
+                                "if file -bi %s | grep '^text/'; then $EDITOR %s; else xdg-open %s; fi",
+#endif
+                                name, name, name);
+                        waitpid(child, NULL, 0);
+                        init_term();
+                        goto redraw;
+                    }
+                    break;
+                }
+            case 'm':
+                if (nselected) {
+                    int fd;
+                    run_cmd(NULL, &fd, "xargs mv");
+                    write_selection(fd, selected, nselected, ndeselected);
+                    close(fd);
+                }
+                break;
+            case 'd':
+                if (nselected) {
+                    int fd;
+                    run_cmd(NULL, &fd, "xargs rm -rf");
+                    write_selection(fd, selected, nselected, ndeselected);
+                    close(fd);
+                }
+                break;
+            case 'p':
+                if (nselected) {
+                    int fd;
+                    run_cmd(NULL, &fd, "xargs cp");
+                    write_selection(fd, selected, nselected, ndeselected);
+                    close(fd);
+                }
+                break;
+            default:
+                goto skip_redraw;
+        }
+        goto skip_redraw;
+    }
+done:
+    close_term();
+    write_selection(STDOUT_FILENO, selected, nselected, ndeselected);
+    return;
+}
+
+int main(int argc, char *argv[])
+{
+    init_term();
+    char path[MAX_PATH];
+    if (!realpath(argc > 1 ? argv[1] : ".", path))
+        err("realpath failed");
+    explore(path);
+done:
+    return 0;
+}
diff --git a/keys.h b/keys.h
new file mode 100644
index 0000000..e1b3e23
--- /dev/null
+++ b/keys.h
@@ -0,0 +1,75 @@
+#define KEY_F1               (0xFFFF-0)
+#define KEY_F2               (0xFFFF-1)
+#define KEY_F3               (0xFFFF-2)
+#define KEY_F4               (0xFFFF-3)
+#define KEY_F5               (0xFFFF-4)
+#define KEY_F6               (0xFFFF-5)
+#define KEY_F7               (0xFFFF-6)
+#define KEY_F8               (0xFFFF-7)
+#define KEY_F9               (0xFFFF-8)
+#define KEY_F10              (0xFFFF-9)
+#define KEY_F11              (0xFFFF-10)
+#define KEY_F12              (0xFFFF-11)
+#define KEY_INSERT           (0xFFFF-12)
+#define KEY_DELETE           (0xFFFF-13)
+#define KEY_HOME             (0xFFFF-14)
+#define KEY_END              (0xFFFF-15)
+#define KEY_PGUP             (0xFFFF-16)
+#define KEY_PGDN             (0xFFFF-17)
+#define KEY_ARROW_UP         (0xFFFF-18)
+#define KEY_ARROW_DOWN       (0xFFFF-19)
+#define KEY_ARROW_LEFT       (0xFFFF-20)
+#define KEY_ARROW_RIGHT      (0xFFFF-21)
+#define KEY_MOUSE_LEFT       (0xFFFF-22)
+#define KEY_MOUSE_RIGHT      (0xFFFF-23)
+#define KEY_MOUSE_MIDDLE     (0xFFFF-24)
+#define KEY_MOUSE_RELEASE    (0xFFFF-25)
+#define KEY_MOUSE_WHEEL_UP   (0xFFFF-26)
+#define KEY_MOUSE_WHEEL_DOWN (0xFFFF-27)
+
+/* These are all ASCII code points below SPACE character and a BACKSPACE key. */
+#define KEY_CTRL_TILDE       0x00
+#define KEY_CTRL_2           0x00 /* clash with 'CTRL_TILDE' */
+#define KEY_CTRL_A           0x01
+#define KEY_CTRL_B           0x02
+#define KEY_CTRL_C           0x03
+#define KEY_CTRL_D           0x04
+#define KEY_CTRL_E           0x05
+#define KEY_CTRL_F           0x06
+#define KEY_CTRL_G           0x07
+#define KEY_BACKSPACE        0x08
+#define KEY_CTRL_H           0x08 /* clash with 'CTRL_BACKSPACE' */
+#define KEY_TAB              0x09
+#define KEY_CTRL_I           0x09 /* clash with 'TAB' */
+#define KEY_CTRL_J           0x0A
+#define KEY_CTRL_K           0x0B
+#define KEY_CTRL_L           0x0C
+#define KEY_ENTER            0x0D
+#define KEY_CTRL_M           0x0D /* clash with 'ENTER' */
+#define KEY_CTRL_N           0x0E
+#define KEY_CTRL_O           0x0F
+#define KEY_CTRL_P           0x10
+#define KEY_CTRL_Q           0x11
+#define KEY_CTRL_R           0x12
+#define KEY_CTRL_S           0x13
+#define KEY_CTRL_T           0x14
+#define KEY_CTRL_U           0x15
+#define KEY_CTRL_V           0x16
+#define KEY_CTRL_W           0x17
+#define KEY_CTRL_X           0x18
+#define KEY_CTRL_Y           0x19
+#define KEY_CTRL_Z           0x1A
+#define KEY_ESC              0x1B
+#define KEY_CTRL_LSQ_BRACKET 0x1B /* clash with 'ESC' */
+#define KEY_CTRL_3           0x1B /* clash with 'ESC' */
+#define KEY_CTRL_4           0x1C
+#define KEY_CTRL_BACKSLASH   0x1C /* clash with 'CTRL_4' */
+#define KEY_CTRL_5           0x1D
+#define KEY_CTRL_RSQ_BRACKET 0x1D /* clash with 'CTRL_5' */
+#define KEY_CTRL_6           0x1E
+#define KEY_CTRL_7           0x1F
+#define KEY_CTRL_SLASH       0x1F /* clash with 'CTRL_7' */
+#define KEY_CTRL_UNDERSCORE  0x1F /* clash with 'CTRL_7' */
+#define KEY_SPACE            0x20
+#define KEY_BACKSPACE2       0x7F
+#define KEY_CTRL_8           0x7F /* clash with 'BACKSPACE2' */