Last active
July 20, 2023 13:02
-
-
Save litoj/daace9d2a12c432e9f766138638e5666 to your computer and use it in GitHub Desktop.
simple parser for all terminal input with keycombo string parser + display, supports all modifiers + multiclick (no-movement based)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// compile: gcc -O2 terminput_parser.c -o input | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
// #define PRINT_POS // enables mouse position printing | |
// #define DEBUG // print the numeric representation of the key + raw received string | |
typedef u_int32_t u32; | |
// Tin = terminal input | |
typedef u32 TinEvent; | |
static unsigned FLAGS = 0; | |
enum TinFlag { | |
SEND_MOUSE = 0x1, // allow sending basic mouse events | |
SEND_MOVE = 0x2, // allow sending mouse movement/drag events (^SEND_MOUSE) | |
SEND_FOCUS = 0x4, // allow sending terminal focus received and lost events (In/Out) | |
USE_APP_MODE = 0x8, // enter/use alternate screen buffer | |
// Kitty keyboard protocol (KKP) -depending options | |
USE_RELEASE = 0x10, // whether to send release key events | |
USE_REPEAT = 0x20, // whether to send repeated key events (unwanted in games) | |
USE_SHIFTED = 0x40, // use the shifted keys, not SHIFT - useful for switching between keyboards | |
USE_BASEKEY = 0x80, // ^SHIFTED_KEYS, use standard layout mappings for modified keys | |
USE_KP = 0x100, // numpad/KP-modified keys won't show as keypad specific | |
USE_CAPS = 0x200, // convert Caps to META, otherwise ignore completely | |
USE_NUMLOCK = 0x400, // let numlock as a valid modifier, otherwise ignore completely | |
}; | |
unsigned short getTinFlags() { | |
return FLAGS; | |
} | |
/** | |
* @param flags meanings of individual bits are represented by enum TinFlag | |
* @param mode how to set flags: 0= replace, 1=toggle, 2=filter, 3=add, 4=subtract, | |
*/ | |
void setTinFlags(unsigned flags, unsigned char mode) { | |
flags = mode <= 1 ? mode ? FLAGS ^ flags : flags | |
: mode <= 3 ? mode == 2 ? FLAGS & flags : FLAGS | flags | |
: FLAGS & ~flags; | |
// basic correction, can be overcome by setting from 0 to both | |
if ((flags & SEND_MOUSE) && (flags & SEND_MOVE)) flags ^= FLAGS & (SEND_MOUSE | SEND_MOVE); | |
unsigned diff = FLAGS ^ flags; | |
if (diff & USE_APP_MODE) | |
fprintf( | |
stderr, "\033[<u\033[?1049%c\033[>%du", flags & USE_APP_MODE ? 'h' : 'l', | |
1 | (flags & (USE_RELEASE | USE_REPEAT) ? 2 : 0) | | |
(flags & (USE_SHIFTED | USE_BASEKEY) ? 4 : 0) | |
); | |
if (diff & (USE_SHIFTED | USE_BASEKEY | USE_RELEASE | USE_REPEAT)) | |
fprintf( | |
stderr, "\033[=%du", | |
1 | (flags & (USE_RELEASE | USE_REPEAT) ? 2 : 0) | | |
(flags & (USE_SHIFTED | USE_BASEKEY) ? 4 : 0) | |
); | |
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking | |
if (diff & SEND_MOUSE) fprintf(stderr, "\033[?1000%c", flags & SEND_MOUSE ? 'h' : 'l'); | |
if (diff & SEND_MOVE) fprintf(stderr, "\033[?1003%c", flags & SEND_MOVE ? 'h' : 'l'); | |
if (diff & SEND_FOCUS) fprintf(stderr, "\033[?1004%c", flags & SEND_FOCUS ? 'h' : 'l'); | |
FLAGS = flags; | |
#ifdef DEBUG | |
fprintf(stderr, "{%X}", flags); | |
#endif | |
} | |
static struct { | |
TinEvent* buff; | |
u32 n, alloc; | |
u32 first, last; | |
} inputQueue = {NULL, 0, 0, 0, 0}; | |
/// @param flags see setTinFlags | |
void initTin(unsigned flags) { | |
system("/bin/stty raw -echo"); | |
// 1006 for '<' prefix, 1016 adds pixel tracking (hidpi screens overflow at 1024/2048) | |
// 4;2m => modifyOtherKeys=2; 1u => enable Kitty full keyboard protocol | |
fprintf(stderr, "\033[?1006h\033[>4;2m\033[>1u"); | |
setTinFlags(flags, 0); | |
inputQueue.buff = (TinEvent*) malloc(sizeof(TinEvent) * (inputQueue.alloc = 8)); | |
} | |
void endTin() { | |
free(inputQueue.buff); | |
inputQueue.buff = NULL; | |
inputQueue.alloc = inputQueue.n = inputQueue.first = inputQueue.last = 0; | |
setTinFlags(0, 0); | |
fprintf(stderr, "\033[?1006l\033[>4m\033[<u"); | |
system("/bin/stty sane"); | |
} | |
enum Input : u32 { | |
KEY = 0, | |
CLICK = 1 << 30, // includes multiclick + release event if other event happened meanwhile | |
SCROLL = CLICK * 2, | |
MOVE = CLICK * 3, // move and drag | |
}; | |
#define INPUT_FILTER MOVE | |
enum Mod { | |
SHIFT = CLICK >> 1, | |
ALT = SHIFT >> 1, | |
CTRL = ALT >> 1, | |
}; | |
#define MOD_FILTER (SHIFT | ALT | CTRL) | |
enum ScrollDir { | |
UMS = 0, | |
DMS = CTRL >> 2, | |
LMS = DMS * 2, | |
RMS = DMS * 3, | |
}; | |
#define SCROLLDIR_FILTER RMS | |
enum Button { | |
NOMB = 0, | |
LMB = CTRL >> 3, | |
MMB = LMB * 2, | |
RMB = LMB * 3, | |
E1MB = LMB * 4, | |
E2MB = LMB * 5, | |
E3MB = LMB * 6, | |
E4MB = LMB * 7, | |
}; | |
#define BUTTON_FILTER E4MB | |
enum ClickCount { | |
NO_CLICK = 0, | |
SINGLE_CLICK = LMB >> 2, | |
DOUBLE_CLICK = 2 * SINGLE_CLICK, | |
TRIPLE_CLICK = 3 * SINGLE_CLICK, | |
}; | |
#define CLICKCOUNT_FILTER TRIPLE_CLICK | |
#define MOUSE_FILTER (BUTTON_FILTER | CLICKCOUNT_FILTER) | |
#define MOUSE_POS_BITS 10 // 10: 1023=max columns/rows | |
#define getMouseCol(e) (e & ((1 << MOUSE_POS_BITS) - 1)) | |
#define getMouseRow(e) getMouseCol(e >> MOUSE_POS_BITS) | |
enum KeyMod { | |
WINKEY = CTRL >> 1, | |
HYPER = WINKEY >> 1, | |
META = HYPER >> 1, | |
NUM_LOCK = META >> 1, | |
}; | |
#define KEYMOD_FILTER (MOD_FILTER | WINKEY | HYPER | META | NUM_LOCK) | |
enum KeyType { | |
KEY_PRESS = 0, | |
KEY_REPEAT = NUM_LOCK >> 2, | |
KEY_RELEASE = KEY_REPEAT * 2, | |
}; | |
#define KEYTYPE_FILTER (KEY_PRESS | KEY_REPEAT) | |
typedef enum { | |
BS = 8, // translated from 127 | |
TAB = 9, | |
CR = 13, | |
ESC = 27, | |
UTF8_END = 0x110000, | |
UP = UTF8_END + 1, // 'A' | |
DOWN = UP + 1, // 'B' | |
RIGHT = DOWN + 1, // 'C' | |
LEFT = RIGHT + 1, // 'D' | |
BEGIN = LEFT + 1, // 'E' | |
END = BEGIN + 1, // 'F' | |
HOME = END + 2, // 'H' | |
FOCUS_IN = HOME + 1, // 'I' | |
INS = FOCUS_IN + 1, // 'J' | |
DEL = INS + 1, // 'K' | |
PG_UP = DEL + 2, // 'M' | |
PG_DOWN = PG_UP + 1, // 'N' | |
FOCUS_OUT = PG_DOWN + 1, // 'O' | |
F1 = FOCUS_OUT + 1, // 'P' | |
F2 = F1 + 1, // 'Q' | |
F3 = F2 + 1, // 'R' | |
F4 = F3 + 1, // 'S' | |
F5 = F4 + 1, // 'T' | |
F6 = F5 + 1, // 'U' | |
F7 = F6 + 1, // 'V' | |
F8 = F7 + 1, // 'W' | |
F9 = F8 + 1, // 'X' | |
F10 = F9 + 1, // 'Y' | |
F11 = F10 + 1, // 'Z' | |
F12 = F11 + 1, // -- result of missing 'G' and 'L' in the terminal encoding | |
} Key; | |
#define isText(e) (' ' <= e && e < UTF8_END) | |
#define KP_TEXT_OFFSET (F12 + 1) | |
#define KP_CONTROL_OFFSET (KP_TEXT_OFFSET + '9' + 1) | |
#define KP_CONTROL_DIFF (KP_CONTROL_OFFSET - UP) | |
#define toKP(e) (e > UTF8_END ? e + KP_CONTROL_DIFF : e + KP_TEXT_OFFSET) | |
#define fromKP(e) (e >= KP_CONTROL_OFFSET ? e - KP_CONTROL_DIFF : e - KP_TEXT_OFFSET) | |
enum StatusCode : u32 { | |
INPUT_IGNORED = MOVE | NOMB | SINGLE_CLICK, | |
INPUT_SHORT = MOVE | NOMB | DOUBLE_CLICK, | |
}; | |
#define VALUE_FILTER ((1 << 21) - 1) // 21b | |
#define ATTR_FILTER ~VALUE_FILTER // 11b | |
#define nameIdx(specialKey) (specialKey + 32 - UTF8_END) | |
static const char* keyNames[] = { | |
[BS] = "BS", [TAB] = "Tab", [CR] = "CR", [ESC] = "Esc", [nameIdx(UP)] = "Up", | |
"Down", "Right", "Left", "Begin", "End", | |
"", "Home", "FocusIn", "Ins", "Del", | |
"", "PgUp", "PgDown", "FocusOut", | |
}; | |
u32 strToInt(const char** pos) { | |
u32 ret = 0; | |
const char* seq = *pos; | |
while ('0' <= *seq && *seq <= '9') ret = ret * 10 + *seq++ - '0'; | |
*pos = seq; | |
return ret; | |
} | |
static enum KeyMod parseMods(u32 x) { | |
enum KeyMod e = 0; | |
x--; | |
if ((FLAGS & USE_NUMLOCK) && (x & 128)) e |= NUM_LOCK; | |
if ((x & 32) || ((FLAGS & USE_CAPS) && (x & 64))) e |= META; | |
if (x & 16) e |= HYPER; | |
if (x & 8) e |= WINKEY; | |
if (x & 4) e |= CTRL; | |
if (x & 2) e |= ALT; | |
if (x & 1) e |= SHIFT; | |
return e; | |
} | |
static TinEvent parseMouseInput(const char** pos) { | |
const char* seq = *pos; | |
TinEvent kind = strToInt(&seq); | |
TinEvent e = 0; | |
if (kind & 4) e |= SHIFT; | |
if (kind & 8) e |= ALT; | |
if (kind & 16) e |= CTRL; | |
if (kind & 64) e |= SCROLL | DMS * (kind & 3); | |
else | |
e |= (kind & 32 ? MOVE : CLICK) | | |
LMB * | |
(kind & 128 ? 4 | (kind & 3) // extra btn | |
: (kind & 3) != 3 ? 1 + (kind & 3) // normal btn | |
: 0); // no btn | |
if (*seq++ != ';') return seq[-1] ? 0 : INPUT_SHORT; | |
e |= strToInt(&seq); | |
if (*seq++ != ';') return seq[-1] ? 0 : INPUT_SHORT; | |
e |= strToInt(&seq) << MOUSE_POS_BITS; | |
if (*seq != 'm' && *seq != 'M') return *seq ? 0 : INPUT_SHORT; | |
*pos = 1 + seq; | |
if (*seq != 'M' && (e & INPUT_FILTER) != CLICK) return INPUT_IGNORED; | |
static TinEvent last = 0; | |
static enum ClickCount clickCount = NO_CLICK; | |
if ((e & INPUT_FILTER) == CLICK) { | |
if (e == last) { | |
if (*seq == 'm') return INPUT_IGNORED; // send MouseRelease only when other events occured | |
if (clickCount < TRIPLE_CLICK) clickCount += SINGLE_CLICK; | |
else clickCount = SINGLE_CLICK; // unmoved-cursor-based multiclick detection | |
} else clickCount = SINGLE_CLICK; | |
if (*seq == 'm') return e; | |
else return (last = e) | clickCount; | |
} else { | |
last = 0; | |
return (e & INPUT_FILTER) == MOVE && (e & BUTTON_FILTER) != NOMB ? e | SINGLE_CLICK : e; | |
} | |
} | |
TinEvent parseUtf8(const char** pos) { | |
const char* seq = *pos; | |
int left = 1; | |
TinEvent k = *seq++ ^ -128; | |
for (TinEvent i = 64; k & i; i >>= 1, left++) k ^= i; | |
if (left == 1 || left > 4) | |
while (*seq++ < 0) {} | |
else | |
while (--left && (*seq >> 6) == -2) k = (k << 6) + (*seq++ ^ -128); | |
*pos = seq; | |
return left ? INPUT_IGNORED : k; | |
} | |
static TinEvent parseCharInput(const char** pos) { | |
if (**pos >= 0) { | |
const char* seq = (*pos)++; | |
if (' ' <= *seq) return *seq == 127 ? BS : *seq; | |
if (keyNames[(unsigned char) *seq]) return *seq == 8 ? CTRL | BS : *seq; | |
else return CTRL | (*seq ? *seq | 96 : ' '); | |
} else return parseUtf8(pos); | |
} | |
static TinEvent parseKeypadInput(TinEvent e) { | |
if (e < 57409) e = e - 57399 + '0'; | |
else if (e <= 57426) { | |
Key val[] = {',', '/', '*', '-', '+', CR, '?', '?', LEFT, | |
RIGHT, UP, DOWN, PG_UP, PG_DOWN, HOME, END, INS, DEL}; | |
e = val[e - 57409]; | |
} | |
return FLAGS & USE_KP ? toKP(e) : e; | |
} | |
/// @param overFill if sequence was possibly trimmed by previous buffer overfill | |
static TinEvent parseRaw(const char** pos, unsigned char len, unsigned char overFill) { | |
#ifdef DEBUG | |
fprintf(stderr, "("); | |
for (const char* x = *pos; *x; x++) { | |
if (*x < 0) fprintf(stderr, "\\%d", (unsigned char) *x); | |
else if (*x < ' ') fprintf(stderr, "^%c", *x + 64); | |
else fprintf(stderr, "%c", *x); | |
} | |
fprintf(stderr, ")"); | |
#endif | |
const char* seq = *pos; | |
TinEvent e = 0; | |
if (*seq != 27) e = parseCharInput(pos); | |
else if ((!overFill && len == 1) || seq[1] == 27) e = *(*pos)++; | |
else if (seq[1] == 'O') { // Arrows | |
if (!seq[2]) return INPUT_SHORT; | |
*pos += 3; | |
e = seq[2] + UP - 'A'; | |
} else if (seq[1] == '[') { // CSI | |
if (!seq[2]) return INPUT_SHORT; // breaks <A-[> when terminal ignores \[>4;2m | |
if (seq[2] == '<') { // Mouse input - click/hover/drag/scroll | |
*pos += 3; | |
e = parseMouseInput(pos); | |
} else if (seq[2]) { | |
const char* at = seq + (seq[2] == '-' ? 3 : 2); | |
e = strToInt(&at); | |
if (*at == ':') { // KKP - shifted key | |
if (*++at != ':' && (FLAGS & USE_SHIFTED)) e = strToInt(&at); | |
else strToInt(&at); | |
if (*at == ':') { // KKP standard/base layout key (unshifted) | |
at++; | |
if (FLAGS & USE_BASEKEY) e = strToInt(&at); | |
else strToInt(&at); | |
} | |
} | |
TinEvent attr = 0; | |
if (*at == ';') { | |
at++; | |
attr = parseMods(strToInt(&at)); | |
if (*at == ':') { // KKP key state - press/repeat/release | |
at++; | |
char s = strToInt(&at); | |
if (s == 2 && (FLAGS & USE_REPEAT)) attr |= KEY_REPEAT; | |
else if (s == 3 && (FLAGS & USE_RELEASE)) attr |= KEY_RELEASE; | |
else if (s != 1) { | |
*pos = 1 + at; | |
return *at ? INPUT_IGNORED : INPUT_SHORT; | |
} | |
} | |
} | |
if (!*at) return INPUT_SHORT; | |
*pos = 1 + at; | |
char end = *at; | |
if ('A' <= end && end <= 'Z') { // Arrows/Tab/F1-F4 | |
e = attr | (end == 'Z' ? TAB : end + UP - 'A'); | |
} else if (end == 'u') { // KKP | |
if (seq[2] == '-') { // (neo)vim weird horizontal scroll (with mods) handling | |
if ((e & VALUE_FILTER) == 19965) e = attr | SCROLL | RMS; | |
else if ((e & VALUE_FILTER) == 20221) e = attr | SCROLL | LMS; | |
else e = INPUT_IGNORED; | |
} else if (57399 <= e && e <= 57426) e = parseKeypadInput(e) | attr; | |
else if (e == 127) e = BS | attr; | |
else if (e < ' ' || e >= UTF8_END || !(FLAGS & USE_SHIFTED)) e |= attr; | |
else e |= attr & ~SHIFT; | |
} else if (end == '~') { // INS-PG_DOWN, F5-F12 | |
if (e <= 6) e = e == 1 ? HOME : e == 4 ? END : INS + (seq[2] - '2'); | |
else if (11 <= e && e <= 24) e = F1 + e - (e < 16 ? 11 : e < 22 ? 12 : 13); | |
else e = INPUT_IGNORED; | |
if (e != INPUT_IGNORED) e |= attr; | |
} else if (e == 27 && end == ';') { // \033[27;mod;key~ when only \033[>4;2m is recognized | |
e = strToInt(pos); | |
if (e == 127) e = BS | attr; // always like USE_SHIFTED | |
else e |= ' ' < e && e < UTF8_END ? attr & ~SHIFT : attr; | |
if ((end = *(*pos)++) != '~') e = end ? INPUT_IGNORED : INPUT_SHORT; | |
} else e = end ? INPUT_IGNORED : INPUT_SHORT; | |
} | |
if (!e) { // breaks <A-[> when terminal ignores special mode escape sequences | |
if (len < 7) return INPUT_SHORT; | |
*pos += 2; | |
e = ALT | '['; | |
} | |
} else { | |
++*pos; | |
e = parseCharInput(pos); // always like USE_SHIFTED | |
if (e != INPUT_IGNORED) e |= ALT; | |
} | |
#ifdef DEBUG | |
// if (e != INPUT_IGNORED) fprintf(stderr, "[%u/%x] ", e, e); | |
#endif | |
return e; | |
} | |
void queueTin(TinEvent e) { | |
if (e == INPUT_IGNORED || e == INPUT_SHORT) return; | |
if (inputQueue.n == inputQueue.alloc) | |
inputQueue.buff = realloc(inputQueue.buff, sizeof(TinEvent) * (inputQueue.alloc *= 2)); | |
inputQueue.buff[inputQueue.last] = e; | |
if (++inputQueue.last == inputQueue.alloc) inputQueue.last = 0; | |
inputQueue.n++; | |
} | |
TinEvent getDirectTin() { | |
static char seq[20]; | |
static int len = 0; | |
static int overFill = 0; | |
TinEvent e; | |
do { | |
e = INPUT_SHORT; | |
const char* pos; | |
if (len) { | |
seq[len] = 0; | |
pos = seq; | |
e = parseRaw(&pos, len, overFill); | |
if (len < 0) len = -len; | |
} | |
overFill = 0; | |
while (e == INPUT_SHORT) { | |
if ((len = read(0, seq + len, sizeof(seq) - 1 - len) + len)) { | |
seq[len] = 0; | |
pos = seq; | |
e = parseRaw(&pos, len, 0); | |
} | |
} | |
if (!e && *seq) { // unknown sequence fallback - print one char to move forward | |
pos = seq; | |
e = parseCharInput(&pos); | |
} | |
int leftOver = 0; | |
const char* end = seq + len; | |
while (pos < end) seq[leftOver++] = *pos++; | |
overFill = len == sizeof(seq) - 1; // last reserved for '\0' | |
len = leftOver; | |
} while (e == INPUT_IGNORED); | |
return e; | |
} | |
TinEvent getNextTin() { | |
if (!inputQueue.n) return getDirectTin(); | |
TinEvent e = inputQueue.buff[inputQueue.first]; | |
if (++inputQueue.first == inputQueue.alloc) inputQueue.first = 0; | |
inputQueue.n--; | |
return e; | |
} | |
/// @returns `INPUT_IGNORED` on error, otherwise the input code | |
/// unknown keynames get parsed as some other key | |
TinEvent stringToCode(const char** pos) { | |
const char* str = *pos; | |
if (!str) return 0; | |
if (*str != '<' || !str[1]) return parseCharInput(pos); | |
TinEvent e = 0; | |
char m = *++str; | |
while (m && str[1] == '-') { // key/mouse modifiers/types parsing | |
if (m <= 'C') e |= m <= 'C' ? m == 'A' ? ALT : CTRL : m == '2' ? DOUBLE_CLICK : TRIPLE_CLICK; | |
else if (m <= 'R') e |= m <= 'M' ? m == 'H' ? HYPER : META : m == 'R' ? KEY_RELEASE : NUM_LOCK; | |
else e |= m == 'S' ? SHIFT : WINKEY; | |
m = *(str += 2); | |
} | |
if (!*str || !str[1] || (*str == '>' && str[1] != '>')) return INPUT_IGNORED; | |
const char* end = str + 1; | |
while (*end && *end != '>') end++; | |
if (!end) return INPUT_IGNORED; | |
int len = end - str; | |
unsigned char isKP = 0; | |
*pos = end + 1; | |
if (str[1] == '>') return *str < ' ' ? INPUT_IGNORED : e | *str; | |
else if (*str < 0) return e | parseCharInput(&str); | |
else if (*str == 'F') { // function keys | |
str++; | |
return e | (F1 - 1 + strToInt(&str)); | |
} else if (end[-2] == 'M' && end[-1] != 'E') { // Mouse (not HOME) | |
char kind = end[-1]; | |
if (kind == 'M') return e | MOVE; | |
if (kind == 'S') return e | SCROLL | (m == 'U' ? UMS : m == 'D' ? DMS : m == 'R' ? RMS : LMS); | |
if (m == 'E') e |= LMB * (str[1] - '1' + 4); // extended buttons (E1MB-E4MB) | |
else e |= m == 'L' ? LMB : m == 'R' ? RMB : MMB; | |
if (kind == 'D' || kind == 'M') return e | MOVE; | |
if (kind == 'R' || (e & CLICKCOUNT_FILTER)) return e | CLICK; // release or multiclick | |
else return e | CLICK | SINGLE_CLICK; | |
} else if (*str == 'k') { // keypad | |
len--; | |
str++; | |
isKP = 1; | |
} | |
// should use a tree/hashmap here, but this is much faster | |
if (len == 2) e |= *str < 'E' ? *str == 'C' ? CR : BS : *str == 'l' ? '<' : UP; | |
else if (len == 3) | |
e |= *str == 'E' ? str[1] == 's' ? ESC : END : *str == 'I' ? INS : *str == 'D' ? DEL : TAB; | |
else if (len == 4) e |= *str == 'D' ? DOWN : *str == 'H' ? HOME : PG_UP; | |
else if (len == 5) e |= BEGIN; | |
else e |= PG_DOWN; | |
return isKP ? toKP(e) : e; | |
} | |
typedef struct { | |
char* data; | |
TinEvent len; | |
TinEvent alloc; | |
} String; | |
void add(char c, String* str) { | |
if (str->alloc <= str->len + 1) str->data = realloc(str->data, sizeof(char) * (str->alloc *= 2)); | |
str->data[str->len++] = c; | |
} | |
void appendUtf8(Key k, String* str) { | |
int extra = k > 0xffff ? 3 : k > 0x7ff ? 2 : 1; | |
int first = 128; | |
for (int i = extra; i; i--) first |= 1 << (7 - i); | |
add(first | k >> (extra * 6), str); | |
while (extra--) add(128 | ((k >> (extra * 6)) & 63), str); | |
} | |
void append(const char* text, String* str) { | |
if (str->len && !str->data[str->len - 1]) str->len--; | |
do add(*text++, str); | |
while (*text); | |
} | |
void appendNum(u32 n, String* str) { | |
TinEvent order = 10; | |
while (n >= order) order *= 10; | |
do { | |
add((n % order) / (order / 10) + '0', str); | |
order /= 10; | |
} while (order > 1); | |
} | |
static void appendKeyEvent(Key k, String* str) { | |
if (k >= KP_TEXT_OFFSET) { | |
add('k', str); | |
k -= k < KP_CONTROL_OFFSET ? KP_TEXT_OFFSET : KP_CONTROL_DIFF; | |
} | |
if (k < 128) { | |
if (k < ' ' && keyNames[k]) append(keyNames[k], str); | |
// else if (k == '<') append("lt", str); | |
else add(k, str); | |
} else if (k < 0x110000) appendUtf8(k, str); | |
else if (nameIdx(k) < sizeof(keyNames) / sizeof(char*)) append(keyNames[nameIdx(k)], str); | |
else if (k <= F12) { | |
add('F', str); | |
appendNum(k - F1 + 1, str); | |
} | |
} | |
static void appendMouseEvent(enum Input e, String* str) { | |
u32 v = e & BUTTON_FILTER; | |
if (e & CLICK) { | |
if (!v) append("MM", str); // mouse movement | |
else { | |
enum ClickCount cc = e & CLICKCOUNT_FILTER; | |
if (cc > SINGLE_CLICK) append(cc == DOUBLE_CLICK ? "2-" : "3-", str); | |
if (v < E1MB) { | |
add(v == LMB ? 'L' : v == MMB ? 'M' : 'R', str); // left, middle, right | |
} else { | |
add('E', str); // extra mouse buttons | |
add(v / LMB - 3 + '0', str); | |
} | |
add('M', str); | |
add(e & SCROLL ? 'D' : cc ? 'C' : 'R', str); // drag, click, release | |
} | |
} else { | |
add(v == UMS ? 'U' : v == DMS ? 'D' : v == LMS ? 'L' : 'R', str); | |
append("MS", str); // mouse scroll | |
} | |
#ifdef PRINT_POS | |
add('(', str); | |
appendNum((e & MOUSE_ROW_FILTER) >> MOUSE_POS_BITS, str); | |
add(';', str); | |
appendNum(e & MOUSE_COL_FILTER, str); | |
add(')', str); | |
#endif | |
} | |
void appendInputEvent(TinEvent e, String* str) { | |
if (e < 32 || e > UTF8_END || e == '<') add('<', str); | |
if (e & INPUT_FILTER) { | |
if (e & MOD_FILTER) { | |
if (e & CTRL) append("C-", str); | |
if (e & SHIFT) append("S-", str); | |
if (e & ALT) append("A-", str); | |
} | |
appendMouseEvent(e, str); | |
} else { | |
if (e & KEYMOD_FILTER) { | |
if (e & NUM_LOCK) append("N-", str); | |
if (e & META) append("M-", str); | |
if (e & HYPER) append("H-", str); | |
if (e & WINKEY) append("W-", str); | |
if (e & CTRL) append("C-", str); // cannot join because of order | |
if (e & ALT) append("A-", str); | |
if (e & SHIFT) append("S-", str); | |
if (e & KEY_RELEASE) append("R-", str); | |
else if (e & KEY_REPEAT) append("r-", str); // not user-mappable, only programmatically | |
} | |
appendKeyEvent(e & VALUE_FILTER, str); | |
} | |
if (e < 32 || e > UTF8_END || e == '<') add('>', str); | |
} | |
String* mkStr(u32 buff) { | |
String* ret = malloc(sizeof(String)); | |
ret->data = malloc(sizeof(char) * buff); | |
ret->len = 0; | |
ret->alloc = buff; | |
return ret; | |
} | |
void freeStr(String* str) { | |
if (str->data != NULL) free(str->data); | |
free(str); | |
} | |
String* codeToString(TinEvent e) { | |
String* str = mkStr(4); | |
appendInputEvent(e, str); | |
add('\0', str); | |
return str; | |
} | |
void printInputCode(TinEvent e) { | |
static String str = {NULL, 0, 0}; | |
if (e == INPUT_IGNORED && str.data != NULL) { | |
free(str.data); | |
str.data = NULL; | |
} else { | |
if (str.data == NULL) { | |
str.data = malloc(sizeof(char) * 4); | |
str.alloc = 4; | |
} | |
str.len = 0; | |
appendInputEvent(e, &str); // doesn't append '\0' | |
write(1, str.data, str.len); | |
} | |
} | |
static int handleStringToKeyMode(TinEvent e) { | |
static String buff = {NULL, 0, 0}; | |
if (buff.data == NULL) { | |
buff.data = malloc(sizeof(char) * 4); | |
buff.alloc = 4; | |
} | |
if (isText(e)) { | |
String evStr = {malloc(sizeof(char) * 1), 0, 1}; | |
appendKeyEvent(e, &evStr); | |
write(1, evStr.data, evStr.len); | |
add('\0', &evStr); | |
append(evStr.data, &buff); | |
free(evStr.data); | |
} else if (e == BS) { | |
if (buff.len) buff.len--; // TODO | |
} else if (e == (ALT | 'i')) { | |
add('\0', &buff); | |
printf("\r\n%s\r\n", buff.data); | |
buff.data--; | |
} else if (e == CR) { | |
printf("\r\n"); | |
if (!buff.len) return 2; | |
add('\0', &buff); | |
const char* tmp = buff.data; | |
while (*tmp) { | |
TinEvent code = stringToCode(&tmp); | |
if (code == INPUT_IGNORED) { | |
fprintf(stderr, "\033[31mkeycombo invalid\033[0m\r\n"); | |
buff.len = tmp - buff.data; | |
return 2; | |
} else queueTin(code); | |
} | |
buff.len--; | |
} else if (e == (CTRL | BS)) { | |
while (buff.len && buff.data[--buff.len] != ' ') {} | |
} else if (e == TAB) { | |
setTinFlags(USE_APP_MODE, 1); | |
return 1; | |
} else if (e == ESC) { | |
setTinFlags(USE_APP_MODE, 1); | |
free(buff.data); | |
buff.data = NULL; | |
return 1; | |
} else { | |
write(1, "\rno binding for: (\033[31m", 23); | |
printInputCode(e); | |
write(1, "\033[m)\r\n", 6); | |
} | |
return 2; | |
} | |
static int handleInputParsingMode(TinEvent e) { | |
static int separated = 0; | |
if (e == CR || (separated && !isText(e))) printf("\r\n"); | |
if (e == (ALT | CR)) separated = !separated; | |
else if (e == ESC || e == (CTRL | 'q')) { | |
printf("\033[1;1H\033[2J"); | |
return 0; | |
} else if (e == (SHIFT | ESC)) return 0; | |
else if (e == TAB) { | |
setTinFlags(USE_APP_MODE, 1); | |
return 2; | |
} | |
printInputCode(e); | |
return 1; | |
} | |
void printFlags() { | |
printf( | |
"\0337\033[H" // (re)set cursor | |
"Btn/Move Focus Rel/Rep Shifted/Base KP/Caps/NumL\r\n" | |
"( 0,1)=%d (2)=%d ( 3,4)=%d ( 5 ,6)=%d (7, 8 ,9)=%d\r\n", | |
FLAGS & 3, (FLAGS & SEND_FOCUS) / SEND_FOCUS, | |
(FLAGS & (USE_RELEASE | USE_REPEAT)) / USE_RELEASE, | |
(FLAGS & (USE_SHIFTED | USE_BASEKEY)) / USE_SHIFTED, | |
(FLAGS & (USE_KP | USE_CAPS | USE_NUMLOCK)) / USE_KP | |
); | |
fprintf(stderr, "\0338"); | |
} | |
int main(int argc, char* argv[]) { | |
initTin(SEND_MOVE | SEND_FOCUS | USE_KP); | |
printFlags(); | |
printf("\n"); | |
int mode = 1; | |
while (mode) { | |
TinEvent e = getNextTin(); | |
if (e == (CTRL | 'l')) { | |
printf("\033[1;1H\033[2J"); | |
printFlags(); | |
printf("\n\n"); | |
} | |
if ((ALT | '0') <= e && e <= (ALT | '9')) { | |
unsigned f = (e ^ ALT) - '0'; | |
setTinFlags(1 << (f < 3 ? f : f + 1), 1); | |
printFlags(); | |
} else if ((e & ATTR_FILTER) == (CLICK | LMB | SINGLE_CLICK) && (getMouseRow(e) <= 2)) { | |
int box[][2] = {{1, 3}, {5, 8}, {10, 14}, {16, 18}, {20, 22}, | |
{25, 31}, {33, 36}, {38, 39}, {41, 44}, {46, 49}}; | |
int col = getMouseCol(e); | |
for (int i = 0; i < 10; i++) { | |
if (box[i][0] <= col && col <= box[i][1]) { | |
setTinFlags(1 << (i < 3 ? i : i + 1), 1); | |
printFlags(); | |
break; | |
} | |
} | |
} else if (mode == 2) mode = handleStringToKeyMode(e); | |
else mode = handleInputParsingMode(e); | |
} | |
printInputCode(INPUT_IGNORED); | |
endTin(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment