1 /*
2 * Arcan Text-Oriented User Interface Library, Extensions
3 * Copyright: 2019-2021, Bjorn Stahl
4 * License: 3-clause BSD
5 * Description: Implementation of a readline/linenoise replacement.
6 * Missing:
7 * Vim mode
8 *
9 * Search Through History (if caller sets history buffer)
10 * - temporary override prompt with state, hide cursor, ...
11 *
12 * Multiline support
13 * Completion popup
14 * -
15 *
16 * Respect geohint (LTR, RTL, double-width)
17 *
18 * Undo- buffer
19 * - (just copy on modification into a window of n buffers, undo/redo pick)
20 *
21 * Accessibility subwindow
22 *
23 * .readline rc file
24 *
25 * State/Preference persistance?
26 */
27 #include <unistd.h>
28 #include <fcntl.h>
29 #include <inttypes.h>
30 #include <stdint.h>
31
32 #include "../../arcan_shmif.h"
33 #include "../../arcan_tui.h"
34 #include "../../arcan_tui_readline.h"
35
36 #define READLINE_MAGIC 0xfefef00d
37
38 struct readline_meta {
39 uint32_t magic;
40 struct tui_readline_opts opts;
41
42 /* re-built on resize */
43 size_t start_col, stop_col, start_row, stop_row;
44
45 char* work; /* UTF-8 */
46 size_t work_ofs; /* in bytes, code-point boundary aligned */
47 size_t work_len; /* in code-points */
48 size_t work_sz; /* in bytes */
49 size_t cursor; /* offset in bytes from start to cursor */
50
51 /* -1 as ok, modified by verify callback */
52 ssize_t broken_offset;
53
54 /* provided by callback in opts or through setter functions */
55 uint8_t* autocomplete;
56
57 /* if we overfit-, this might be drawn with middle truncated to 2/3 of capacity */
58 const struct tui_cell* prompt;
59 size_t prompt_len;
60
61 int finished;
62
63 /* restore on release */
64 struct tui_cbcfg old_handlers;
65 int old_flags;
66 };
67
68 /* generic 'insert at cursor' */
69 static void add_input(
70 struct tui_context* T, struct readline_meta* M, const char* u8, size_t len);
71
refresh(struct tui_context * T,struct readline_meta * M)72 static void refresh(struct tui_context* T, struct readline_meta* M)
73 {
74 size_t rows, cols;
75 arcan_tui_dimensions(T, &rows, &cols);
76
77 /* first redraw everything so that we are sure we have synched contents */
78 if (M->old_handlers.recolor){
79 M->old_handlers.recolor(T, M->old_handlers.tag);
80 }
81
82 /* these are resolved on resize and calls to update margin */
83 size_t x1 = M->start_col;
84 size_t x2 = M->stop_col;
85 size_t y1 = M->start_row;
86 size_t y2 = M->stop_row;
87
88 /* can't do nothing if we don't have the space */
89 if (x1 > x2 || y1 > y2)
90 return;
91
92 size_t cx = 0, cy = 0;
93 size_t limit = (x2 - x1) + 1;
94 if (limit < 3)
95 return;
96
97 arcan_tui_move_to(T, x1, y1);
98
99 /* standard prompt attribute */
100 /* then error alert if we have a bad offset */
101 struct tui_screen_attr alert = {
102 .aflags = TUI_ATTR_COLOR_INDEXED,
103 .fc[0] = TUI_COL_WARNING,
104 .bc[0] = TUI_COL_WARNING
105 };
106
107 /* reset our reserved range to the default attribute as that might have
108 * a different background style in order to indicate 'input' field */
109 arcan_tui_erase_region(T, x1, y1, x2, y2, NULL);
110
111 /* if we don't need to truncate or slide the edit window,
112 * or do multiline, things are 'easy' */
113 size_t prompt_len = M->prompt_len;
114
115 /* cur allocation down to match ~1/3 of the available space, suffix
116 * with ..> */
117 size_t ul = limit / 3;
118 if (prompt_len > ul && prompt_len + M->work_len > limit){
119
120 if (ul > 2){
121 for (size_t i = 0; i < ul - 2 && i < prompt_len; i++, limit--)
122 arcan_tui_write(T, M->prompt[i].ch, &M->prompt[i].attr);
123 }
124
125 arcan_tui_write(T, '.', NULL);
126 arcan_tui_write(T, '.', NULL);
127 limit -= 2;
128 }
129 /* draw prompt like normal */
130 else {
131 for (size_t i = 0; i < M->prompt_len; i++){
132 arcan_tui_write(T, M->prompt[i].ch, &M->prompt[i].attr);
133 }
134
135 limit = limit - prompt_len;
136 }
137
138 /* if the core text does not fit, start with the cursor position, scan forwards
139 * and backwards until filled - this is preped to be content dependent for
140 * double-width etc. later */
141 size_t pos = 0;
142 if (M->work_len > limit){
143 pos = M->cursor;
144 size_t tail = M->cursor;
145 size_t count = limit;
146
147 while(count){
148 if (pos){
149 while(pos && (M->work[--pos] & 0xc0) == 0x80){}
150 count--;
151 }
152
153 if (count && tail < M->work_ofs){
154 while(tail < M->work_ofs && (M->work[++tail] & 0xc0) == 0x80){}
155 count--;
156 }
157 }
158 }
159
160 for (size_t i = 0; i < M->work_len && i < limit; i++){
161 uint32_t ch;
162 if (pos == M->cursor){
163 arcan_tui_cursorpos(T, &cx, &cy);
164 }
165 ssize_t step = arcan_tui_utf8ucs4((char*) &M->work[pos], &ch);
166
167 pos += step;
168 arcan_tui_write(T, ch,
169 M->broken_offset != -1 && pos >= M->broken_offset ? &alert : NULL);
170 }
171
172 if (cx)
173 arcan_tui_move_to(T, cx, cy);
174 }
175
validate_context(struct tui_context * T,struct readline_meta ** M)176 static bool validate_context(struct tui_context* T, struct readline_meta** M)
177 {
178 if (!T)
179 return false;
180
181 struct tui_cbcfg handlers;
182 arcan_tui_update_handlers(T, NULL, &handlers, sizeof(struct tui_cbcfg));
183
184 struct readline_meta* ch = handlers.tag;
185 if (!ch || ch->magic != READLINE_MAGIC)
186 return false;
187
188 *M = ch;
189 return true;
190 }
191
step_cursor_left(struct tui_context * T,struct readline_meta * M)192 static void step_cursor_left(struct tui_context* T, struct readline_meta* M)
193 {
194 while(M->cursor && M->work && (M->work[--M->cursor] & 0xc0) == 0x80){}
195 }
196
step_cursor_right(struct tui_context * T,struct readline_meta * M)197 static void step_cursor_right(struct tui_context* T, struct readline_meta* M)
198 {
199 while(M->cursor < M->work_ofs && (M->work[++M->cursor] & 0xc0) == 0x80){}
200 }
201
delete_at_cursor(struct tui_context * T,struct readline_meta * M)202 static bool delete_at_cursor(struct tui_context* T, struct readline_meta* M)
203 {
204 return true;
205 }
206
erase_at_cursor(struct tui_context * T,struct readline_meta * M)207 static bool erase_at_cursor(struct tui_context* T, struct readline_meta* M)
208 {
209 if (!M->cursor || !M->work_len)
210 return true;
211
212 /* sweep to previous utf8 start */
213 size_t c_cursor = M->cursor - 1;
214 while (c_cursor && (M->work[c_cursor] & 0xc0) == 0x80)
215 c_cursor--;
216
217 size_t len = M->cursor - c_cursor;
218
219 /* and either '0 out' if at end, or slide */
220 if (M->cursor == M->work_ofs){
221 memset(&M->work[c_cursor], '\0', len);
222 }
223 else
224 memmove(&M->work[c_cursor], &M->work[M->cursor], M->work_ofs - M->cursor);
225
226 M->cursor = c_cursor;
227 M->work_len--;
228 M->work_ofs -= len;
229
230 /* check if we are broken at some offset */
231 if (M->opts.verify){
232 M->broken_offset = M->opts.verify((const char*)M->work, M->old_handlers.tag);
233 }
234
235 refresh(T, M);
236
237 return true;
238 }
239
add_linefeed(struct tui_context * T,struct readline_meta * M)240 static bool add_linefeed(struct tui_context* T, struct readline_meta* M)
241 {
242 if (!M->opts.multiline){
243 /* if normal validation has refused it, don't allow commit */
244 if (-1 != M->broken_offset){
245 return true;
246 }
247 /* treat as commit */
248 M->finished = 1;
249 }
250
251 return true;
252 }
253
delete_last_word(struct tui_context * T,struct readline_meta * M)254 static void delete_last_word(struct tui_context* T, struct readline_meta* M)
255 {
256 /* start from current cursor position, find the first non-space, then find the
257 * beginning or the next space and memset + memmove from there */
258 }
259
cut_to_eol(struct tui_context * T,struct readline_meta * M)260 static void cut_to_eol(struct tui_context* T, struct readline_meta* M)
261 {
262 if (!M->work)
263 return;
264
265 arcan_tui_copy(T, &M->work[M->cursor]);
266
267 M->work[M->cursor] = '\0';
268 M->work_ofs = M->cursor;
269
270 M->work_len = 0;
271 size_t pos = 0;
272 while (pos < M->cursor){
273 if ((M->work[pos++] & 0xc0) != 0x80)
274 M->work_len++;
275 }
276
277 refresh(T, M);
278 }
279
cut_to_sol(struct tui_context * T,struct readline_meta * M)280 static void cut_to_sol(struct tui_context* T, struct readline_meta* M)
281 {
282 if (!M->work)
283 return;
284
285 memmove(M->work, &M->work[M->cursor], M->work_ofs - M->cursor);
286 M->work[M->cursor] = '\0';
287 arcan_tui_copy(T, M->work);
288 M->work_ofs = M->cursor;
289 M->cursor = 0;
290
291 M->work_len = 0;
292 size_t pos = 0;
293 while (pos < M->cursor){
294 if ((M->work[pos++] & 0xc0) != 0x80)
295 M->work_len++;
296 }
297
298 refresh(T, M);
299 }
300
on_utf8_paste(struct tui_context * T,const uint8_t * u8,size_t len,bool cont,void * tag)301 static void on_utf8_paste(
302 struct tui_context* T, const uint8_t* u8, size_t len, bool cont, void* tag)
303 {
304 struct readline_meta* M;
305 if (!validate_context(T, &M))
306 return;
307
308 /* split up into multiple calls based on filter character if needed */
309 if (M->opts.filter_character){
310 for (size_t i = 0; i < len;){
311 uint32_t ch;
312 size_t step = arcan_tui_utf8ucs4((char*)&u8[i], &ch);
313 if (M->opts.filter_character(ch, M->work_len + 1, M->old_handlers.tag))
314 add_input(T, M, (char*)&u8[i], step);
315 i += step;
316 }
317 }
318 else {
319 add_input(T, M, (char*)u8, len);
320 }
321
322 refresh(T, M);
323 }
324
on_utf8_input(struct tui_context * T,const char * u8,size_t len,void * tag)325 static bool on_utf8_input(
326 struct tui_context* T, const char* u8, size_t len, void* tag)
327 {
328 struct readline_meta* M;
329 if (!validate_context(T, &M))
330 return true;
331
332 /* if it is a commit, refuse on validation failure - 0-length should
333 * be filtered through filter_character so no reason to forward it here */
334 if (*u8 == '\n' || *u8 == '\r')
335 return add_linefeed(T, M);
336
337 /* backspace */
338 else if (*u8 == 0x08)
339 return erase_at_cursor(T, M);
340
341 /* first filter things out */
342 if (M->opts.filter_character){
343 uint32_t ch;
344 arcan_tui_utf8ucs4(u8, &ch);
345
346 if (!M->opts.filter_character(ch, M->work_len + 1, M->old_handlers.tag)){
347 return true;
348 }
349 }
350
351 add_input(T, M, u8, len);
352
353 /* missing - if we have a valid suggestion popup, rebuild it with
354 * the new filter-set and reset the cursor in the popup to the current position */
355
356 refresh(T, M);
357 return true;
358 }
359
on_key_input(struct tui_context * T,uint32_t keysym,uint8_t scancode,uint8_t mods,uint16_t subid,void * tag)360 void on_key_input(struct tui_context* T,
361 uint32_t keysym, uint8_t scancode, uint8_t mods, uint16_t subid, void* tag)
362 {
363 struct readline_meta* M;
364 if (!validate_context(T, &M))
365 return;
366
367 bool meta = mods & (TUIM_LCTRL | TUIM_RCTRL);
368 if (meta){
369 if (keysym == TUIK_RETURN){
370 M->finished = 1;
371 }
372 else if (keysym == TUIK_L){
373 arcan_tui_readline_reset(T);
374 }
375 /* delete at/right of cursor, same as DELETE, if line is empty,
376 * as escape with hint to exit */
377 else if (keysym == TUIK_D){
378 }
379 /* swap with previous */
380 else if (keysym == TUIK_T){
381 }
382 else if (keysym == TUIK_B){
383 step_cursor_left(T, M);
384 refresh(T, M);
385 }
386 else if (keysym == TUIK_F){
387 step_cursor_right(T, M);
388 refresh(T, M);
389 }
390 else if (keysym == TUIK_K){
391 cut_to_eol(T, M);
392 }
393 else if (keysym == TUIK_U){
394 cut_to_sol(T, M);
395 }
396 /* step previous in history */
397 else if (keysym == TUIK_P){
398 }
399 /* step next in history */
400 else if (keysym == TUIK_N){
401 }
402 else if (keysym == TUIK_A){
403 /* start of line, same as HOME */
404 M->cursor = 0;
405 refresh(T, M);
406 }
407 else if (keysym == TUIK_E){
408 /* end of line, same as END */
409 M->cursor = M->work_ofs;
410 refresh(T, M);
411 }
412 else if (keysym == TUIK_W){
413 /* delete last word */
414 delete_last_word(T, M);
415 }
416 return;
417 }
418
419 if (keysym == TUIK_LEFT){
420 step_cursor_left(T, M);
421 refresh(T, M);
422 }
423
424 else if (keysym == TUIK_RIGHT){
425 refresh(T, M);
426 step_cursor_right(T, M);
427 }
428
429 else if (keysym == TUIK_ESCAPE && M->opts.allow_exit){
430 arcan_tui_readline_reset(T);
431 M->finished = -1;
432 }
433
434 else if (keysym == TUIK_BACKSPACE)
435 erase_at_cursor(T, M);
436
437 else if (keysym == TUIK_DELETE)
438 delete_at_cursor(T, M);
439
440 /* finish or if multi-line and meta-held, add '\n' */
441 else if (keysym == TUIK_RETURN)
442 add_linefeed(T, M);
443 }
444
add_input(struct tui_context * T,struct readline_meta * M,const char * u8,size_t len)445 static void add_input(
446 struct tui_context* T, struct readline_meta* M, const char* u8, size_t len)
447 {
448 /* grow / reallocate, keep space for NUL */
449 if (M->work_ofs + len + 1 >= M->work_sz){
450 size_t cols;
451 arcan_tui_dimensions(T, NULL, &cols);
452 char* new_buf = realloc(M->work, M->work_sz + cols + 1);
453 if (NULL == new_buf){
454 return;
455 }
456 M->work = new_buf;
457 M->work_sz += cols + 1;
458 M->work[M->work_ofs] = '\0';
459 }
460
461 /* add the input, move the cursor if we are at the end */
462 if (M->cursor == M->work_ofs){
463 M->cursor += len;
464 memcpy(&M->work[M->work_ofs], u8, len);
465 M->work[M->cursor] = '\0';
466 }
467
468 /* slide buffer to the right if inserting in the middle */
469 else {
470 memmove(
471 &M->work[M->cursor + len],
472 &M->work[M->cursor],
473 M->work_ofs - M->cursor
474 );
475
476 memcpy(&M->work[M->cursor], u8, len);
477 M->cursor += len;
478 }
479
480 /* check if we are broken at some offset */
481 if (M->opts.verify){
482 M->broken_offset = M->opts.verify((const char*)M->work, M->old_handlers.tag);
483 }
484
485 /* number of code points have changed, now we need to convert to logical pos */
486 size_t pos = 0;
487 size_t count = 0;
488 while (pos < len){
489 if ((u8[pos++] & 0xc0) != 0x80)
490 count++;
491 }
492
493 M->work_ofs += len;
494 M->work_len += count;
495 }
496
on_mouse_button_input(struct tui_context * T,int x,int y,int button,bool active,int modifiers,void * tag)497 static void on_mouse_button_input(struct tui_context* T,
498 int x, int y, int button, bool active, int modifiers, void* tag)
499 {
500 struct readline_meta* M;
501 if (!validate_context(T, &M) || !active)
502 return;
503
504 if (!(
505 x >= M->start_col && x <= M->stop_col &&
506 y >= M->start_row && y <= M->stop_row)){
507 if (M->opts.allow_exit)
508 M->finished = -1;
509 return;
510 }
511
512 size_t cx, cy;
513 arcan_tui_cursorpos(T, &cx, &cy);
514
515 size_t w = M->stop_col - M->start_col;
516 size_t c_ofs = (cx - M->start_col) + (cy - M->start_row) * w;
517 size_t m_ofs = (x - M->start_col) + (y - M->start_row) * w;
518
519 if (m_ofs == c_ofs){
520 return;
521 }
522 else if (m_ofs > c_ofs){
523 for (size_t i = 0; i < m_ofs - c_ofs; i++)
524 step_cursor_right(T, M);
525 }
526 else {
527 for (size_t i = 0; i < c_ofs - m_ofs; i++)
528 step_cursor_left(T, M);
529 }
530 refresh(T, M);
531 }
532
533 /*
534 * accessor if we are running in manual mode
535 */
arcan_tui_readline_region(struct tui_context * T,size_t x1,size_t y1,size_t x2,size_t y2)536 void arcan_tui_readline_region(
537 struct tui_context* T, size_t x1, size_t y1, size_t x2, size_t y2)
538 {
539 struct readline_meta* M;
540 if (!validate_context(T, &M))
541 return;
542
543 M->start_col = x1;
544 M->stop_col = x2;
545 M->start_row = y1;
546 M->stop_row = y2;
547 }
548
on_recolor(struct tui_context * T,void * tag)549 static void on_recolor(struct tui_context* T, void* tag)
550 {
551 struct readline_meta* M;
552 if (!validate_context(T, &M))
553 return;
554
555 if (M->old_handlers.recolor)
556 M->old_handlers.recolor(T, M->old_handlers.tag);
557
558 refresh(T, M);
559 }
560
arcan_tui_readline_reset(struct tui_context * T)561 void arcan_tui_readline_reset(struct tui_context* T)
562 {
563 struct readline_meta* M;
564 if (!validate_context(T, &M) || !M->work)
565 return;
566
567 M->finished = 0;
568 M->work[0] = 0;
569 M->work_ofs = 0;
570 M->work_len = 0;
571 M->cursor = 0;
572
573 refresh(T, M);
574 }
575
576 /*
577 * set prefix/prompt that will be drawn (assuming there is enough
578 * space for it to fit, or it will be truncated) - only cause a refresh
579 * if the contents have changed from the last drawn prompt.
580 */
arcan_tui_set_prompt(struct tui_context * T,const struct tui_cell * prompt)581 void arcan_tui_set_prompt(struct tui_context* T, const struct tui_cell* prompt)
582 {
583 struct readline_meta* M;
584 if (!validate_context(T, &M))
585 return;
586
587 /* early out, setting to an empty prompt */
588 if (!prompt && M->prompt){
589 M->prompt = NULL;
590 M->prompt_len = 0;
591 refresh(T, M);
592 return;
593 }
594
595 bool same = M->prompt != NULL;
596
597 /* both len and cmp */
598 size_t len = 0;
599 for (; prompt[len].ch; len++){}
600
601 M->prompt = prompt;
602 M->prompt_len = len;
603
604 refresh(T, M);
605 }
606
reset_boundaries(struct tui_context * T,struct readline_meta * M,size_t cols,size_t rows)607 static void reset_boundaries(
608 struct tui_context* T, struct readline_meta* M, size_t cols, size_t rows)
609 {
610 /* align from bottom and clamp */
611 if (M->opts.anchor_row < 0 && M->opts.n_rows < rows){
612 M->start_row = M->stop_row = 0;
613
614 size_t pad = -(M->opts.anchor_row) + M->opts.n_rows;
615
616 if (rows > pad){
617 M->start_row = rows - pad;
618 M->stop_row = M->start_row + M->opts.n_rows - 1;
619 }
620 }
621 /* align from top */
622 else if (M->opts.anchor_row > 0){
623 M->start_row = 0;
624 M->stop_row = M->stop_row + M->opts.n_rows - 1;
625 }
626 /* manual mode, ignore */
627 else {
628 M->start_row = 0;
629 M->stop_row = rows - 1;
630 }
631
632 if (M->opts.margin_left)
633 M->start_col = M->opts.margin_left - 1;
634 else
635 M->start_col = 0;
636
637 if (M->opts.margin_right && cols > M->opts.margin_right)
638 M->stop_col = cols - M->opts.margin_right - 1;
639 else
640 M->stop_col = cols - 1;
641
642 /* safety clamp upper bound,
643 * sanity check and early out also happens in the refresh */
644 if (M->stop_row >= rows)
645 M->stop_row = rows - 1;
646
647 if (M->stop_col >= cols)
648 M->stop_col = cols - 1;
649 }
650
on_resized(struct tui_context * T,size_t neww,size_t newh,size_t cols,size_t rows,void * tag)651 static void on_resized(struct tui_context* T,
652 size_t neww, size_t newh, size_t cols, size_t rows, void* tag)
653 {
654 struct readline_meta* M;
655 if (!validate_context(T, &M))
656 return;
657
658 if (M->old_handlers.resized)
659 M->old_handlers.resized(T, neww, newh, cols, rows, tag);
660
661 reset_boundaries(T, M, cols, rows);
662 refresh(T, M);
663 }
664
on_label_input(struct tui_context * T,const char * label,bool active,void * tag)665 static bool on_label_input(
666 struct tui_context* T, const char* label, bool active, void* tag)
667 {
668 struct readline_meta* M;
669 if (!validate_context(T, &M))
670 return false;
671
672 if (M->old_handlers.input_label)
673 return M->old_handlers.input_label(T, label, active, M->old_handlers.tag);
674
675 return false;
676 }
677
on_subwindow(struct tui_context * T,arcan_tui_conn * connection,uint32_t id,uint8_t type,void * tag)678 static bool on_subwindow(struct tui_context* T,
679 arcan_tui_conn* connection, uint32_t id, uint8_t type, void* tag)
680 {
681 /* if it is a popup, that would be ours for hints - otherwise
682 * send it onwards to the outer scope */
683 struct readline_meta* M;
684 if (!validate_context(T, &M))
685 return false;
686
687 if (M->old_handlers.subwindow)
688 return M->old_handlers.subwindow(T, connection, id, type, tag);
689
690 return false;
691 }
692
on_label_query(struct tui_context * T,size_t index,const char * country,const char * lang,struct tui_labelent * dstlbl,void * t)693 static bool on_label_query(struct tui_context* T,
694 size_t index, const char* country, const char* lang,
695 struct tui_labelent* dstlbl, void* t)
696 {
697 struct readline_meta* M;
698 if (!validate_context(T, &M))
699 return false;
700
701 /* let the old context also get a chance */
702 if (M->old_handlers.query_label)
703 return M->old_handlers.query_label(T,
704 index - 1, country, lang, dstlbl, M->old_handlers.tag);
705
706 /* space to add our own labels, for switching input modes, triggering
707 * completion and so on. */
708 return false;
709 }
710
on_reset(struct tui_context * T,int level,void * tag)711 static void on_reset(struct tui_context* T, int level, void* tag)
712 {
713 struct readline_meta* M;
714 if (!validate_context(T, &M))
715 return;
716
717 arcan_tui_readline_reset(T);
718
719 if (M->old_handlers.reset)
720 M->old_handlers.reset(T, level, M->old_handlers.tag);
721 }
722
arcan_tui_readline_setup(struct tui_context * T,struct tui_readline_opts * opts,size_t opt_sz)723 void arcan_tui_readline_setup(
724 struct tui_context* T, struct tui_readline_opts* opts, size_t opt_sz)
725 {
726 if (!T || !opts)
727 return;
728
729 struct readline_meta* meta = malloc(sizeof(struct readline_meta));
730 if (!meta)
731 return;
732
733 *meta = (struct readline_meta){
734 .magic = READLINE_MAGIC,
735 .opts = *opts
736 };
737
738 size_t sz = sizeof(struct tui_readline_opts);
739 memcpy(&meta->opts, opts, opt_sz > sz ? sz : opt_sz);
740 if (!meta->opts.n_rows)
741 meta->opts.n_rows = 1;
742
743 struct tui_cbcfg cbcfg = {
744 .input_key = on_key_input,
745 .input_mouse_button = on_mouse_button_input,
746 .recolor = on_recolor,
747 .input_utf8 = on_utf8_input,
748 .utf8 = on_utf8_paste,
749 .resized = on_resized,
750 .input_label = on_label_input,
751 .subwindow = on_subwindow,
752 .query_label = on_label_query,
753 .reset = on_reset,
754 /* input_alabel - block */
755 /* input_mouse_motion - block? or treat as selection for replace */
756 /* input_misc - block */
757 /* state - block */
758 /* bchunk - block / forward */
759 /* vpaste - block / forward */
760 /* apaste - block / forward */
761 /* tick - forward */
762 /* utf8 - treat as multiple input_text calls */
763 /* resized - forward */
764 /* reset - trigger recolor, forward */
765 /* geohint - forward */
766 /* substitute - block */
767 /* visibility - forward */
768 /* exec_state - forward */
769 .tag = meta
770 };
771
772 /* two possible approach to this, one is fully self contains and works like all the
773 * other widgets, the other is a _setup and then continously call readline_at */
774 arcan_tui_update_handlers(T, &cbcfg, &meta->old_handlers, sizeof(struct tui_cbcfg));
775
776 size_t rows, cols;
777 arcan_tui_dimensions(T, &rows, &cols);
778 reset_boundaries(T, meta, cols, rows);
779 refresh(T, meta);
780 }
781
arcan_tui_readline_release(struct tui_context * T)782 void arcan_tui_readline_release(struct tui_context* T)
783 {
784 struct readline_meta* M;
785 if (!validate_context(T, &M))
786 return;
787
788 M->magic = 0xdeadbeef;
789 free(M->work);
790
791 /* completion buffers etc. are retained by the user so ignore */
792
793 arcan_tui_update_handlers(T, &M->old_handlers, NULL, sizeof(struct tui_cbcfg));
794 free(M);
795 }
796
arcan_tui_readline_finished(struct tui_context * T,char ** buffer)797 int arcan_tui_readline_finished(struct tui_context* T, char** buffer)
798 {
799 struct readline_meta* M;
800 if (buffer)
801 *buffer = NULL;
802
803 if (!validate_context(T, &M))
804 return false;
805
806 /* if we have a completion string, return that, otherwise return the work buffer */
807 if (buffer)
808 *buffer = M->work;
809
810 return M->finished;
811 }
812
813 #ifdef EXAMPLE
814
815 #include <errno.h>
816 #include <ctype.h>
817
test_refresh(struct tui_context * T)818 static void test_refresh(struct tui_context* T)
819 {
820 arcan_tui_erase_screen(T, false);
821 /* clear to color,
822 * fill with a..z */
823 }
824
test_resize(struct tui_context * T,size_t neww,size_t newh,size_t col,size_t row,void * M)825 static void test_resize(struct tui_context* T,
826 size_t neww, size_t newh, size_t col, size_t row, void* M)
827 {
828 printf("resize\n");
829 test_refresh(T);
830 }
831
test_validate(const char * message,void * T)832 ssize_t test_validate(const char* message, void* T)
833 {
834 for (size_t i = 0; message[i]; i++)
835 if (message[i] == 'a')
836 return i;
837
838 return -1;
839 }
840
no_num(uint32_t ch,size_t length,void * tag)841 static bool no_num(uint32_t ch, size_t length, void* tag)
842 {
843 if (ch >= '0' && ch <= '9')
844 return false;
845
846 return true;
847 }
848
arcan_tui_readline_history(struct tui_context * T,const char ** buf,size_t count)849 void arcan_tui_readline_history(struct tui_context* T, const char** buf, size_t count)
850 {
851
852 }
853
main(int argc,char ** argv)854 int main(int argc, char** argv)
855 {
856 /* basic 'just fill with blue' and have the default-attribute for the text field */
857 struct tui_cbcfg cbcfg = {
858 .resized = test_resize
859 };
860
861 arcan_tui_conn* conn = arcan_tui_open_display("readline", "test");
862 struct tui_context* tui = arcan_tui_setup(conn, NULL, &cbcfg, sizeof(cbcfg));
863
864 struct tui_screen_attr attr = {0};
865 arcan_tui_get_color(tui, TUI_COL_TEXT, attr.fc);
866 attr.bc[2] = 0xaa;
867
868 arcan_tui_defattr(tui, &attr);
869
870 /* show 'a' characters as invalid,
871 * don't allow numbers,
872 * shutdown on exit */
873 arcan_tui_readline_setup(tui,
874 &(struct tui_readline_opts){
875 .anchor_row = -2,
876 .n_rows = 1,
877 .margin_left = 20,
878 .margin_right = 20,
879 .filter_character = no_num,
880 .multiline = false,
881 .allow_exit = true,
882 .verify = test_validate,
883 }, sizeof(struct tui_readline_opts)
884 );
885
886 struct tui_cell prompt[] = {
887 {
888 .attr = attr,
889 .ch = 'h'
890 },
891 {
892 .attr = attr,
893 .ch = 'i',
894 },
895 {
896 .attr = attr,
897 .ch = 't',
898 },
899 {
900 .attr = attr,
901 .ch = 'h',
902 },
903 {
904 .attr = attr,
905 .ch = 'e',
906 },
907 {
908 .attr = attr,
909 .ch = 'r',
910 },
911 {
912 .attr = attr,
913 .ch = 'e',
914 },
915 {
916 .attr = attr,
917 .ch = '>'
918 },
919 {0}
920 };
921
922 arcan_tui_set_prompt(tui, prompt);
923
924 char* out;
925 bool running = true;
926
927 while(running){
928 while (!arcan_tui_readline_finished(tui, &out) && running){
929 struct tui_process_res res = arcan_tui_process(&tui, 1, NULL, 0, -1);
930 if (res.errc == TUI_ERRC_OK){
931 if (-1 == arcan_tui_refresh(tui) && errno == EINVAL)
932 running = false;
933 }
934 }
935
936 if (out && running){
937 if (strcmp(out, "exit") == 0)
938 break;
939 else {
940 /* set last input as prompt */
941 arcan_tui_readline_reset(tui);
942 }
943 }
944 }
945
946 arcan_tui_readline_release(tui);
947 arcan_tui_destroy(tui, NULL);
948
949 return EXIT_SUCCESS;
950 }
951
952 #endif
953