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