diff options
Diffstat (limited to 'src/input.cpp')
| -rw-r--r-- | src/input.cpp | 1949 |
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; +} |