#include "input.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* ================================================================== */ /* 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; }