summaryrefslogtreecommitdiffstats
path: root/src/input.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/input.cpp')
-rw-r--r--src/input.cpp1949
1 files changed, 1949 insertions, 0 deletions
diff --git a/src/input.cpp b/src/input.cpp
new file mode 100644
index 0000000..935f998
--- /dev/null
+++ b/src/input.cpp
@@ -0,0 +1,1949 @@
+#include "input.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <poll.h>
+#include <sys/stat.h>
+#include <termios.h>
+
+#include <cjson/cJSON.h>
+
+/* ================================================================== */
+/* Actions / events */
+/* ================================================================== */
+
+void input_action_free(input_action *a) {
+ if (!a) return;
+ free(a->text);
+ a->text = NULL;
+ a->kind = ACT_NONE;
+}
+
+void evlist_init(evlist *l) {
+ memset(l, 0, sizeof(*l));
+}
+
+void evlist_push(evlist *l, input_key k, const unsigned char *raw, size_t raw_len) {
+ if (l->len == l->cap) {
+ size_t new_cap = l->cap ? l->cap * 2 : 8;
+ input_key *ni = (input_key *) xmalloc(new_cap * sizeof(*ni));
+ unsigned char* *nr = (unsigned char* * ) xmalloc(new_cap * sizeof(*nr));
+ size_t *nl = (size_t *) xmalloc(new_cap * sizeof(*nl));
+ if (l->cap > 0) {
+ for (size_t i = 0; i < l->len; i++) {
+ size_t src = (l->head + i) % l->cap;
+ ni[i] = l->items[src];
+ nr[i] = l->raws[src];
+ nl[i] = l->lens[src];
+ }
+ free(l->items);
+ free(l->raws);
+ free(l->lens);
+ }
+ l->items = ni;
+ l->raws = nr;
+ l->lens = nl;
+ l->head = 0;
+ l->cap = new_cap;
+ }
+ size_t i = (l->head + l->len) % l->cap;
+ l->items[i] = k;
+ l->raws[i] = (unsigned char* ) xmalloc(raw_len + 1);
+ memcpy(l->raws[i], raw, raw_len);
+ l->raws[i][raw_len] = '\0';
+ l->lens[i] = raw_len;
+ l->len++;
+}
+
+void evlist_pop(evlist *l) {
+ if (l->len == 0) return;
+ free(l->raws[l->head]);
+ l->head = (l->head + 1 == l->cap) ? 0 : l->head + 1;
+ l->len--;
+}
+
+void evlist_free(evlist *l) {
+ if (l->cap > 0)
+ for (size_t i = 0; i < l->len; i++)
+ free(l->raws[(l->head + i) % l->cap]);
+ free(l->items);
+ free(l->raws);
+ free(l->lens);
+ memset(l, 0, sizeof(*l));
+}
+
+/* ================================================================== */
+/* Display width */
+/* ================================================================== */
+
+/* Unicode width tables generated from `unicode-width` 0.2 (the oracle's
+ * source of truth), covering U+0000..U+2FFFF. Zero-width (control chars,
+ * combining marks) and wide/fullwidth codepoint ranges. */
+static const uint32_t zw[][2] = {
+ {0x0u, 0x1fu},
+ {0x7fu, 0x9fu},
+ {0xadu, 0xadu},
+ {0x300u, 0x36fu},
+ {0x483u, 0x489u},
+ {0x591u, 0x5bdu},
+ {0x5bfu, 0x5bfu},
+ {0x5c1u, 0x5c2u},
+ {0x5c4u, 0x5c5u},
+ {0x5c7u, 0x5c7u},
+ {0x605u, 0x605u},
+ {0x610u, 0x61au},
+ {0x61cu, 0x61cu},
+ {0x64bu, 0x65fu},
+ {0x670u, 0x670u},
+ {0x6d6u, 0x6dcu},
+ {0x6dfu, 0x6e4u},
+ {0x6e7u, 0x6e8u},
+ {0x6eau, 0x6edu},
+ {0x70fu, 0x70fu},
+ {0x711u, 0x711u},
+ {0x730u, 0x74au},
+ {0x7a6u, 0x7b0u},
+ {0x7ebu, 0x7f3u},
+ {0x7fdu, 0x7fdu},
+ {0x816u, 0x819u},
+ {0x81bu, 0x823u},
+ {0x825u, 0x827u},
+ {0x829u, 0x82du},
+ {0x859u, 0x85bu},
+ {0x890u, 0x891u},
+ {0x897u, 0x89fu},
+ {0x8cau, 0x902u},
+ {0x93au, 0x93au},
+ {0x93cu, 0x93cu},
+ {0x941u, 0x948u},
+ {0x94du, 0x94du},
+ {0x951u, 0x957u},
+ {0x962u, 0x963u},
+ {0x981u, 0x981u},
+ {0x9bcu, 0x9bcu},
+ {0x9beu, 0x9beu},
+ {0x9c1u, 0x9c4u},
+ {0x9cdu, 0x9cdu},
+ {0x9d7u, 0x9d7u},
+ {0x9e2u, 0x9e3u},
+ {0x9feu, 0x9feu},
+ {0xa01u, 0xa02u},
+ {0xa3cu, 0xa3cu},
+ {0xa41u, 0xa42u},
+ {0xa47u, 0xa48u},
+ {0xa4bu, 0xa4du},
+ {0xa51u, 0xa51u},
+ {0xa70u, 0xa71u},
+ {0xa75u, 0xa75u},
+ {0xa81u, 0xa82u},
+ {0xabcu, 0xabcu},
+ {0xac1u, 0xac5u},
+ {0xac7u, 0xac8u},
+ {0xacdu, 0xacdu},
+ {0xae2u, 0xae3u},
+ {0xafau, 0xaffu},
+ {0xb01u, 0xb01u},
+ {0xb3cu, 0xb3cu},
+ {0xb3eu, 0xb3fu},
+ {0xb41u, 0xb44u},
+ {0xb4du, 0xb4du},
+ {0xb55u, 0xb57u},
+ {0xb62u, 0xb63u},
+ {0xb82u, 0xb82u},
+ {0xbbeu, 0xbbeu},
+ {0xbc0u, 0xbc0u},
+ {0xbcdu, 0xbcdu},
+ {0xbd7u, 0xbd7u},
+ {0xc00u, 0xc00u},
+ {0xc04u, 0xc04u},
+ {0xc3cu, 0xc3cu},
+ {0xc3eu, 0xc40u},
+ {0xc46u, 0xc48u},
+ {0xc4au, 0xc4du},
+ {0xc55u, 0xc56u},
+ {0xc62u, 0xc63u},
+ {0xc81u, 0xc81u},
+ {0xcbcu, 0xcbcu},
+ {0xcbfu, 0xcc0u},
+ {0xcc2u, 0xcc2u},
+ {0xcc6u, 0xcc8u},
+ {0xccau, 0xccdu},
+ {0xcd5u, 0xcd6u},
+ {0xce2u, 0xce3u},
+ {0xd00u, 0xd01u},
+ {0xd3bu, 0xd3cu},
+ {0xd3eu, 0xd3eu},
+ {0xd41u, 0xd44u},
+ {0xd4du, 0xd4eu},
+ {0xd57u, 0xd57u},
+ {0xd62u, 0xd63u},
+ {0xd81u, 0xd81u},
+ {0xdcau, 0xdcau},
+ {0xdcfu, 0xdcfu},
+ {0xdd2u, 0xdd4u},
+ {0xdd6u, 0xdd6u},
+ {0xddfu, 0xddfu},
+ {0xe31u, 0xe31u},
+ {0xe34u, 0xe3au},
+ {0xe47u, 0xe4eu},
+ {0xeb1u, 0xeb1u},
+ {0xeb4u, 0xebcu},
+ {0xec8u, 0xeceu},
+ {0xf18u, 0xf19u},
+ {0xf35u, 0xf35u},
+ {0xf37u, 0xf37u},
+ {0xf39u, 0xf39u},
+ {0xf71u, 0xf7eu},
+ {0xf80u, 0xf84u},
+ {0xf86u, 0xf87u},
+ {0xf8du, 0xf97u},
+ {0xf99u, 0xfbcu},
+ {0xfc6u, 0xfc6u},
+ {0x102du, 0x1030u},
+ {0x1032u, 0x1037u},
+ {0x1039u, 0x103au},
+ {0x103du, 0x103eu},
+ {0x1058u, 0x1059u},
+ {0x105eu, 0x1060u},
+ {0x1071u, 0x1074u},
+ {0x1082u, 0x1082u},
+ {0x1085u, 0x1086u},
+ {0x108du, 0x108du},
+ {0x109du, 0x109du},
+ {0x1160u, 0x11ffu},
+ {0x135du, 0x135fu},
+ {0x1712u, 0x1715u},
+ {0x1732u, 0x1734u},
+ {0x1752u, 0x1753u},
+ {0x1772u, 0x1773u},
+ {0x17b4u, 0x17b5u},
+ {0x17b7u, 0x17bdu},
+ {0x17c6u, 0x17c6u},
+ {0x17c9u, 0x17d3u},
+ {0x17ddu, 0x17ddu},
+ {0x180bu, 0x180fu},
+ {0x1885u, 0x1886u},
+ {0x18a9u, 0x18a9u},
+ {0x1920u, 0x1922u},
+ {0x1927u, 0x1928u},
+ {0x1932u, 0x1932u},
+ {0x1939u, 0x193bu},
+ {0x1a17u, 0x1a18u},
+ {0x1a1bu, 0x1a1bu},
+ {0x1a56u, 0x1a56u},
+ {0x1a58u, 0x1a5eu},
+ {0x1a60u, 0x1a60u},
+ {0x1a62u, 0x1a62u},
+ {0x1a65u, 0x1a6cu},
+ {0x1a73u, 0x1a7cu},
+ {0x1a7fu, 0x1a7fu},
+ {0x1ab0u, 0x1addu},
+ {0x1ae0u, 0x1aebu},
+ {0x1b00u, 0x1b03u},
+ {0x1b34u, 0x1b3du},
+ {0x1b42u, 0x1b44u},
+ {0x1b6bu, 0x1b73u},
+ {0x1b80u, 0x1b81u},
+ {0x1ba2u, 0x1ba5u},
+ {0x1ba8u, 0x1badu},
+ {0x1be6u, 0x1be6u},
+ {0x1be8u, 0x1be9u},
+ {0x1bedu, 0x1bedu},
+ {0x1befu, 0x1bf3u},
+ {0x1c2cu, 0x1c33u},
+ {0x1c36u, 0x1c37u},
+ {0x1cd0u, 0x1cd2u},
+ {0x1cd4u, 0x1ce0u},
+ {0x1ce2u, 0x1ce8u},
+ {0x1cedu, 0x1cedu},
+ {0x1cf4u, 0x1cf4u},
+ {0x1cf8u, 0x1cf9u},
+ {0x1dc0u, 0x1dffu},
+ {0x200bu, 0x200fu},
+ {0x202au, 0x202eu},
+ {0x2060u, 0x206fu},
+ {0x20d0u, 0x20f0u},
+ {0x2cefu, 0x2cf1u},
+ {0x2de0u, 0x2dffu},
+ {0x302au, 0x302fu},
+ {0x3099u, 0x309au},
+ {0x3164u, 0x3164u},
+ {0xa66fu, 0xa672u},
+ {0xa674u, 0xa67du},
+ {0xa69eu, 0xa69fu},
+ {0xa6f0u, 0xa6f1u},
+ {0xa802u, 0xa802u},
+ {0xa806u, 0xa806u},
+ {0xa80bu, 0xa80bu},
+ {0xa825u, 0xa826u},
+ {0xa82cu, 0xa82cu},
+ {0xa8c4u, 0xa8c5u},
+ {0xa8e0u, 0xa8f1u},
+ {0xa8fau, 0xa8fau},
+ {0xa8ffu, 0xa8ffu},
+ {0xa926u, 0xa92du},
+ {0xa947u, 0xa951u},
+ {0xa953u, 0xa953u},
+ {0xa980u, 0xa982u},
+ {0xa9b3u, 0xa9b3u},
+ {0xa9b6u, 0xa9b9u},
+ {0xa9bcu, 0xa9bdu},
+ {0xa9c0u, 0xa9c0u},
+ {0xa9e5u, 0xa9e5u},
+ {0xaa29u, 0xaa2eu},
+ {0xaa31u, 0xaa32u},
+ {0xaa35u, 0xaa36u},
+ {0xaa43u, 0xaa43u},
+ {0xaa4cu, 0xaa4cu},
+ {0xaa7cu, 0xaa7cu},
+ {0xaab0u, 0xaab0u},
+ {0xaab2u, 0xaab4u},
+ {0xaab7u, 0xaab8u},
+ {0xaabeu, 0xaabfu},
+ {0xaac1u, 0xaac1u},
+ {0xaaecu, 0xaaedu},
+ {0xaaf6u, 0xaaf6u},
+ {0xabe5u, 0xabe5u},
+ {0xabe8u, 0xabe8u},
+ {0xabedu, 0xabedu},
+ {0xd7b0u, 0xd7c6u},
+ {0xd7cbu, 0xd7fbu},
+ {0xfb1eu, 0xfb1eu},
+ {0xfe00u, 0xfe0fu},
+ {0xfe20u, 0xfe2fu},
+ {0xfeffu, 0xfeffu},
+ {0xff9eu, 0xffa0u},
+ {0xfff0u, 0xfff8u},
+ {0x101fdu, 0x101fdu},
+ {0x102e0u, 0x102e0u},
+ {0x10376u, 0x1037au},
+ {0x10a01u, 0x10a03u},
+ {0x10a05u, 0x10a06u},
+ {0x10a0cu, 0x10a0fu},
+ {0x10a38u, 0x10a3au},
+ {0x10a3fu, 0x10a3fu},
+ {0x10ae5u, 0x10ae6u},
+ {0x10d24u, 0x10d27u},
+ {0x10d69u, 0x10d6du},
+ {0x10eabu, 0x10eacu},
+ {0x10efau, 0x10effu},
+ {0x10f46u, 0x10f50u},
+ {0x10f82u, 0x10f85u},
+ {0x11001u, 0x11001u},
+ {0x11038u, 0x11046u},
+ {0x11070u, 0x11070u},
+ {0x11073u, 0x11074u},
+ {0x1107fu, 0x11081u},
+ {0x110b3u, 0x110b6u},
+ {0x110b9u, 0x110bau},
+ {0x110c2u, 0x110c2u},
+ {0x11100u, 0x11102u},
+ {0x11127u, 0x1112bu},
+ {0x1112du, 0x11134u},
+ {0x11173u, 0x11173u},
+ {0x11180u, 0x11181u},
+ {0x111b6u, 0x111beu},
+ {0x111c0u, 0x111c0u},
+ {0x111c2u, 0x111c3u},
+ {0x111c9u, 0x111ccu},
+ {0x111cfu, 0x111cfu},
+ {0x1122fu, 0x11231u},
+ {0x11234u, 0x11237u},
+ {0x1123eu, 0x1123eu},
+ {0x11241u, 0x11241u},
+ {0x112dfu, 0x112dfu},
+ {0x112e3u, 0x112eau},
+ {0x11300u, 0x11301u},
+ {0x1133bu, 0x1133cu},
+ {0x1133eu, 0x1133eu},
+ {0x11340u, 0x11340u},
+ {0x1134du, 0x1134du},
+ {0x11357u, 0x11357u},
+ {0x11366u, 0x1136cu},
+ {0x11370u, 0x11374u},
+ {0x113b8u, 0x113b8u},
+ {0x113bbu, 0x113c0u},
+ {0x113c2u, 0x113c2u},
+ {0x113c5u, 0x113c5u},
+ {0x113c7u, 0x113c9u},
+ {0x113ceu, 0x113d2u},
+ {0x113e1u, 0x113e2u},
+ {0x11438u, 0x1143fu},
+ {0x11442u, 0x11444u},
+ {0x11446u, 0x11446u},
+ {0x1145eu, 0x1145eu},
+ {0x114b0u, 0x114b0u},
+ {0x114b3u, 0x114b8u},
+ {0x114bau, 0x114bau},
+ {0x114bdu, 0x114bdu},
+ {0x114bfu, 0x114c0u},
+ {0x114c2u, 0x114c3u},
+ {0x115afu, 0x115afu},
+ {0x115b2u, 0x115b5u},
+ {0x115bcu, 0x115bdu},
+ {0x115bfu, 0x115c0u},
+ {0x115dcu, 0x115ddu},
+ {0x11633u, 0x1163au},
+ {0x1163du, 0x1163du},
+ {0x1163fu, 0x11640u},
+ {0x116abu, 0x116abu},
+ {0x116adu, 0x116adu},
+ {0x116b0u, 0x116b7u},
+ {0x1171du, 0x1171du},
+ {0x1171fu, 0x1171fu},
+ {0x11722u, 0x11725u},
+ {0x11727u, 0x1172bu},
+ {0x1182fu, 0x11837u},
+ {0x11839u, 0x1183au},
+ {0x11930u, 0x11930u},
+ {0x1193bu, 0x1193fu},
+ {0x11941u, 0x11941u},
+ {0x11943u, 0x11943u},
+ {0x119d4u, 0x119d7u},
+ {0x119dau, 0x119dbu},
+ {0x119e0u, 0x119e0u},
+ {0x11a01u, 0x11a0au},
+ {0x11a33u, 0x11a38u},
+ {0x11a3bu, 0x11a3eu},
+ {0x11a47u, 0x11a47u},
+ {0x11a51u, 0x11a56u},
+ {0x11a59u, 0x11a5bu},
+ {0x11a84u, 0x11a96u},
+ {0x11a98u, 0x11a99u},
+ {0x11b60u, 0x11b60u},
+ {0x11b62u, 0x11b64u},
+ {0x11b66u, 0x11b66u},
+ {0x11c30u, 0x11c36u},
+ {0x11c38u, 0x11c3du},
+ {0x11c3fu, 0x11c3fu},
+ {0x11c92u, 0x11ca7u},
+ {0x11caau, 0x11cb0u},
+ {0x11cb2u, 0x11cb3u},
+ {0x11cb5u, 0x11cb6u},
+ {0x11d31u, 0x11d36u},
+ {0x11d3au, 0x11d3au},
+ {0x11d3cu, 0x11d3du},
+ {0x11d3fu, 0x11d47u},
+ {0x11d90u, 0x11d91u},
+ {0x11d95u, 0x11d95u},
+ {0x11d97u, 0x11d97u},
+ {0x11ef3u, 0x11ef4u},
+ {0x11f00u, 0x11f02u},
+ {0x11f36u, 0x11f3au},
+ {0x11f40u, 0x11f42u},
+ {0x11f5au, 0x11f5au},
+ {0x13440u, 0x13440u},
+ {0x13447u, 0x13455u},
+ {0x1611eu, 0x16129u},
+ {0x1612du, 0x1612fu},
+ {0x16af0u, 0x16af4u},
+ {0x16b30u, 0x16b36u},
+ {0x16f4fu, 0x16f4fu},
+ {0x16f8fu, 0x16f92u},
+ {0x16fe4u, 0x16fe4u},
+ {0x16ff0u, 0x16ff1u},
+ {0x1bc9du, 0x1bc9eu},
+ {0x1bca0u, 0x1bca3u},
+ {0x1cf00u, 0x1cf2du},
+ {0x1cf30u, 0x1cf46u},
+ {0x1d165u, 0x1d169u},
+ {0x1d16du, 0x1d182u},
+ {0x1d185u, 0x1d18bu},
+ {0x1d1aau, 0x1d1adu},
+ {0x1d242u, 0x1d244u},
+ {0x1da00u, 0x1da36u},
+ {0x1da3bu, 0x1da6cu},
+ {0x1da75u, 0x1da75u},
+ {0x1da84u, 0x1da84u},
+ {0x1da9bu, 0x1da9fu},
+ {0x1daa1u, 0x1daafu},
+ {0x1e000u, 0x1e006u},
+ {0x1e008u, 0x1e018u},
+ {0x1e01bu, 0x1e021u},
+ {0x1e023u, 0x1e024u},
+ {0x1e026u, 0x1e02au},
+ {0x1e08fu, 0x1e08fu},
+ {0x1e130u, 0x1e136u},
+ {0x1e2aeu, 0x1e2aeu},
+ {0x1e2ecu, 0x1e2efu},
+ {0x1e4ecu, 0x1e4efu},
+ {0x1e5eeu, 0x1e5efu},
+ {0x1e6e3u, 0x1e6e3u},
+ {0x1e6e6u, 0x1e6e6u},
+ {0x1e6eeu, 0x1e6efu},
+ {0x1e6f5u, 0x1e6f5u},
+ {0x1e8d0u, 0x1e8d6u},
+ {0x1e944u, 0x1e94au},
+};
+static const size_t zw_n = 394;
+
+static const uint32_t wd[][2] = {
+ {0x1100u, 0x115fu},
+ {0x17a4u, 0x17a4u},
+ {0x231au, 0x231bu},
+ {0x2329u, 0x232au},
+ {0x23e9u, 0x23ecu},
+ {0x23f0u, 0x23f0u},
+ {0x23f3u, 0x23f3u},
+ {0x25fdu, 0x25feu},
+ {0x2614u, 0x2615u},
+ {0x2630u, 0x2637u},
+ {0x2648u, 0x2653u},
+ {0x267fu, 0x267fu},
+ {0x268au, 0x268fu},
+ {0x2693u, 0x2693u},
+ {0x26a1u, 0x26a1u},
+ {0x26aau, 0x26abu},
+ {0x26bdu, 0x26beu},
+ {0x26c4u, 0x26c5u},
+ {0x26ceu, 0x26ceu},
+ {0x26d4u, 0x26d4u},
+ {0x26eau, 0x26eau},
+ {0x26f2u, 0x26f3u},
+ {0x26f5u, 0x26f5u},
+ {0x26fau, 0x26fau},
+ {0x26fdu, 0x26fdu},
+ {0x2705u, 0x2705u},
+ {0x270au, 0x270bu},
+ {0x2728u, 0x2728u},
+ {0x274cu, 0x274cu},
+ {0x274eu, 0x274eu},
+ {0x2753u, 0x2755u},
+ {0x2757u, 0x2757u},
+ {0x2795u, 0x2797u},
+ {0x27b0u, 0x27b0u},
+ {0x27bfu, 0x27bfu},
+ {0x2b1bu, 0x2b1cu},
+ {0x2b50u, 0x2b50u},
+ {0x2b55u, 0x2b55u},
+ {0x2e80u, 0x2e99u},
+ {0x2e9bu, 0x2ef3u},
+ {0x2f00u, 0x2fd5u},
+ {0x2ff0u, 0x3029u},
+ {0x3030u, 0x303eu},
+ {0x3041u, 0x3096u},
+ {0x309bu, 0x30ffu},
+ {0x3105u, 0x312fu},
+ {0x3131u, 0x3163u},
+ {0x3165u, 0x318eu},
+ {0x3190u, 0x31e5u},
+ {0x31efu, 0x321eu},
+ {0x3220u, 0x3247u},
+ {0x3250u, 0xa48cu},
+ {0xa490u, 0xa4c6u},
+ {0xa960u, 0xa97cu},
+ {0xac00u, 0xd7a3u},
+ {0xf900u, 0xfaffu},
+ {0xfe10u, 0xfe19u},
+ {0xfe30u, 0xfe52u},
+ {0xfe54u, 0xfe66u},
+ {0xfe68u, 0xfe6bu},
+ {0xff01u, 0xff60u},
+ {0xffe0u, 0xffe6u},
+ {0x16fe0u, 0x16fe3u},
+ {0x16ff2u, 0x16ff6u},
+ {0x17000u, 0x18cd5u},
+ {0x18cffu, 0x18d1eu},
+ {0x18d80u, 0x18df2u},
+ {0x1aff0u, 0x1aff3u},
+ {0x1aff5u, 0x1affbu},
+ {0x1affdu, 0x1affeu},
+ {0x1b000u, 0x1b122u},
+ {0x1b132u, 0x1b132u},
+ {0x1b150u, 0x1b152u},
+ {0x1b155u, 0x1b155u},
+ {0x1b164u, 0x1b167u},
+ {0x1b170u, 0x1b2fbu},
+ {0x1d300u, 0x1d356u},
+ {0x1d360u, 0x1d376u},
+ {0x1f004u, 0x1f004u},
+ {0x1f0cfu, 0x1f0cfu},
+ {0x1f18eu, 0x1f18eu},
+ {0x1f191u, 0x1f19au},
+ {0x1f200u, 0x1f202u},
+ {0x1f210u, 0x1f23bu},
+ {0x1f240u, 0x1f248u},
+ {0x1f250u, 0x1f251u},
+ {0x1f260u, 0x1f265u},
+ {0x1f300u, 0x1f320u},
+ {0x1f32du, 0x1f335u},
+ {0x1f337u, 0x1f37cu},
+ {0x1f37eu, 0x1f393u},
+ {0x1f3a0u, 0x1f3cau},
+ {0x1f3cfu, 0x1f3d3u},
+ {0x1f3e0u, 0x1f3f0u},
+ {0x1f3f4u, 0x1f3f4u},
+ {0x1f3f8u, 0x1f43eu},
+ {0x1f440u, 0x1f440u},
+ {0x1f442u, 0x1f4fcu},
+ {0x1f4ffu, 0x1f53du},
+ {0x1f54bu, 0x1f54eu},
+ {0x1f550u, 0x1f567u},
+ {0x1f57au, 0x1f57au},
+ {0x1f595u, 0x1f596u},
+ {0x1f5a4u, 0x1f5a4u},
+ {0x1f5fbu, 0x1f64fu},
+ {0x1f680u, 0x1f6c5u},
+ {0x1f6ccu, 0x1f6ccu},
+ {0x1f6d0u, 0x1f6d2u},
+ {0x1f6d5u, 0x1f6d8u},
+ {0x1f6dcu, 0x1f6dfu},
+ {0x1f6ebu, 0x1f6ecu},
+ {0x1f6f4u, 0x1f6fcu},
+ {0x1f7e0u, 0x1f7ebu},
+ {0x1f7f0u, 0x1f7f0u},
+ {0x1f90cu, 0x1f93au},
+ {0x1f93cu, 0x1f945u},
+ {0x1f947u, 0x1f9ffu},
+ {0x1fa70u, 0x1fa7cu},
+ {0x1fa80u, 0x1fa8au},
+ {0x1fa8eu, 0x1fac6u},
+ {0x1fac8u, 0x1fac8u},
+ {0x1facdu, 0x1fadcu},
+ {0x1fadfu, 0x1faeau},
+ {0x1faefu, 0x1faf8u},
+ {0x20000u, 0x2fffdu},
+};
+static const size_t wd_n = 125;
+
+static int range_has(const uint32_t t[][2], size_t n, uint32_t cp) {
+ size_t lo = 0, hi = n;
+ while (lo < hi) {
+ size_t mid = lo + (hi - lo) / 2;
+ if (t[mid][0] > cp) hi = mid;
+ else if (t[mid][1] < cp) lo = mid + 1;
+ else return 1;
+ }
+ return 0;
+}
+
+static int charw(size_t cp) {
+ if (cp < 0x20 || (cp >= 0x7F && cp <= 0x9F)) return 0; /* C0, DEL, C1 */
+ if (range_has(zw, zw_n, (uint32_t)cp)) return 0;
+ if (range_has(wd, wd_n, (uint32_t)cp)) return 2;
+ return 1;
+}
+
+size_t input_display_len_n(const char *s, size_t n) {
+ size_t width = 0;
+ const char *p = s;
+ const char *end = s + n;
+ while (p < end) {
+ size_t cp;
+ const char *nxt = utf8_next(p, end, &cp);
+ if (cp == 0x1B) {
+ if (nxt >= end) break; /* lone trailing ESC: not rendered */
+ unsigned char second = (unsigned char)*nxt;
+ if (second == '[') {
+ const char *q = nxt + 1;
+ while (q < end) {
+ size_t c2;
+ const char *q2 = utf8_next(q, end, &c2);
+ if (c2 >= 0x40 && c2 <= 0x7E) break;
+ q = q2;
+ }
+ if (q >= end) return width;
+ size_t c3;
+ p = utf8_next(q, end, &c3);
+ continue;
+ }
+ if (second == ']' || second == 'P' || second == '^' || second == 'X' ||
+ second == '_') {
+ /* OSC / DCS / SOS / PM / APC: scan to BEL or ESC \ */
+ int prev_esc = 0;
+ const char *q = nxt + 1;
+ while (q < end) {
+ unsigned char c = (unsigned char)*q;
+ if (c == 0x07 || (prev_esc && c == '\\')) break;
+ prev_esc = (c == 0x1B);
+ q++;
+ }
+ if (q >= end) return width;
+ p = q + 1;
+ continue;
+ }
+ if (second == 'O') {
+ /* SS3: `O` plus one more final byte (e.g. ESC O A). */
+ if (nxt + 1 >= end) return width;
+ size_t c3;
+ p = utf8_next(nxt + 1, end, &c3);
+ continue;
+ }
+ if (second >= 0x20 && second <= 0x7E) {
+ /* Intermediates 0x20..=0x2F then one final byte; a bare
+ * final byte ends the sequence on the spot. */
+ const char *q = nxt + 1;
+ while (q < end) {
+ unsigned char c = (unsigned char)*q;
+ if (c < 0x20 || c > 0x2F) break;
+ q++;
+ }
+ if (q >= end) return width;
+ size_t c3;
+ p = utf8_next(q, end, &c3);
+ continue;
+ }
+ p = nxt; /* other: lone ESC */
+ continue;
+ }
+ if (cp != 0x7F) width += (size_t)charw(cp);
+ p = nxt;
+ }
+ return width;
+}
+
+size_t input_display_len(const char *s) {
+ return input_display_len_n(s, strlen(s));
+}
+
+/* ================================================================== */
+/* Parser */
+/* ================================================================== */
+
+void key_parser_init(key_parser *p) {
+ p->state = KST_GROUND;
+ xbuf_init(&p->seq);
+ xbuf_init(&p->csi_params);
+ p->utf8_len = 0;
+ p->pasting = false;
+}
+
+void key_parser_free(key_parser *p) {
+ xbuf_free(&p->seq);
+ xbuf_free(&p->csi_params);
+ p->state = KST_GROUND;
+ p->utf8_len = 0;
+ p->pasting = false;
+}
+
+static void kp_emit(key_parser *p, evlist *out, int kind, unsigned int cp) {
+ evlist_push(out, (input_key){kind, cp}, (const unsigned char *)p->seq.data,
+ p->seq.len);
+ xbuf_reset(&p->seq);
+ xbuf_reset(&p->csi_params);
+}
+
+static int kp_digit(unsigned char b) {
+ return b >= '0' && b <= '9';
+}
+
+static int first_num(const char *s, unsigned long *out) {
+ unsigned long d = 0;
+ int any = 0;
+ for (const char *p = s; *p >= '0' && *p <= '9'; p++) {
+ if (any) d = d * 10 + (unsigned long)(*p - '0');
+ else
+ d = (unsigned long)(*p - '0');
+ any = 1;
+ if (d > 0xFFFFFFFFUL) break;
+ }
+ if (!any) return 0;
+ *out = d;
+ return 1;
+}
+
+static int csi_u(const char *params, int *key) {
+ const char *semi = strchr(params, ';');
+ size_t first_len = semi ? (size_t)(semi - params) : strlen(params);
+ if (first_len == 0 || first_len > 10) return 0;
+ unsigned long cp = 0;
+ for (size_t i = 0; i < first_len; i++) {
+ cp = cp * 10 + (unsigned long)(params[i] - '0');
+ if (cp > 0xFFFFFFFFUL) return 0;
+ }
+ unsigned long mods = 1;
+ if (semi) {
+ const char *m = semi + 1;
+ size_t mlen = strlen(m);
+ if (mlen > 0 && mlen <= 10) {
+ unsigned long v = 0;
+ int ok = 1;
+ for (size_t i = 0; i < mlen; i++) {
+ if (m[i] < '0' || m[i] > '9') {
+ ok = 0;
+ break;
+ }
+ v = v * 10 + (unsigned long)(m[i] - '0');
+ if (v > 0xFFFFFFFFUL) {
+ ok = 0;
+ break;
+ }
+ }
+ if (ok) mods = v;
+ }
+ }
+ int shift = ((mods - 1UL) & 1UL) == 1UL;
+ int k;
+ switch (cp) {
+ case 9u:
+ k = shift ? KEY_BACKTAB : KEY_TAB;
+ break;
+ case 12u:
+ k = KEY_BACKSPACE;
+ break;
+ case 13u:
+ k = KEY_ENTER;
+ break;
+ case 27u:
+ k = KEY_ESC;
+ break;
+ case 127u:
+ k = KEY_DELETE;
+ break;
+ case 0x2190u:
+ k = KEY_LEFT;
+ break;
+ case 0x2191u:
+ k = KEY_UP;
+ break;
+ case 0x2192u:
+ k = KEY_RIGHT;
+ break;
+ case 0x2193u:
+ k = KEY_DOWN;
+ break;
+ default:
+ return 0;
+ }
+ *key = k;
+ return 1;
+}
+
+static int kp_ground(key_parser *p, unsigned char b, evlist *out);
+
+static size_t utf8_lead_len(unsigned char lead) {
+ if (lead >= 0xF0 && lead < 0xF8) return 4;
+ if (lead >= 0xE0) return 3;
+ if (lead >= 0xC0) return 2;
+ return 1;
+}
+
+static int kp_utf8_step(key_parser *p, unsigned char b, evlist *out) {
+ if (p->utf8_len == 0) {
+ if (b < 0xC0 || b >= 0xF8) {
+ p->utf8_len = 0;
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_UNKNOWN, 0);
+ return 1;
+ }
+ p->utf8[p->utf8_len++] = b;
+ return 0;
+ }
+ if (b >= 0x80 && b < 0xC0) {
+ if (p->utf8_len < 8) p->utf8[p->utf8_len++] = b;
+ size_t expected = utf8_lead_len(p->utf8[0]);
+ if (p->utf8_len < expected) return 0;
+ if (utf8_is_valid((const char *)p->utf8, p->utf8_len)) {
+ size_t cp = 0;
+ utf8_next((const char *)p->utf8, (const char *)p->utf8 + p->utf8_len,
+ &cp);
+ unsigned char tmp[8];
+ size_t n = p->utf8_len;
+ memcpy(tmp, p->utf8, n);
+ p->utf8_len = 0;
+ xbuf_reset(&p->seq);
+ for (size_t i = 0; i < n; i++) xbuf_push(&p->seq, (char)tmp[i]);
+ kp_emit(p, out, KEY_CHAR, cp);
+ return 1;
+ }
+ p->utf8_len = 0;
+ return 0;
+ }
+ p->utf8_len = 0;
+ return kp_ground(p, b, out);
+}
+
+static int kp_ground(key_parser *p, unsigned char b, evlist *out) {
+ if (b == 0x1B) {
+ p->state = KST_ESCAPE;
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ return 0;
+ }
+ if (b == 0x09) {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_TAB, 0);
+ return 1;
+ }
+ if (b == 0x0D || b == 0x0A) {
+ /* CR (0x0D) submits. During bracketed paste every CR/LF is a newline,
+ * preserving multi-line pastes. LF (0x0A, Ctrl+J) outside of paste is
+ * a dead key. Multi-line typing is done with backslash+Enter. */
+ if (p->pasting) {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_NEWLINE, 0);
+ return 1;
+ }
+ if (b == 0x0D) {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_ENTER, 0);
+ return 1;
+ }
+ xbuf_reset(&p->seq); /* discard LF: dead key, no event */
+ return 0;
+ }
+ if (b == 0x7F || b == 0x08) {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_BACKSPACE, 0);
+ return 1;
+ }
+ switch (b) {
+ case 0x01:
+ case 0x03:
+ case 0x04:
+ case 0x05:
+ case 0x0B:
+ case 0x0C:
+ case 0x15:
+ case 0x17:
+ case 0x19:
+ case 0x1F: {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ int k;
+ switch (b) {
+ case 0x01: k = KEY_CTRL_A; break;
+ case 0x03: k = KEY_CTRL_C; break;
+ case 0x04: k = KEY_CTRL_D; break;
+ case 0x05: k = KEY_CTRL_E; break;
+ case 0x0B: k = KEY_CTRL_K; break;
+ case 0x0C: k = KEY_CTRL_L; break;
+ case 0x15: k = KEY_CTRL_U; break;
+ case 0x19: k = KEY_CTRL_Y; break;
+ case 0x1F: k = KEY_UNDO; break;
+ default: k = KEY_CTRL_W; break;
+ }
+ /* A bare Ctrl+C is an interrupt, not pasted text: if a bracketed
+ * paste start (CSI 200~) was received but its 201~ terminator never
+ * arrived, paste mode would otherwise stick and turn every Enter into
+ * a newline. Reset it so the user can submit again. */
+ if (b == 0x03)
+ p->pasting = false;
+ kp_emit(p, out, k, 0);
+ return 1;
+ }
+ default:
+ break;
+ }
+ if (b < 0x20) {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_UNKNOWN, 0);
+ return 1;
+ }
+ if (b < 0x80) {
+ xbuf_reset(&p->seq);
+ xbuf_push(&p->seq, (char)b);
+ kp_emit(p, out, KEY_CHAR, b);
+ return 1;
+ }
+ return kp_utf8_step(p, b, out);
+}
+
+static int kp_escape(key_parser *p, unsigned char b, evlist *out) {
+ p->state = KST_GROUND;
+ xbuf_push(&p->seq, (char)b);
+ if (b == '[') {
+ p->state = KST_CSI;
+ xbuf_reset(&p->csi_params);
+ return 0;
+ }
+ if (b == 'O') {
+ p->state = KST_SS3;
+ return 0;
+ }
+ if (b >= 0x20 && b <= 0x7E) {
+ kp_emit(p, out, KEY_CHAR, b);
+ return 1;
+ }
+ kp_emit(p, out, KEY_ESC, 0);
+ return 1;
+}
+
+static int kp_csi(key_parser *p, unsigned char b, evlist *out) {
+ xbuf_push(&p->seq, (char)b);
+ if (kp_digit(b) || b == ';' || b == '<' || b == '=' || b == '?' || b == '>') {
+ xbuf_push(&p->csi_params, (char)b);
+ return 0;
+ }
+ p->state = KST_GROUND;
+ char *params = xbuf_steal(&p->csi_params);
+ int k = -1;
+ if (b == 'A')
+ k = KEY_UP;
+ else if (b == 'B')
+ k = KEY_DOWN;
+ else if (b == 'C')
+ k = KEY_RIGHT;
+ else if (b == 'D')
+ k = KEY_LEFT;
+ else if (b == 'H')
+ k = KEY_HOME;
+ else if (b == 'F')
+ k = KEY_END;
+ else if (b == 'Z')
+ k = KEY_BACKTAB;
+ else if (b == 'u')
+ k = csi_u(params, &k) ? k : -1;
+ else if (b == '~') {
+ unsigned long n;
+ if (first_num(params, &n)) {
+ switch (n) {
+ case 200u:
+ free(params);
+ p->pasting = true;
+ return 0;
+ case 201u:
+ free(params);
+ p->pasting = false;
+ return 0;
+ case 1u:
+ case 7u:
+ k = KEY_HOME;
+ break;
+ case 3u:
+ k = KEY_DELETE;
+ break;
+ case 4u:
+ case 8u:
+ k = KEY_END;
+ break;
+ case 5u:
+ k = KEY_PAGEUP;
+ break;
+ case 6u:
+ k = KEY_PAGEDOWN;
+ break;
+ default:
+ k = KEY_UNKNOWN;
+ break;
+ }
+ } else {
+ k = KEY_UNKNOWN;
+ }
+ }
+ else if (b == 'R' || b == 'M' || b == 'h' || b == 'l' || b == 'm')
+ k = -1;
+ free(params);
+ if (k < 0) return 0;
+ kp_emit(p, out, k, 0);
+ return 1;
+}
+
+static int kp_ss3(key_parser *p, unsigned char b, evlist *out) {
+ xbuf_push(&p->seq, (char)b);
+ p->state = KST_GROUND;
+ int k;
+ switch (b) {
+ case 'A': k = KEY_UP; break;
+ case 'B': k = KEY_DOWN; break;
+ case 'C': k = KEY_RIGHT; break;
+ case 'D': k = KEY_LEFT; break;
+ case 'H': k = KEY_HOME; break;
+ case 'F': k = KEY_END; break;
+ default: k = KEY_UNKNOWN; break;
+ }
+ kp_emit(p, out, k, 0);
+ return 1;
+}
+
+size_t key_parser_feed(key_parser *p, const unsigned char *in, size_t n, evlist *out) {
+ size_t emitted = 0;
+ for (size_t i = 0; i < n; i++) {
+ unsigned char b = in[i];
+ switch (p->state) {
+ case KST_ESCAPE:
+ emitted += kp_escape(p, b, out);
+ break;
+ case KST_CSI:
+ emitted += kp_csi(p, b, out);
+ break;
+ case KST_SS3:
+ emitted += kp_ss3(p, b, out);
+ break;
+ default:
+ emitted += kp_ground(p, b, out);
+ break;
+ }
+ }
+ return emitted;
+}
+
+int key_parser_flush(key_parser *p, evlist *out) {
+ if (p->state != KST_ESCAPE) return 0;
+ p->state = KST_GROUND;
+ kp_emit(p, out, KEY_ESC, 0);
+ return 1;
+}
+
+/* ================================================================== */
+/* Editor */
+/* ================================================================== */
+
+static size_t char_start(const char *s, size_t pos) {
+ size_t len = strlen(s);
+ if (pos > len) pos = len;
+ size_t j = pos;
+ while (j > 0 && (((unsigned char)s[j - 1]) & 0xC0) == 0x80) j--;
+ return j == 0 ? 0 : j - 1;
+}
+
+static size_t char_end(const char *s, size_t len, size_t pos) {
+ if (pos >= len) return len;
+ size_t cp;
+ const char *nxt = utf8_next(s + pos, s + len, &cp);
+ return (size_t)(nxt - s);
+}
+
+/* Byte offset of the first column of the logical line holding `pos`. */
+static size_t line_start_of(const char *s, size_t pos) {
+ size_t len = strlen(s);
+ if (pos > len) pos = len;
+ size_t i = pos;
+ while (i > 0 && s[i - 1] != '\n') i--;
+ return i;
+}
+
+/* Byte offset of the line terminator ('\n') for the line holding `pos`, or
+ * end-of-string when that line is the last one. */
+static size_t line_end_of(const char *s, size_t pos) {
+ size_t len = strlen(s);
+ if (pos > len) pos = len;
+ size_t i = pos;
+ while (i < len && s[i] != '\n') i++;
+ return i;
+}
+
+/* Physical (row, col) of a byte offset in the buffer, tracking the *visual*
+ * cursor position the way a delayed-wrap terminal (xterm/VTE) actually keeps
+ * it: writing a character into the last column leaves the cursor sitting on
+ * that last column with a pending wrap, the wrap fires only when the *next*
+ * character is written, and a newline clears the pending wrap. So the cursor
+ * never sits on an empty spurious row — a full-width line followed by a
+ * newline does not gain one. Line 0 is prefixed by the 2-column CHAT_PROMPT.
+ * The returned (row, col) is exactly where the terminal cursor is after the
+ * text up to that byte has been printed, so relative cursor moves from it are
+ * reliable. */
+static void input_wrap_caret_impl(const char *text, size_t caret, size_t w,
+ size_t *out_row, size_t *out_col,
+ bool *out_pending) {
+ if (w == 0) w = 1;
+ size_t len = text ? strlen(text) : 0;
+ if (caret > len) caret = len;
+ size_t r = 0, c = 0;
+ int pending = 0;
+#define ADV_COL() do { \
+ if (pending) { pending = 0; r++; c = 0; } \
+ c++; \
+ if (c == w) { pending = 1; c = w - 1; } \
+ } while (0)
+ /* The 2-column prompt is always printed in front of line 0. */
+ ADV_COL();
+ ADV_COL();
+ size_t byte = 0;
+ while (byte < caret) {
+ size_t cp;
+ const char *nxt = utf8_next(text + byte, text + len, &cp);
+ size_t cl = (size_t)(nxt - (text + byte));
+ if (cl == 0) cl = 1;
+ if (cp == '\n') {
+ pending = 0;
+ c = 0;
+ r++;
+ } else {
+ ADV_COL();
+ }
+ byte += cl;
+ }
+#undef ADV_COL
+ if (out_row) *out_row = r;
+ if (out_col) *out_col = c;
+ if (out_pending) *out_pending = (pending != 0);
+}
+
+void input_wrap_caret(const char *text, size_t caret, size_t w,
+ size_t *out_row, size_t *out_col) {
+ input_wrap_caret_impl(text, caret, w, out_row, out_col, NULL);
+}
+
+bool input_wrap_pending(const char *text, size_t caret, size_t w) {
+ bool pending = false;
+ input_wrap_caret_impl(text, caret, w, NULL, NULL, &pending);
+ return pending;
+}
+
+/* Total physical rows the buffer occupies when printed, i.e. the row of the
+ * visual end-of-text cursor plus one. */
+size_t input_wrap_total(const char *text, size_t w) {
+ size_t r, c;
+ const char *t = text ? text : "";
+ input_wrap_caret(t, strlen(t), w, &r, &c);
+ return r + 1;
+}
+
+static char *splice_remove(const char *s, size_t start, size_t end) {
+ size_t len = strlen(s);
+ char *out = (char *) xmalloc(len - (end - start) + 1);
+ memcpy(out, s, start);
+ memcpy(out + start, s + end, len - end);
+ out[len - (end - start)] = '\0';
+ return out;
+}
+
+static size_t word_backward(const char *s, size_t byte) {
+ size_t i = byte;
+ while (i > 0 && s[i - 1] == ' ') i--;
+ while (i > 0 &&
+ (isalnum((unsigned char)s[i - 1]) || s[i - 1] == '_'))
+ i--;
+ while (i > 0 && s[i - 1] == ' ') i--;
+ return i;
+}
+
+#define EDITOR_UNDO_LIMIT 100
+
+void editor_init(editor *e) {
+ e->text = xstrdup("");
+ e->caret = 0;
+ e->killed = NULL;
+ e->undo = NULL;
+ e->undo_len = 0;
+ e->undo_cap = 0;
+}
+
+void editor_free(editor *e) {
+ free(e->text);
+ for (size_t i = 0; i < e->undo_len; i++)
+ free(e->undo[i].text);
+ free(e->undo);
+ free(e->killed);
+ memset(e, 0, sizeof(*e));
+}
+
+/* Snapshot the current buffer onto the bounded undo stack. */
+static void editor_record(editor *e) {
+ if (e->undo_len == e->undo_cap) {
+ size_t ncap = e->undo_cap ? e->undo_cap * 2 : 16;
+ if (ncap > EDITOR_UNDO_LIMIT) {
+ free(e->undo[0].text);
+ memmove(e->undo, e->undo + 1, (e->undo_len - 1) * sizeof(*e->undo));
+ e->undo_len--;
+ } else {
+ e->undo = (editor_undo_entry *) xrealloc(e->undo, ncap * sizeof(*e->undo));
+ e->undo_cap = ncap;
+ }
+ }
+ e->undo[e->undo_len].text = xstrdup(e->text ? e->text : "");
+ e->undo[e->undo_len].caret = e->caret;
+ e->undo_len++;
+}
+
+/* Restore the most recent snapshot. False when there is nothing to undo. */
+static bool editor_undo_pop(editor *e) {
+ if (e->undo_len == 0)
+ return false;
+ e->undo_len--;
+ free(e->text);
+ e->text = e->undo[e->undo_len].text;
+ e->undo[e->undo_len].text = NULL;
+ e->caret = e->undo[e->undo_len].caret;
+ return true;
+}
+
+void editor_clear(editor *e) {
+ e->text[0] = '\0';
+ e->caret = 0;
+}
+
+void editor_set(editor *e, const char *text) {
+ free(e->text);
+ e->text = xstrdup(text);
+ e->caret = strlen(e->text);
+}
+
+char *editor_text(const editor *e) {
+ return xstrdup(e->text);
+}
+
+bool editor_is_empty(const editor *e) {
+ return e->text[0] == '\0';
+}
+
+size_t editor_line_count(const editor *e) {
+ size_t n = 1;
+ for (const char *p = e->text; *p; p++)
+ if (*p == '\n') n++;
+ return n;
+}
+
+size_t editor_caret_line(const editor *e) {
+ size_t n = 0;
+ for (size_t i = 0; i < e->caret && e->text[i]; i++)
+ if (e->text[i] == '\n') n++;
+ return n;
+}
+
+static void ed_insert_str(editor *e, const char *s) {
+ size_t len = strlen(e->text);
+ if (e->caret > len) e->caret = len;
+ size_t ins = strlen(s);
+ char *nt = (char *) xmalloc(len + ins + 1);
+ memcpy(nt, e->text, e->caret);
+ memcpy(nt + e->caret, s, ins);
+ memcpy(nt + e->caret + ins, e->text + e->caret, len - e->caret);
+ nt[len + ins] = '\0';
+ free(e->text);
+ e->text = nt;
+ e->caret += ins;
+}
+
+static void ed_insert_char(editor *e, size_t cp) {
+ char tmp[8];
+ size_t i = 0;
+ if (cp < 0x80)
+ tmp[i++] = (char)cp;
+ else if (cp < 0x800) {
+ tmp[i++] = (char)(0xC0 | (cp >> 6));
+ tmp[i++] = (char)(0x80 | (cp & 0x3F));
+ } else if (cp < 0x10000) {
+ tmp[i++] = (char)(0xE0 | (cp >> 12));
+ tmp[i++] = (char)(0x80 | ((cp >> 6) & 0x3F));
+ tmp[i++] = (char)(0x80 | (cp & 0x3F));
+ } else {
+ tmp[i++] = (char)(0xF0 | (cp >> 18));
+ tmp[i++] = (char)(0x80 | ((cp >> 12) & 0x3F));
+ tmp[i++] = (char)(0x80 | ((cp >> 6) & 0x3F));
+ tmp[i++] = (char)(0x80 | (cp & 0x3F));
+ }
+ tmp[i] = '\0';
+ ed_insert_str(e, tmp);
+}
+
+static void ed_backspace(editor *e) {
+ if (e->caret > 0) {
+ size_t s = char_start(e->text, e->caret);
+ char *killed = xstrndup(e->text + s, e->caret - s);
+ char *nt = splice_remove(e->text, s, e->caret);
+ free(e->text);
+ e->text = nt;
+ e->caret = s;
+ free(e->killed);
+ e->killed = killed;
+ }
+}
+
+static void ed_delete(editor *e) {
+ size_t len = strlen(e->text);
+ if (e->caret < len) {
+ size_t c = char_end(e->text, len, e->caret);
+ char *killed = xstrndup(e->text + e->caret, c - e->caret);
+ char *nt = splice_remove(e->text, e->caret, c);
+ free(e->text);
+ e->text = nt;
+ free(e->killed);
+ e->killed = killed;
+ }
+}
+
+/* Move the caret to the previous (dir<0) or next (dir>0) line, keeping the
+ * column and clamping it to the target line's length on a UTF-8 boundary.
+ * No-op at the top/bottom edge. */
+static void ed_move_line(editor *e, int dir) {
+ const char *s = e->text;
+ size_t len = strlen(s);
+ if (e->caret > len) e->caret = len;
+ size_t start = e->caret;
+ while (start > 0 && s[start - 1] != '\n') start--;
+ size_t col = e->caret - start;
+ if (dir < 0) {
+ if (start == 0) return; /* first line */
+ size_t pstart = start - 1;
+ while (pstart > 0 && s[pstart - 1] != '\n') pstart--;
+ size_t plen = (start - 1) - pstart;
+ size_t c = col < plen ? col : plen;
+ while (c > 0 && (((unsigned char)s[pstart + c]) & 0xC0) == 0x80) c--;
+ e->caret = pstart + c;
+ } else {
+ size_t end = e->caret;
+ while (end < len && s[end] != '\n') end++;
+ if (end >= len) return; /* last line */
+ size_t nstart = end + 1;
+ size_t nend = nstart;
+ while (nend < len && s[nend] != '\n') nend++;
+ size_t nlen = nend - nstart;
+ size_t c = col < nlen ? col : nlen;
+ while (c > 0 && (((unsigned char)s[nstart + c]) & 0xC0) == 0x80) c--;
+ e->caret = nstart + c;
+ }
+}
+
+void editor_apply(editor *e, input_key key, input_action *a) {
+ a->kind = ACT_NONE;
+ a->text = NULL;
+ switch (key.kind) {
+ case KEY_CHAR:
+ editor_record(e);
+ ed_insert_char(e, key.cp);
+ break;
+ case KEY_TAB:
+ editor_record(e);
+ ed_insert_str(e, "\t");
+ break;
+ case KEY_NEWLINE: /* pasted CR/LF: insert a real newline */
+ editor_record(e);
+ ed_insert_str(e, "\n");
+ break;
+ case KEY_ENTER: {
+ /* Backslash + Enter: line continuation, not submit. Universal across
+ * terminals (works where Shift+Enter is not distinguishable from
+ * Enter). The backslash is the char immediately before the caret on
+ * its own line -- so a middle line of a multi-line buffer can be
+ * split, not just the last line. */
+ if (e->caret > 0 && e->text[e->caret - 1] == '\\') {
+ editor_record(e);
+ e->text[e->caret - 1] = '\n';
+ a->kind = ACT_NEWLINE;
+ break;
+ }
+ a->kind = ACT_SUBMIT;
+ a->text = editor_text(e);
+ break;
+ }
+ case KEY_BACKTAB:
+ a->kind = ACT_TOGGLE_MODE;
+ break;
+ case KEY_ESC:
+ a->kind = ACT_STOP;
+ break;
+ case KEY_BACKSPACE:
+ editor_record(e);
+ ed_backspace(e);
+ break;
+ case KEY_DELETE:
+ editor_record(e);
+ ed_delete(e);
+ break;
+ case KEY_LEFT:
+ /* Stay on the current line: the caret never crosses a newline to the
+ * left, so a press at the line start is a no-op. */
+ if (e->caret > line_start_of(e->text, e->caret))
+ e->caret = char_start(e->text, e->caret);
+ break;
+ case KEY_RIGHT:
+ if (e->caret < line_end_of(e->text, e->caret))
+ e->caret = char_end(e->text, strlen(e->text), e->caret);
+ break;
+ case KEY_HOME:
+ case KEY_CTRL_A:
+ e->caret = line_start_of(e->text, e->caret);
+ break;
+ case KEY_END:
+ case KEY_CTRL_E:
+ e->caret = line_end_of(e->text, e->caret);
+ break;
+ case KEY_CTRL_U:
+ editor_record(e);
+ if (e->caret > 0) {
+ char *killed = xstrndup(e->text, e->caret);
+ char *keep = xstrdup(e->text + e->caret);
+ free(e->text);
+ e->text = keep;
+ e->caret = 0;
+ free(e->killed);
+ e->killed = killed;
+ }
+ break;
+ case KEY_CTRL_K: {
+ editor_record(e);
+ size_t len = strlen(e->text);
+ if (e->caret < len) {
+ char *killed = xstrdup(e->text + e->caret);
+ char *keep = xstrndup(e->text, e->caret);
+ free(e->text);
+ e->text = keep;
+ free(e->killed);
+ e->killed = killed;
+ }
+ break;
+ }
+ case KEY_CTRL_W: {
+ editor_record(e);
+ size_t i = word_backward(e->text, e->caret);
+ if (i < e->caret) {
+ char *killed = xstrndup(e->text + i, e->caret - i);
+ char *nt = splice_remove(e->text, i, e->caret);
+ free(e->text);
+ e->text = nt;
+ e->caret = i;
+ free(e->killed);
+ e->killed = killed;
+ }
+ break;
+ }
+ case KEY_CTRL_Y:
+ if (e->killed && e->killed[0]) {
+ editor_record(e);
+ ed_insert_str(e, e->killed);
+ }
+ break;
+ case KEY_UNDO:
+ editor_undo_pop(e);
+ break;
+ case KEY_CTRL_C:
+ if (editor_is_empty(e))
+ a->kind = ACT_QUIT;
+ else {
+ editor_record(e);
+ editor_clear(e);
+ }
+ break;
+ case KEY_CTRL_D:
+ if (editor_is_empty(e))
+ a->kind = ACT_QUIT;
+ else
+ ed_delete(e);
+ break;
+ case KEY_CTRL_L:
+ a->kind = ACT_CLEAR_SCREEN;
+ break;
+ case KEY_UP:
+ ed_move_line(e, -1);
+ break;
+ case KEY_DOWN:
+ ed_move_line(e, +1);
+ break;
+ case KEY_PAGEUP:
+ case KEY_PAGEDOWN:
+ case KEY_UNKNOWN:
+ break;
+ }
+}
+
+/* ================================================================== */
+/* History */
+/* ================================================================== */
+
+input_history *input_history_new(void) {
+ input_history *h = (input_history *) xmalloc(sizeof(*h));
+ sl_init(&h->entries);
+ h->head = 0;
+ h->active = false;
+ h->index = 0;
+ h->snapshot = NULL;
+ h->path = NULL;
+ return h;
+}
+
+void input_history_free(input_history *h) {
+ sl_free(&h->entries);
+ free(h->snapshot);
+ free(h->path);
+ free(h);
+}
+
+void input_history_reset(input_history *h) {
+ h->active = false;
+ h->index = 0;
+ free(h->snapshot);
+ h->snapshot = NULL;
+}
+
+static size_t hist_live(input_history *h) {
+ return h->entries.len > h->head ? h->entries.len - h->head : 0;
+}
+
+static void history_evict_head(input_history *h) {
+ h->head++;
+ /* Amortized O(1): once half the storage is dead, free the dead prefix
+ * and slide the live entries to the front (absolute `index` moves with
+ * them). Evicted-but-uncompacted entries are freed by sl_free. */
+ if (h->head * 2 >= h->entries.len) {
+ size_t live = h->entries.len - h->head;
+ for (size_t i = 0; i < h->head; i++)
+ free(h->entries.items[i]);
+ memmove(h->entries.items, h->entries.items + h->head,
+ live * sizeof(*h->entries.items));
+ h->entries.len = live;
+ if (h->active)
+ h->index -= h->head;
+ h->head = 0;
+ }
+}
+
+static int make_parent_dirs(const char *path) {
+ char *copy = xstrdup(path);
+ for (char *p = copy + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ if (copy[0] && mkdir(copy, 0755) != 0 && errno != EEXIST) {
+ free(copy);
+ return -1;
+ }
+ *p = '/';
+ }
+ }
+ free(copy);
+ return 0;
+}
+
+bool input_history_persist(input_history *h) {
+ if (!h->path) return false;
+ cJSON *arr = cJSON_CreateArray();
+ for (size_t i = h->head; i < h->entries.len; i++)
+ cJSON_AddItemToArray(arr, cJSON_CreateString(h->entries.items[i]));
+ char *s = cJSON_PrintUnformatted(arr);
+ cJSON_Delete(arr);
+ char *err = NULL;
+ bool ok = make_parent_dirs(h->path) == 0 &&
+ write_file_from_buf(h->path, s, strlen(s), &err) == 0;
+ free(err);
+ free(s);
+ return ok;
+}
+
+input_history *input_history_open(const char *path) {
+ input_history *h = input_history_new();
+ h->path = xstrdup(path);
+ char *data = NULL;
+ size_t len = 0;
+ char *err = NULL;
+ int rc = read_file_to_buf(path, &data, &len, &err);
+ if (rc == 0) {
+ cJSON *arr = data ? cJSON_Parse(data) : NULL;
+ free(data);
+ if (arr && cJSON_IsArray(arr)) {
+ int n = cJSON_GetArraySize(arr);
+ for (int i = 0; i < n; i++) {
+ cJSON *it = cJSON_GetArrayItem(arr, i);
+ if (cJSON_IsString(it) && it->valuestring)
+ sl_push(&h->entries, xstrdup(it->valuestring));
+ }
+ }
+ cJSON_Delete(arr);
+ } else {
+ free(err);
+ if (errno != ENOENT) {
+ input_history_free(h);
+ return NULL;
+ }
+ }
+ while (hist_live(h) > LLSH_HISTORY_LIMIT) history_evict_head(h);
+ return h;
+}
+
+void input_history_push(input_history *h, const char *text) {
+ char *trimmed = xstr_trim_dup(text);
+ if (!trimmed[0]) {
+ free(trimmed);
+ input_history_reset(h);
+ return;
+ }
+ if (hist_live(h) > 0) {
+ const char *last = h->entries.items[h->entries.len - 1];
+ if (xstreq(last, trimmed)) {
+ free(trimmed);
+ input_history_reset(h);
+ return;
+ }
+ }
+ if (hist_live(h) == LLSH_HISTORY_LIMIT) history_evict_head(h);
+ sl_push(&h->entries, trimmed);
+ input_history_reset(h);
+ input_history_persist(h);
+}
+
+char *input_history_walk(input_history *h, int dir, size_t caret_line,
+ const char *current) {
+ if (dir < 0) {
+ /* Up: walk newest -> oldest. While composing (not active) engage
+ * only from the first line so a mid-buffer Up still moves the caret;
+ * the first Up snapshots the line being edited so a Down can restore
+ * it. Mid-walk (active) the press always advances, regardless of
+ * where the caret now sits. */
+ if (!h->active) {
+ if (hist_live(h) == 0 || caret_line != 0)
+ return NULL;
+ free(h->snapshot);
+ h->snapshot = xstrdup(current ? current : "");
+ h->active = true;
+ h->index = h->entries.len - 1;
+ } else if (h->index == h->head) {
+ return NULL; /* oldest already shown: hand back to the caret */
+ } else {
+ h->index -= 1;
+ }
+ return xstrdup(h->entries.items[h->index]);
+ }
+
+ /* Down: only meaningful mid-walk. Step back toward the newest entry, or,
+ * past the newest, hand back the pre-recall line and end the walk.
+ * While composing a Down never recalls — it moves the caret. */
+ if (!h->active)
+ return NULL;
+ if (h->index + 1 < h->entries.len) {
+ h->index += 1;
+ return xstrdup(h->entries.items[h->index]);
+ }
+ h->active = false;
+ char *s = h->snapshot;
+ h->snapshot = NULL;
+ return s;
+}
+
+/* ================================================================== */
+/* Terminal */
+/* ================================================================== */
+
+int input_isatty(void) {
+ return isatty(STDIN_FILENO);
+}
+
+int input_setup_terminal(void) {
+ struct termios orig;
+ if (tcgetattr(STDIN_FILENO, &orig) != 0) return -1;
+ struct termios raw = orig;
+ raw.c_iflag &= ~((tcflag_t)(BRKINT | ICRNL | INPCK | ISTRIP | IXON));
+ raw.c_lflag &= ~((tcflag_t)(ECHO | ICANON | IEXTEN | ISIG));
+ raw.c_oflag &= ~((tcflag_t)OPOST);
+ raw.c_cc[VMIN] = 1;
+ raw.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) != 0) return -1;
+ struct termios t;
+ if (tcgetattr(STDIN_FILENO, &t) == 0) {
+ t.c_oflag |= (tcflag_t)(OPOST | ONLCR);
+ (void)tcsetattr(STDIN_FILENO, TCSANOW, &t);
+ }
+ const char *init = "\x1b[>1u\x1b[?2004h";
+ (void)!write(STDOUT_FILENO, init, strlen(init));
+ fflush(stdout);
+ return 0;
+}
+
+void input_teardown_terminal(void) {
+ const char *fin = "\x1b[?2004l\x1b[>0u";
+ (void)!write(STDOUT_FILENO, fin, strlen(fin));
+ fflush(stdout);
+ struct termios t;
+ if (tcgetattr(STDIN_FILENO, &t) != 0) return;
+ t.c_lflag |= (tcflag_t)(ICANON | ECHO | ISIG | IEXTEN);
+ t.c_iflag |= (tcflag_t)(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ t.c_oflag |= (tcflag_t)(OPOST | ONLCR);
+ t.c_cc[VMIN] = 1;
+ t.c_cc[VTIME] = 0;
+ (void)tcsetattr(STDIN_FILENO, TCSANOW, &t);
+}
+
+/* ================================================================== */
+/* Reader */
+/* ================================================================== */
+
+struct input_key_reader {
+ reader_ctx *ctx;
+ pthread_t tid;
+ int started;
+ volatile int stop;
+ int exit;
+ pthread_mutex_t qlock;
+ evlist q;
+};
+
+input_key_reader *input_key_reader_new(reader_ctx *ctx) {
+ input_key_reader *r = (input_key_reader *) xmalloc(sizeof(*r));
+ memset(r, 0, sizeof(*r));
+ r->ctx = ctx;
+ pthread_mutex_init(&r->qlock, NULL);
+ evlist_init(&r->q);
+ return r;
+}
+
+void input_key_reader_send(input_key_reader *r, const input_key *k) {
+ pthread_mutex_lock(&r->qlock);
+ evlist_push(&r->q, *k, (const unsigned char *)"", 0);
+ pthread_mutex_unlock(&r->qlock);
+}
+
+void input_key_reader_send_raw(input_key_reader *r, const input_key *k,
+ const char *raw) {
+ pthread_mutex_lock(&r->qlock);
+ evlist_push(&r->q, *k, (const unsigned char *)raw, strlen(raw));
+ pthread_mutex_unlock(&r->qlock);
+}
+
+bool input_key_reader_recv(input_key_reader *r, input_key *out,
+ const unsigned char **raw, size_t *raw_len) {
+ pthread_mutex_lock(&r->qlock);
+ if (r->q.len == 0) {
+ pthread_mutex_unlock(&r->qlock);
+ return false;
+ }
+ *out = r->q.items[r->q.head];
+ size_t n = r->q.lens[r->q.head];
+ if (raw) {
+ /* The queue frees its slot on pop, so hand the caller an owned copy;
+ * a borrowed pointer here was freed before the caller used it. */
+ unsigned char *copy = (unsigned char* ) xmalloc(n + 1);
+ memcpy(copy, r->q.raws[r->q.head], n);
+ copy[n] = '\0';
+ *raw = copy;
+ }
+ if (raw_len) *raw_len = n;
+ evlist_pop(&r->q);
+ pthread_mutex_unlock(&r->qlock);
+ return true;
+}
+
+void input_raw_free(const unsigned char *raw) {
+ free((void *)raw);
+}
+
+/* Queue the event and, if set, invoke the direct hook.
+ * Returns non-zero to stop reading (receiver gone). */
+static int reader_dispatch(input_key_reader *r, const input_key *k,
+ const unsigned char *raw, size_t raw_len) {
+ pthread_mutex_lock(&r->qlock);
+ evlist_push(&r->q, *k, raw, raw_len);
+ pthread_mutex_unlock(&r->qlock);
+ if (r->ctx && r->ctx->send) return r->ctx->send(r, k, raw, raw_len);
+ return 0;
+}
+
+static void *reader_thread(void *arg) {
+ input_key_reader *r = (input_key_reader *) arg;
+ unsigned char buf[512];
+ for (;;) {
+ if (r->stop) break;
+ struct pollfd pfd = {.fd = STDIN_FILENO, .events = POLLIN, .revents = 0};
+ int pr = poll(&pfd, 1, 50);
+ if (pr < 0) {
+ if (errno == EINTR) continue;
+ break;
+ }
+ if (pr == 0) continue;
+ if ((pfd.revents & (POLLIN | POLLHUP | POLLERR)) == 0) continue;
+ ssize_t n = 0;
+ do {
+ n = read(STDIN_FILENO, buf, sizeof buf);
+ } while (n < 0 && errno == EINTR);
+ if (n < 0) break;
+ if (n == 0) break; /* EOF: no synthetic Ctrl-D */
+ evlist evs;
+ evlist_init(&evs);
+ key_parser_feed(r->ctx->parser, buf, (size_t)n, &evs);
+ for (size_t j = 0; j < evs.len; j++) {
+ if (reader_dispatch(r, &evs.items[j], evs.raws[j],
+ evs.lens[j]) != 0) {
+ evlist_free(&evs);
+ r->exit = 1;
+ return NULL;
+ }
+ }
+ evlist_free(&evs);
+ key_parser_flush(r->ctx->parser, &evs);
+ for (size_t j = 0; j < evs.len; j++) {
+ if (reader_dispatch(r, &evs.items[j], evs.raws[j],
+ evs.lens[j]) != 0) {
+ evlist_free(&evs);
+ r->exit = 1;
+ return NULL;
+ }
+ }
+ evlist_free(&evs);
+ }
+ r->exit = 1;
+ return NULL;
+}
+
+int input_key_reader_start(input_key_reader *r) {
+ r->stop = 0;
+ r->exit = 0;
+ if (pthread_create(&r->tid, NULL, reader_thread, r) != 0) return -1;
+ r->started = 1;
+ return 0;
+}
+
+void input_key_reader_stop(input_key_reader *r) {
+ if (!r->started) return;
+ r->stop = 1;
+ /* The thread notices stop within one poll timeout (50 ms). */
+ pthread_join(r->tid, NULL);
+ r->started = 0;
+}
+
+void input_key_reader_free(input_key_reader *r) {
+ input_key_reader_stop(r);
+ evlist_free(&r->q);
+ pthread_mutex_destroy(&r->qlock);
+ if (r->ctx) {
+ if (r->ctx->parser) {
+ key_parser_free(r->ctx->parser);
+ free(r->ctx->parser);
+ }
+ free(r->ctx);
+ }
+ free(r);
+}
+
+/* ================================================================== */
+/* Shell shadow */
+/* ================================================================== */
+
+char *shell_shadow_apply(editor *shadow, input_key key) {
+ input_action a;
+ memset(&a, 0, sizeof(a));
+ editor_apply(shadow, key, &a);
+ if (a.kind == ACT_SUBMIT && a.text) {
+ size_t n;
+ xtrim_span(a.text, strlen(a.text), &n);
+ if (n > 0) {
+ char *text = a.text;
+ editor_clear(shadow);
+ a.text = NULL;
+ return text;
+ }
+ }
+ input_action_free(&a);
+ return NULL;
+}
+
+char *input_history_path_for(const char *config_dir) {
+ size_t n = strlen(config_dir);
+ char *p = (char *) xmalloc(n + 1 + 14); /* "/llsh/history\0" */
+ memcpy(p, config_dir, n);
+ memcpy(p + n, "/llsh/history\0", 15);
+ return p;
+}
+
+char *input_default_config_dir(void) {
+ const char *xdg = getenv("XDG_CONFIG_HOME");
+ if (xdg && xdg[0] == '/') return xstrdup(xdg);
+ const char *home = getenv("HOME");
+ if (home && home[0]) {
+ size_t n = strlen(home);
+ char *p = (char *) xmalloc(n + 1 + 8); /* "/.config\0" */
+ memcpy(p, home, n);
+ memcpy(p + n, "/.config\0", 9);
+ return p;
+ }
+ return xstrdup("/tmp");
+}
+
+char *input_default_history_path(void) {
+ char *dir = input_default_config_dir();
+ char *p = input_history_path_for(dir);
+ free(dir);
+ return p;
+}