summaryrefslogtreecommitdiff
path: root/src/input.c
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /src/input.c
term: move the term core to subprojects/term
The core now builds, tests, and benchmarks alone, as a Meson subproject with no dependency on the main tree. - include/term/ holds term.h and the header-only unicode/. src/ holds the core and its private headers. The main tree gets only include/term through term_dep, so it cannot reach a private header. - The term tests, the term benchmarks, the Kitty parser suite, and tools/amalgamate.py and tools/generate_unicode.py move with it. The tests run as term:<name>. - The main build passes its -march (arch_flag) and b_ndebug to the subproject. A directory configured before this commit needs meson configure -Dterm:arch_flag=-march=... once. - The term tests compile with -UNDEBUG. They do work inside assert. - The subproject exports term_test_dep with term_api.h, for the main tree's render, thumbnail, color, and selection tests. - The Kitty bridge used struct vector2i and has not compiled since the public API redesign. It builds again. 2 of its 14 tests fail: REP after a tab, and a cursor report after a resize. - dirgraph.py drops the term and unicode layers. Its self-test uses pty. - The symbols test ignores the __odr_asan symbols that ASan adds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/input.c')
-rw-r--r--src/input.c771
1 files changed, 771 insertions, 0 deletions
diff --git a/src/input.c b/src/input.c
new file mode 100644
index 0000000..49e41b2
--- /dev/null
+++ b/src/input.c
@@ -0,0 +1,771 @@
+#include "term.h"
+#include "unicode/utf8.h"
+
+#include <string.h>
+
+struct seqbuf {
+ char *p;
+ size_t left;
+ bool overflow;
+};
+
+static inline void sb_init(struct seqbuf *sb, char *buf, size_t buf_sz)
+{
+ sb->p = buf;
+ sb->left = buf_sz;
+ sb->overflow = false;
+}
+
+static inline void sb_ch(struct seqbuf *sb, char c)
+{
+ if (!sb->left) { sb->overflow = true; return; }
+ *sb->p++ = c;
+ sb->left--;
+}
+
+static inline void sb_u32(struct seqbuf *sb, uint32_t val)
+{
+ char tmp[10];
+ size_t n = 0;
+ do {
+ tmp[n++] = (char)('0' + (val % 10));
+ val /= 10;
+ } while (val);
+ while (n) sb_ch(sb, tmp[--n]);
+}
+
+static inline void sb_str(struct seqbuf *sb, const char *s, size_t len)
+{
+ for (size_t i = 0; i < len; i++) sb_ch(sb, s[i]);
+}
+
+static inline size_t sb_len(const struct seqbuf *sb, const char *buf)
+{
+ return (size_t)(sb->p - buf);
+}
+
+static inline void sb_bytes(struct seqbuf *sb, const char *s, size_t len)
+{
+ if (len > sb->left) { sb->overflow = true; return; }
+ memcpy(sb->p, s, len);
+ sb->p += len;
+ sb->left -= len;
+}
+
+struct kitty_desc {
+ uint32_t code;
+ char final;
+};
+
+static const struct kitty_desc kitty_desc[TERM_KEY_COUNT] = {
+ [TERM_KEY_RETURN] = { 13, 'u' },
+ [TERM_KEY_BACKSPACE] = { 127, 'u' },
+ [TERM_KEY_TAB] = { 9, 'u' },
+ [TERM_KEY_ESCAPE] = { 27, 'u' },
+
+ [TERM_KEY_UP] = { 1, 'A' },
+ [TERM_KEY_DOWN] = { 1, 'B' },
+ [TERM_KEY_RIGHT] = { 1, 'C' },
+ [TERM_KEY_LEFT] = { 1, 'D' },
+ [TERM_KEY_HOME] = { 1, 'H' },
+ [TERM_KEY_END] = { 1, 'F' },
+ [TERM_KEY_PAGE_UP] = { 5, '~' },
+ [TERM_KEY_PAGE_DOWN] = { 6, '~' },
+ [TERM_KEY_INSERT] = { 2, '~' },
+ [TERM_KEY_DELETE] = { 3, '~' },
+
+ [TERM_KEY_F1] = { 1, 'P' },
+ [TERM_KEY_F2] = { 1, 'Q' },
+ [TERM_KEY_F3] = { 13, '~' },
+ [TERM_KEY_F4] = { 1, 'S' },
+ [TERM_KEY_F5] = { 15, '~' },
+ [TERM_KEY_F6] = { 17, '~' },
+ [TERM_KEY_F7] = { 18, '~' },
+ [TERM_KEY_F8] = { 19, '~' },
+ [TERM_KEY_F9] = { 20, '~' },
+ [TERM_KEY_F10] = { 21, '~' },
+ [TERM_KEY_F11] = { 23, '~' },
+ [TERM_KEY_F12] = { 24, '~' },
+ [TERM_KEY_F13] = { 57376, 'u' },
+ [TERM_KEY_F14] = { 57377, 'u' },
+ [TERM_KEY_F15] = { 57378, 'u' },
+ [TERM_KEY_F16] = { 57379, 'u' },
+ [TERM_KEY_F17] = { 57380, 'u' },
+ [TERM_KEY_F18] = { 57381, 'u' },
+ [TERM_KEY_F19] = { 57382, 'u' },
+ [TERM_KEY_F20] = { 57383, 'u' },
+ [TERM_KEY_F21] = { 57384, 'u' },
+ [TERM_KEY_F22] = { 57385, 'u' },
+ [TERM_KEY_F23] = { 57386, 'u' },
+ [TERM_KEY_F24] = { 57387, 'u' },
+ [TERM_KEY_F25] = { 57388, 'u' },
+ [TERM_KEY_F26] = { 57389, 'u' },
+ [TERM_KEY_F27] = { 57390, 'u' },
+ [TERM_KEY_F28] = { 57391, 'u' },
+ [TERM_KEY_F29] = { 57392, 'u' },
+ [TERM_KEY_F30] = { 57393, 'u' },
+ [TERM_KEY_F31] = { 57394, 'u' },
+ [TERM_KEY_F32] = { 57395, 'u' },
+ [TERM_KEY_F33] = { 57396, 'u' },
+ [TERM_KEY_F34] = { 57397, 'u' },
+ [TERM_KEY_F35] = { 57398, 'u' },
+
+ [TERM_KEY_KP_0] = { 57399, 'u' },
+ [TERM_KEY_KP_1] = { 57400, 'u' },
+ [TERM_KEY_KP_2] = { 57401, 'u' },
+ [TERM_KEY_KP_3] = { 57402, 'u' },
+ [TERM_KEY_KP_4] = { 57403, 'u' },
+ [TERM_KEY_KP_5] = { 57404, 'u' },
+ [TERM_KEY_KP_6] = { 57405, 'u' },
+ [TERM_KEY_KP_7] = { 57406, 'u' },
+ [TERM_KEY_KP_8] = { 57407, 'u' },
+ [TERM_KEY_KP_9] = { 57408, 'u' },
+ [TERM_KEY_KP_DECIMAL] = { 57409, 'u' },
+ [TERM_KEY_KP_DIVIDE] = { 57410, 'u' },
+ [TERM_KEY_KP_MULTIPLY] = { 57411, 'u' },
+ [TERM_KEY_KP_SUBTRACT] = { 57412, 'u' },
+ [TERM_KEY_KP_ADD] = { 57413, 'u' },
+ [TERM_KEY_KP_ENTER] = { 57414, 'u' },
+ [TERM_KEY_KP_EQUAL] = { 57415, 'u' },
+ [TERM_KEY_KP_SEPARATOR] = { 57416, 'u' },
+ [TERM_KEY_KP_LEFT] = { 57417, 'u' },
+ [TERM_KEY_KP_RIGHT] = { 57418, 'u' },
+ [TERM_KEY_KP_UP] = { 57419, 'u' },
+ [TERM_KEY_KP_DOWN] = { 57420, 'u' },
+ [TERM_KEY_KP_PAGE_UP] = { 57421, 'u' },
+ [TERM_KEY_KP_PAGE_DOWN] = { 57422, 'u' },
+ [TERM_KEY_KP_HOME] = { 57423, 'u' },
+ [TERM_KEY_KP_END] = { 57424, 'u' },
+ [TERM_KEY_KP_INSERT] = { 57425, 'u' },
+ [TERM_KEY_KP_DELETE] = { 57426, 'u' },
+ [TERM_KEY_KP_BEGIN] = { 1, 'E' },
+
+ [TERM_KEY_CAPS_LOCK] = { 57358, 'u' },
+ [TERM_KEY_SCROLL_LOCK] = { 57359, 'u' },
+ [TERM_KEY_NUM_LOCK] = { 57360, 'u' },
+ [TERM_KEY_PRINT_SCREEN] = { 57361, 'u' },
+ [TERM_KEY_PAUSE] = { 57362, 'u' },
+ [TERM_KEY_MENU] = { 57363, 'u' },
+
+ [TERM_KEY_MEDIA_PLAY] = { 57428, 'u' },
+ [TERM_KEY_MEDIA_PAUSE] = { 57429, 'u' },
+ [TERM_KEY_MEDIA_STOP] = { 57432, 'u' },
+ [TERM_KEY_MEDIA_FORWARD] = { 57433, 'u' },
+ [TERM_KEY_MEDIA_REWIND] = { 57434, 'u' },
+ [TERM_KEY_MEDIA_NEXT] = { 57435, 'u' },
+ [TERM_KEY_MEDIA_PREV] = { 57436, 'u' },
+ [TERM_KEY_MEDIA_RECORD] = { 57437, 'u' },
+ [TERM_KEY_VOLUME_DOWN] = { 57438, 'u' },
+ [TERM_KEY_VOLUME_UP] = { 57439, 'u' },
+ [TERM_KEY_MUTE] = { 57440, 'u' },
+
+ [TERM_KEY_LSHIFT] = { 57441, 'u' },
+ [TERM_KEY_LCTRL] = { 57442, 'u' },
+ [TERM_KEY_LALT] = { 57443, 'u' },
+ [TERM_KEY_LSUPER] = { 57444, 'u' },
+ [TERM_KEY_LHYPER] = { 57445, 'u' },
+ [TERM_KEY_LMETA] = { 57446, 'u' },
+ [TERM_KEY_RSHIFT] = { 57447, 'u' },
+ [TERM_KEY_RCTRL] = { 57448, 'u' },
+ [TERM_KEY_RALT] = { 57449, 'u' },
+ [TERM_KEY_RSUPER] = { 57450, 'u' },
+ [TERM_KEY_RHYPER] = { 57451, 'u' },
+ [TERM_KEY_RMETA] = { 57452, 'u' },
+ [TERM_KEY_ISO_LEVEL3_SHIFT] = { 57453, 'u' },
+ [TERM_KEY_ISO_LEVEL5_SHIFT] = { 57454, 'u' },
+};
+
+struct legacy_desc {
+ uint8_t tilde;
+ char final;
+ bool app_cursor;
+ char keypad_ss3;
+ enum term_key equiv;
+};
+
+static const struct legacy_desc legacy_desc[TERM_KEY_COUNT] = {
+ [TERM_KEY_UP] = { .tilde = 0, .final = 'A', .app_cursor = true },
+ [TERM_KEY_DOWN] = { .tilde = 0, .final = 'B', .app_cursor = true },
+ [TERM_KEY_RIGHT] = { .tilde = 0, .final = 'C', .app_cursor = true },
+ [TERM_KEY_LEFT] = { .tilde = 0, .final = 'D', .app_cursor = true },
+ [TERM_KEY_HOME] = { .tilde = 0, .final = 'H', .app_cursor = true },
+ [TERM_KEY_END] = { .tilde = 0, .final = 'F', .app_cursor = true },
+ [TERM_KEY_PAGE_UP] = { .tilde = 5, .final = '~' },
+ [TERM_KEY_PAGE_DOWN] = { .tilde = 6, .final = '~' },
+ [TERM_KEY_INSERT] = { .tilde = 2, .final = '~' },
+ [TERM_KEY_DELETE] = { .tilde = 3, .final = '~' },
+
+ [TERM_KEY_F1] = { .tilde = 0, .final = 'P' },
+ [TERM_KEY_F2] = { .tilde = 0, .final = 'Q' },
+ [TERM_KEY_F3] = { .tilde = 0, .final = 'R' },
+ [TERM_KEY_F4] = { .tilde = 0, .final = 'S' },
+ [TERM_KEY_F5] = { .tilde = 15, .final = '~' },
+ [TERM_KEY_F6] = { .tilde = 17, .final = '~' },
+ [TERM_KEY_F7] = { .tilde = 18, .final = '~' },
+ [TERM_KEY_F8] = { .tilde = 19, .final = '~' },
+ [TERM_KEY_F9] = { .tilde = 20, .final = '~' },
+ [TERM_KEY_F10] = { .tilde = 21, .final = '~' },
+ [TERM_KEY_F11] = { .tilde = 23, .final = '~' },
+ [TERM_KEY_F12] = { .tilde = 24, .final = '~' },
+
+ [TERM_KEY_KP_0] = { .keypad_ss3 = 'p' },
+ [TERM_KEY_KP_1] = { .keypad_ss3 = 'q' },
+ [TERM_KEY_KP_2] = { .keypad_ss3 = 'r' },
+ [TERM_KEY_KP_3] = { .keypad_ss3 = 's' },
+ [TERM_KEY_KP_4] = { .keypad_ss3 = 't' },
+ [TERM_KEY_KP_5] = { .keypad_ss3 = 'u' },
+ [TERM_KEY_KP_6] = { .keypad_ss3 = 'v' },
+ [TERM_KEY_KP_7] = { .keypad_ss3 = 'w' },
+ [TERM_KEY_KP_8] = { .keypad_ss3 = 'x' },
+ [TERM_KEY_KP_9] = { .keypad_ss3 = 'y' },
+ [TERM_KEY_KP_DECIMAL] = { .keypad_ss3 = 'n' },
+ [TERM_KEY_KP_DIVIDE] = { .keypad_ss3 = 'o' },
+ [TERM_KEY_KP_MULTIPLY] = { .keypad_ss3 = 'j' },
+ [TERM_KEY_KP_SUBTRACT] = { .keypad_ss3 = 'm' },
+ [TERM_KEY_KP_ADD] = { .keypad_ss3 = 'k' },
+ [TERM_KEY_KP_ENTER] = { .keypad_ss3 = 'M', .equiv = TERM_KEY_RETURN },
+ [TERM_KEY_KP_LEFT] = { .equiv = TERM_KEY_LEFT },
+ [TERM_KEY_KP_RIGHT] = { .equiv = TERM_KEY_RIGHT },
+ [TERM_KEY_KP_UP] = { .equiv = TERM_KEY_UP },
+ [TERM_KEY_KP_DOWN] = { .equiv = TERM_KEY_DOWN },
+ [TERM_KEY_KP_PAGE_UP] = { .equiv = TERM_KEY_PAGE_UP },
+ [TERM_KEY_KP_PAGE_DOWN] = { .equiv = TERM_KEY_PAGE_DOWN },
+ [TERM_KEY_KP_HOME] = { .equiv = TERM_KEY_HOME },
+ [TERM_KEY_KP_END] = { .equiv = TERM_KEY_END },
+ [TERM_KEY_KP_INSERT] = { .equiv = TERM_KEY_INSERT },
+ [TERM_KEY_KP_DELETE] = { .equiv = TERM_KEY_DELETE },
+ [TERM_KEY_KP_BEGIN] = { .tilde = 0, .final = 'E' },
+};
+
+enum input_mod_key {
+ MOD_KEY_SHIFT = 0,
+ MOD_KEY_CTRL,
+ MOD_KEY_ALT,
+ MOD_KEY_SUPER,
+ MOD_KEY_HYPER,
+ MOD_KEY_META,
+ MOD_KEY_ISO3,
+ MOD_KEY_ISO5,
+ MOD_KEY_COUNT,
+};
+
+struct mod_desc {
+ enum input_mod_key group;
+ enum term_key partner;
+};
+
+static const struct mod_desc mod_desc[TERM_KEY_COUNT] = {
+ [TERM_KEY_LSHIFT] = { MOD_KEY_SHIFT, TERM_KEY_RSHIFT },
+ [TERM_KEY_RSHIFT] = { MOD_KEY_SHIFT, TERM_KEY_LSHIFT },
+ [TERM_KEY_LCTRL] = { MOD_KEY_CTRL, TERM_KEY_RCTRL },
+ [TERM_KEY_RCTRL] = { MOD_KEY_CTRL, TERM_KEY_LCTRL },
+ [TERM_KEY_LALT] = { MOD_KEY_ALT, TERM_KEY_RALT },
+ [TERM_KEY_RALT] = { MOD_KEY_ALT, TERM_KEY_LALT },
+ [TERM_KEY_LSUPER] = { MOD_KEY_SUPER, TERM_KEY_RSUPER },
+ [TERM_KEY_RSUPER] = { MOD_KEY_SUPER, TERM_KEY_LSUPER },
+ [TERM_KEY_LHYPER] = { MOD_KEY_HYPER, TERM_KEY_RHYPER },
+ [TERM_KEY_RHYPER] = { MOD_KEY_HYPER, TERM_KEY_LHYPER },
+ [TERM_KEY_LMETA] = { MOD_KEY_META, TERM_KEY_RMETA },
+ [TERM_KEY_RMETA] = { MOD_KEY_META, TERM_KEY_LMETA },
+ [TERM_KEY_ISO_LEVEL3_SHIFT] = { MOD_KEY_ISO3, TERM_KEY_NONE },
+ [TERM_KEY_ISO_LEVEL5_SHIFT] = { MOD_KEY_ISO5, TERM_KEY_NONE },
+};
+
+static const unsigned mod_group_input_mod[MOD_KEY_COUNT] = {
+ TERM_MOD_SHIFT, TERM_MOD_CTRL, TERM_MOD_ALT,
+ TERM_MOD_SUPER, TERM_MOD_HYPER, TERM_MOD_META,
+ 0, 0,
+};
+
+static inline bool key_is_modifier(enum term_key k)
+{
+ return (unsigned)k < TERM_KEY_COUNT &&
+ (mod_desc[k].partner != TERM_KEY_NONE ||
+ mod_desc[k].group == MOD_KEY_ISO3 ||
+ mod_desc[k].group == MOD_KEY_ISO5);
+}
+
+static inline bool key_is_lock(enum term_key k)
+{
+ return k == TERM_KEY_CAPS_LOCK || k == TERM_KEY_SCROLL_LOCK ||
+ k == TERM_KEY_NUM_LOCK;
+}
+
+static unsigned effective_mods(const struct term_keyboard_state *s,
+ const struct term_key_event *ev)
+{
+ unsigned mods = ev->mods;
+ if (!key_is_modifier(ev->key)) return mods;
+
+ const struct mod_desc *d = &mod_desc[ev->key];
+ if (d->group >= MOD_KEY_ISO3) return mods;
+
+ unsigned bit = mod_group_input_mod[d->group];
+ bool other_held = term_keyboard_key_down(s, d->partner);
+ if (ev->action != TERM_KEY_RELEASE || other_held) mods |= bit;
+ else mods &= ~bit;
+ return mods;
+}
+
+static unsigned lock_mod_bit(enum term_key key)
+{
+ if (key == TERM_KEY_CAPS_LOCK) return TERM_MOD_CAPS;
+ if (key == TERM_KEY_NUM_LOCK) return TERM_MOD_NUM;
+ return 0;
+}
+
+static unsigned toggle_locks(enum term_key key, enum term_key_action action,
+ unsigned mods)
+{
+ unsigned bit = lock_mod_bit(key);
+ if (bit && action == TERM_KEY_PRESS) mods ^= bit;
+ return mods;
+}
+
+/* Convert logical input modifiers to terminal protocol bits. */
+static unsigned protocol_modifiers(unsigned mods)
+{
+ return ((mods & TERM_MOD_SHIFT) ? 1u : 0u) |
+ ((mods & TERM_MOD_ALT) ? 2u : 0u) |
+ ((mods & TERM_MOD_CTRL) ? 4u : 0u) |
+ ((mods & TERM_MOD_SUPER) ? 8u : 0u) |
+ ((mods & TERM_MOD_HYPER) ? 16u : 0u) |
+ ((mods & TERM_MOD_META) ? 32u : 0u) |
+ ((mods & TERM_MOD_CAPS) ? 64u : 0u) |
+ ((mods & TERM_MOD_NUM) ? 128u : 0u);
+}
+
+static void format_kitty(struct seqbuf *sb, uint32_t code, char final,
+ unsigned flags, bool functional, uint32_t shifted,
+ uint32_t base, unsigned mods, unsigned event,
+ bool report_text, uint32_t text)
+{
+ char tmp[64];
+ struct seqbuf body;
+ sb_init(&body, tmp, sizeof(tmp));
+
+ if (final != 'u' && final != '~' && code <= 1 && !shifted && !base &&
+ !mods && !(flags & TERM_KEYBOARD_EVENT_TYPES) && !report_text) {
+
+ sb_ch(&body, '\033');
+ sb_ch(&body, '[');
+ sb_ch(&body, final);
+ } else {
+ sb_ch(&body, '\033');
+ sb_ch(&body, '[');
+ sb_u32(&body, code);
+ if ((flags & TERM_KEYBOARD_ALTERNATE_KEYS) && !functional) {
+ if (shifted) {
+ sb_ch(&body, ':');
+ sb_u32(&body, shifted);
+ }
+ if (base) {
+ sb_ch(&body, ':');
+ if (!shifted) sb_ch(&body, ':');
+ sb_u32(&body, base);
+ }
+ }
+ if (mods || (flags & TERM_KEYBOARD_EVENT_TYPES) || report_text) {
+ sb_ch(&body, ';');
+ sb_u32(&body, mods + 1);
+ }
+ if ((flags & TERM_KEYBOARD_EVENT_TYPES) && event != 1) {
+ sb_ch(&body, ':');
+ sb_u32(&body, event);
+ }
+ if (report_text) {
+ sb_ch(&body, ';');
+ sb_u32(&body, text);
+ }
+ sb_ch(&body, final);
+ }
+
+ if (body.overflow) {
+ sb->overflow = true;
+ return;
+ }
+ sb_str(sb, tmp, sb_len(&body, tmp));
+}
+
+static bool encode_kitty(const struct term_keyboard_state *s,
+ const struct term_key_event *ev,
+ const struct term_modes *m,
+ struct seqbuf *sb)
+{
+ unsigned flags = m->keyboard;
+ unsigned event = ev->action == TERM_KEY_PRESS ? 1u :
+ ev->action == TERM_KEY_REPEAT ? 2u : 3u;
+ if (!flags) return ev->action == TERM_KEY_RELEASE;
+
+ const struct kitty_desc *d = &kitty_desc[ev->key];
+ bool functional = d->code != 0;
+ char final = functional ? d->final : 'u';
+ bool all = (flags & TERM_KEYBOARD_ALL_KEYS) != 0;
+
+ if ((key_is_modifier(ev->key) || key_is_lock(ev->key)) && !all)
+ return true;
+
+ unsigned mods = effective_mods(s, ev);
+ mods = toggle_locks(ev->key, ev->action, mods);
+
+ bool plain_control = ev->key == TERM_KEY_RETURN || ev->key == TERM_KEY_TAB ||
+ ev->key == TERM_KEY_BACKSPACE;
+ if (ev->action == TERM_KEY_RELEASE &&
+ (!(flags & TERM_KEYBOARD_EVENT_TYPES) || (!all && plain_control)))
+ return true;
+
+ const unsigned command_mods = TERM_MOD_ALT | TERM_MOD_CTRL |
+ TERM_MOD_SUPER | TERM_MOD_HYPER |
+ TERM_MOD_META;
+ uint32_t text = ev->text;
+ bool has_text = text >= 32 && !(text >= 127 && text < 160) &&
+ !(mods & command_mods);
+ if (!all && has_text) return ev->action == TERM_KEY_RELEASE;
+ if (!all && plain_control && !(protocol_modifiers(mods) & 63))
+ return ev->action == TERM_KEY_RELEASE;
+ if (!(flags & (TERM_KEYBOARD_DISAMBIGUATE | TERM_KEYBOARD_EVENT_TYPES | TERM_KEYBOARD_ALL_KEYS)))
+ return ev->action == TERM_KEY_RELEASE;
+
+ uint32_t code = d->code, shifted = 0, base = 0;
+ if (!functional) {
+ code = ev->codepoint;
+ if (flags & TERM_KEYBOARD_ALTERNATE_KEYS) {
+ shifted = (mods & TERM_MOD_SHIFT) ? ev->shifted : 0;
+ base = ev->base;
+ if (shifted == code) shifted = 0;
+ if (base == code) base = 0;
+ }
+ }
+ if (!code) return true;
+
+ bool report_text = all && (flags & TERM_KEYBOARD_ASSOCIATED_TEXT) && has_text &&
+ ev->action != TERM_KEY_RELEASE;
+ format_kitty(sb, code, final, flags, functional, shifted, base, protocol_modifiers(mods),
+ event, report_text, text);
+ return true;
+}
+
+static uint32_t control_base_char(const struct term_key_event *ev)
+{
+ uint32_t ch = ((ev->mods & TERM_MOD_SHIFT) && ev->shifted)
+ ? ev->shifted : ev->codepoint;
+ return ch < 0x80 ? ch : 0;
+}
+
+static bool encode_legacy(const struct term_key_event *ev,
+ const struct term_modes *m,
+ struct seqbuf *sb)
+{
+ if (m->app_keypad && (unsigned)ev->keypad < TERM_KEY_COUNT &&
+ legacy_desc[ev->keypad].keypad_ss3) {
+ sb_ch(sb, '\033');
+ sb_ch(sb, 'O');
+ sb_ch(sb, legacy_desc[ev->keypad].keypad_ss3);
+ return true;
+ }
+
+ uint32_t ch = control_base_char(ev);
+ if ((ev->mods & TERM_MOD_CTRL) && ch) {
+ if ((ch >= 'a' && ch <= 'z') || (ch >= '@' && ch <= '_')) {
+ sb_ch(sb, (char)(ch & 0x1f));
+ return true;
+ }
+ if (ch == ' ') { sb_ch(sb, 0); return true; }
+ if (ch == '-') { sb_ch(sb, 0x1f); return true; }
+ }
+
+ unsigned csi = protocol_modifiers(ev->mods) & 63;
+ unsigned p = csi + 1;
+ bool shift = (ev->mods & TERM_MOD_SHIFT) != 0;
+
+ enum term_key k = ev->key;
+ if ((unsigned)k < TERM_KEY_COUNT && legacy_desc[k].equiv != TERM_KEY_NONE)
+ k = legacy_desc[k].equiv;
+
+ if ((unsigned)k < TERM_KEY_COUNT) {
+ const struct legacy_desc *d = &legacy_desc[k];
+ if (d->final) {
+ if (d->tilde) {
+ sb_ch(sb, '\033');
+ sb_ch(sb, '[');
+ sb_u32(sb, d->tilde);
+ if (csi) {
+ sb_ch(sb, ';');
+ sb_u32(sb, p);
+ }
+ sb_ch(sb, '~');
+ return true;
+ }
+ if (csi) {
+ sb_ch(sb, '\033');
+ sb_ch(sb, '[');
+ sb_ch(sb, '1');
+ sb_ch(sb, ';');
+ sb_u32(sb, p);
+ sb_ch(sb, d->final);
+ return true;
+ }
+ sb_ch(sb, '\033');
+ sb_ch(sb, d->app_cursor && !m->app_cursor ? '[' : 'O');
+ sb_ch(sb, d->final);
+ return true;
+ }
+ }
+
+ switch (k) {
+ case TERM_KEY_RETURN: sb_ch(sb, '\r'); return true;
+ case TERM_KEY_BACKSPACE:
+ sb_ch(sb, m->backarrow_bs ? '\x08' : '\x7f');
+ return true;
+ case TERM_KEY_TAB:
+ if (shift) { sb_str(sb, "\033[Z", 3); }
+ else { sb_ch(sb, '\t'); }
+ return true;
+ case TERM_KEY_ESCAPE: sb_ch(sb, '\033'); return true;
+ default: break;
+ }
+
+ if (ev->text && !ev->text_deferred) {
+ char tmp[8];
+ size_t n = (size_t)utf8_encode(ev->text, tmp);
+ sb_str(sb, tmp, n);
+ return true;
+ }
+ return false;
+}
+
+void term_keyboard_update(struct term_keyboard_state *s, const struct term_key_event *ev)
+{
+ if (!s || !ev || ev->key <= TERM_KEY_NONE || ev->key >= TERM_KEY_COUNT)
+ return;
+ if (ev->action == TERM_KEY_RELEASE)
+ s->down[ev->key >> 6] &= ~(1ull << (ev->key & 63));
+ else if (ev->action == TERM_KEY_PRESS || ev->action == TERM_KEY_REPEAT)
+ s->down[ev->key >> 6] |= 1ull << (ev->key & 63);
+}
+
+bool term_keyboard_key_down(const struct term_keyboard_state *s, enum term_key key)
+{
+ return s && key > TERM_KEY_NONE && (unsigned)key < TERM_KEY_COUNT &&
+ ((s->down[key >> 6] >> (key & 63)) & 1);
+}
+
+struct term_input_result term_input_encode_key(const struct term_keyboard_state *s,
+ const struct term_key_event *ev,
+ const struct term_modes *m,
+ char *buf, size_t buf_sz)
+{
+ struct term_input_result out = {0};
+ if (!s || !ev || !m || !buf ||
+ (ev->action != TERM_KEY_PRESS && ev->action != TERM_KEY_REPEAT &&
+ ev->action != TERM_KEY_RELEASE) ||
+ (unsigned)ev->key >= TERM_KEY_COUNT)
+ return out;
+
+ struct seqbuf sb;
+ sb_init(&sb, buf, buf_sz);
+
+ bool handled = encode_kitty(s, ev, m, &sb);
+ if (!handled) {
+ if (ev->action == TERM_KEY_RELEASE)
+ return out;
+
+ bool alt = (ev->mods & TERM_MOD_ALT) != 0;
+ struct seqbuf body;
+ char tmp[64];
+ if (alt) {
+ sb_init(&body, tmp, sizeof(tmp));
+ } else {
+ body = sb;
+ }
+
+ handled = encode_legacy(ev, m, &body);
+ if (handled) {
+ if (body.overflow) {
+ sb.overflow = true;
+ } else {
+ size_t n = sb_len(&body, alt ? tmp : buf);
+ if (alt) {
+ bool already_esc = n && tmp[0] == '\033';
+ size_t prefix = !already_esc;
+ if (n + prefix <= buf_sz) {
+ if (prefix) buf[0] = '\033';
+ memcpy(buf + prefix, tmp, n);
+ n += prefix;
+ } else {
+ sb.overflow = true;
+ n = 0;
+ }
+ }
+ out.len = n;
+ }
+ }
+ } else {
+ out.len = sb_len(&sb, buf);
+ }
+
+ if (!handled)
+ return out;
+ if (sb.overflow) {
+ out.status = TERM_INPUT_BUFFER_FULL;
+ out.len = 0;
+ } else {
+ out.status = out.len ? TERM_INPUT_BYTES : TERM_INPUT_CONSUMED;
+ }
+ return out;
+}
+
+struct term_input_result term_input_encode_mouse(const struct term_mouse_event *ev,
+ const struct term_modes *m,
+ char *buf, size_t buf_sz)
+{
+ struct term_input_result out = {0};
+ if (!ev || !m || !buf) return out;
+
+ if (!m->mouse || (ev->mods & TERM_MOD_SHIFT)) return out;
+
+ out.status = TERM_INPUT_CONSUMED;
+ if (ev->kind == TERM_MOUSE_LEAVE) return out;
+ struct term_vector2i cell = ev->pos;
+ if (cell.x < 0 || cell.y < 0 || cell.x >= m->grid.x || cell.y >= m->grid.y)
+ return out;
+ if (m->mouse == TERM_MOUSE_X10 && (ev->kind == TERM_MOUSE_RELEASE || ev->kind == TERM_MOUSE_MOTION)) {
+ return out;
+ }
+ if (ev->kind == TERM_MOUSE_MOTION && (m->mouse == TERM_MOUSE_NORMAL ||
+ (m->mouse == TERM_MOUSE_BUTTON && ev->button == 3))) {
+ return out;
+ }
+ int button;
+ if (ev->button >= TERM_MOUSE_BUTTON_PRIMARY && ev->button <= TERM_MOUSE_BUTTON_NONE)
+ button = ev->button;
+ else if (ev->button >= TERM_MOUSE_BUTTON_EXTRA_1 && ev->button <= TERM_MOUSE_BUTTON_EXTRA_5)
+ button = 8 + ev->button - TERM_MOUSE_BUTTON_EXTRA_1;
+ else if (ev->button >= TERM_MOUSE_WHEEL_UP && ev->button <= TERM_MOUSE_WHEEL_RIGHT)
+ button = 64 + ev->button - TERM_MOUSE_WHEEL_UP;
+ else {
+ return out;
+ }
+
+ int mods = 0;
+ if (ev->mods & TERM_MOD_ALT) mods |= 8;
+ if (ev->mods & TERM_MOD_CTRL) mods |= 16;
+
+ int code = button | mods | (ev->kind == TERM_MOUSE_MOTION ? 32 : 0);
+ int legacy_code = ev->kind == TERM_MOUSE_RELEASE ? 3 | mods : code;
+ struct term_vector2i report = m->mouse_encoding == TERM_MOUSE_ENC_SGR_PIXELS
+ ? ev->pixel : cell;
+ if (report.x < 0 || report.y < 0)
+ return out;
+ uint32_t report_x = (uint32_t)report.x + 1;
+ uint32_t report_y = (uint32_t)report.y + 1;
+
+ struct seqbuf sb;
+ sb_init(&sb, buf, buf_sz);
+
+ if (m->mouse_encoding == TERM_MOUSE_ENC_SGR ||
+ m->mouse_encoding == TERM_MOUSE_ENC_SGR_PIXELS) {
+ sb_ch(&sb, '\033');
+ sb_ch(&sb, '[');
+ sb_ch(&sb, '<');
+ sb_u32(&sb, (uint32_t)code);
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)report_x);
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)report_y);
+ sb_ch(&sb, ev->kind == TERM_MOUSE_RELEASE ? 'm' : 'M');
+ } else if (m->mouse_encoding == TERM_MOUSE_ENC_URXVT) {
+ sb_ch(&sb, '\033');
+ sb_ch(&sb, '[');
+ sb_u32(&sb, (uint32_t)(legacy_code + 32));
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)(cell.x + 1));
+ sb_ch(&sb, ';');
+ sb_u32(&sb, (uint32_t)(cell.y + 1));
+ sb_ch(&sb, 'M');
+ } else if (m->mouse_encoding == TERM_MOUSE_ENC_UTF8) {
+ const unsigned values[3] = {
+ (unsigned)legacy_code + 32, (unsigned)cell.x + 33,
+ (unsigned)cell.y + 33,
+ };
+ sb_str(&sb, "\033[M", 3);
+ for (int i = 0; i < 3; i++) {
+ char tmp[8];
+ size_t n = (size_t)utf8_encode(values[i], tmp);
+ sb_str(&sb, tmp, n);
+ }
+ } else {
+ if (legacy_code > 223 || cell.x > 222 || cell.y > 222)
+ return out;
+ sb_ch(&sb, '\033');
+ sb_ch(&sb, '[');
+ sb_ch(&sb, 'M');
+ sb_ch(&sb, (char)(legacy_code + 32));
+ sb_ch(&sb, (char)(cell.x + 33));
+ sb_ch(&sb, (char)(cell.y + 33));
+ }
+
+ if (sb.overflow) {
+ out.status = TERM_INPUT_BUFFER_FULL;
+ out.len = 0;
+ return out;
+ }
+ out.len = sb_len(&sb, buf);
+ out.status = TERM_INPUT_BYTES;
+ return out;
+}
+
+/* Bidi controls can make pasted text look different from what runs. */
+static bool is_direction_format(uint32_t cp)
+{
+ return cp == 0x061c ||
+ (cp >= 0x200e && cp <= 0x200f) ||
+ (cp >= 0x202a && cp <= 0x202e) ||
+ (cp >= 0x2066 && cp <= 0x2069) ||
+ cp == 0xfeff;
+}
+
+struct term_input_result term_input_encode_paste(const struct term_modes *m,
+ const char *text, size_t len,
+ char *buf, size_t capacity)
+{
+ struct term_input_result out = {0};
+ if (!m || !buf || (!text && len)) return out;
+
+ struct seqbuf sb;
+ sb_init(&sb, buf, capacity);
+ if (m->bracketed_paste) sb_bytes(&sb, "\033[200~", 6);
+ size_t body = sb_len(&sb, buf);
+ for (size_t i = 0; i < len;) {
+ size_t start = i;
+ while (i < len && (unsigned char)text[i] >= 0x20 && (unsigned char)text[i] < 0x7f)
+ i++;
+ if (i > start) {
+ sb_bytes(&sb, text + start, i - start);
+ continue;
+ }
+ uint32_t cp;
+ int n = utf8_decode_one(text + i, len - i, &cp);
+ if (!n) { i++; continue; }
+ if (cp == '\r' || cp == '\n') {
+ sb_ch(&sb, '\r');
+ if (cp == '\r' && i + 1 < len && text[i + 1] == '\n') i++;
+ } else if (cp == '\t') {
+ sb_ch(&sb, '\t');
+ } else if (cp >= 0xa0 && !is_direction_format(cp)) {
+ sb_bytes(&sb, text + i, (size_t)n);
+ }
+ i += (size_t)n;
+ }
+
+ out.status = TERM_INPUT_CONSUMED;
+ if (!sb.overflow && sb_len(&sb, buf) == body) return out;
+ if (m->bracketed_paste) sb_bytes(&sb, "\033[201~", 6);
+ if (sb.overflow) {
+ out.status = TERM_INPUT_BUFFER_FULL;
+ return out;
+ }
+ out.len = sb_len(&sb, buf);
+ out.status = TERM_INPUT_BYTES;
+ return out;
+}