1 #include "../../arcan_shmif.h"
2 #include "../../arcan_tui.h"
3 #include "../../arcan_tui_listwnd.h"
4 #include <errno.h>
5 #include <ctype.h>
6 #include <inttypes.h>
7 #include <stdint.h>
8 #include <string.h>
9 #include <assert.h>
10 
11 /*
12  * Useful enhancements missing:
13  *
14  *  - generalize all the proxying to a default table and mask out / manually
15  *    replace the ones that should be treated like that so we don't repeat
16  *    the same code everywhere.
17  *
18  *  - scroll the current selected line on tick if cropped
19  *  - expose scrollbar metadata
20  *  - indicate sublevel / subnodes (can turn into tree-view through mask/unmask)
21  *    use an attribute for level so |-> can be added do sub-ones,
22  *    more difficult is adding an expand-collapse so selection would expand
23  *  - handle accessibility subwindow (provide only selected item for t2s)
24  *  - allow multiple weighted column formats for wider windows
25  *  - prefix typing for searching
26  */
27 
28 #ifndef COUNT_OF
29 #define COUNT_OF(x) \
30 	((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
31 #endif
32 
33 #define INACTIVE_ITEM (LIST_SEPARATOR | LIST_LABEL | LIST_PASSIVE | LIST_HIDE)
34 #define HIDDEN_ITEM (LIST_HIDE)
35 
36 #define LISTWND_MAGIC 0xfadef00e
37 struct listwnd_meta {
38 /* debug-help, check against LISTWND_MAGIC */
39 	uint32_t magic;
40 
41 /* actual entries, flags can mutate, size cannot */
42 	struct tui_list_entry* list;
43 	size_t list_sz;
44 
45 /* current logical cursor position and resolved screen position */
46 	size_t list_pos;
47 	size_t list_row;
48 
49 /* first row start */
50 	size_t list_ofs;
51 
52 /* set when user has made a selection, and the selected item */
53 	int entry_state;
54 	size_t entry_pos;
55 
56 /* drawing characters for the flags */
57 	uint32_t line_ch;
58 	uint32_t check_ch;
59 	uint32_t sub_ch;
60 
61 /* to restore the context */
62 	struct tui_cbcfg old_handlers;
63 	int old_flags;
64 	size_t orig_w, orig_h;
65 };
66 
67 /* context validation, perform on every exported symbol */
validate(struct tui_context * T,struct listwnd_meta ** M)68 static bool validate(struct tui_context* T, struct listwnd_meta** M)
69 {
70 	if (!T)
71 		return false;
72 
73 	struct tui_cbcfg handlers;
74 	arcan_tui_update_handlers(T, NULL, &handlers, sizeof(struct tui_cbcfg));
75 
76 	struct listwnd_meta* ch = handlers.tag;
77 	if (!ch || ch->magic != LISTWND_MAGIC)
78 		return false;
79 
80 	if (M)
81 		*M = ch;
82 
83 	return true;
84 }
85 
get_visible_offset(struct listwnd_meta * M)86 static size_t get_visible_offset(struct listwnd_meta* M)
87 {
88 	size_t ofs = 0;
89 	for (size_t i = M->list_ofs; i < M->list_pos; i++){
90 		if (M->list[i].attributes & HIDDEN_ITEM)
91 			continue;
92 		ofs++;
93 	}
94 	return ofs;
95 }
96 
arcan_tui_listwnd_tell(struct tui_context * T)97 ssize_t arcan_tui_listwnd_tell(struct tui_context* T)
98 {
99 	struct listwnd_meta* M;
100 	if (!validate(T, &M))
101 		return -1;
102 
103 	return M->list_pos;
104 }
105 
redraw(struct tui_context * T,struct listwnd_meta * M)106 static void redraw(struct tui_context* T, struct listwnd_meta* M)
107 {
108 	size_t c_row = 0;
109 	size_t rows, cols;
110 	arcan_tui_dimensions(T, &rows, &cols);
111 
112 	if (!rows)
113 		return;
114 
115 /* safeguard that we fit in the current screen, else we search */
116 	for (size_t ofs = get_visible_offset(M); ofs > rows; M->list_ofs++){
117 		ofs = get_visible_offset(M);
118 	}
119 
120 #define GET_COL_INDEX(X) {.aflags = TUI_ATTR_COLOR_INDEXED, .fc[0] = X, .bc[0] = X};
121 
122 	struct tui_screen_attr reset_def = GET_COL_INDEX(TUI_COL_TEXT);
123 	struct tui_screen_attr def = GET_COL_INDEX(TUI_COL_LABEL);
124 	struct tui_screen_attr sel = GET_COL_INDEX(TUI_COL_HIGHLIGHT);
125 	sel.aflags |= TUI_ATTR_BOLD;
126 	struct tui_screen_attr inact = GET_COL_INDEX(TUI_COL_INACTIVE);
127 	struct tui_screen_attr label = GET_COL_INDEX(TUI_COL_LABEL);
128 
129 /* erase the screen as well as the entries can be fewer than the number of rows */
130 	arcan_tui_defattr(T, &reset_def);
131 	arcan_tui_erase_screen(T, false);
132 
133 /* now we can just clear / draw the items on the page */
134 	c_row = 0;
135 	for (size_t i = M->list_ofs; rows && i < M->list_sz; i++){
136 		int lattr = M->list[i].attributes;
137 		const char* label = M->list[i].label;
138 
139 		if (lattr & HIDDEN_ITEM)
140 			continue;
141 
142 		rows--;
143 		arcan_tui_move_to(T, 0, c_row);
144 
145 		struct tui_screen_attr* cattr = &def;
146 		if (i == M->list_pos){
147 			cattr = &sel;
148 			M->list_row = c_row;
149 		}
150 
151 /* cursor state doesn't matter for passive */
152 		if (lattr & LIST_PASSIVE){
153 			cattr = &inact;
154 		}
155 
156 		if (lattr & LIST_SEPARATOR){
157 			for (size_t c = 0; c < cols; c++)
158 				arcan_tui_write(T, M->line_ch, &inact);
159 			c_row++;
160 			continue;
161 		}
162 
163 /* clear the target line with the attribute (as it can contain bgc) */
164 		size_t ofs = 0;
165 		arcan_tui_defattr(T, cattr);
166 		arcan_tui_erase_region(T, 0, c_row, cols, c_row, false);
167 
168 /* tactic: draw as much as possible from starting label offset,
169  * recall (& 0xc0) != 0x80 for utf8- start */
170 		arcan_tui_move_to(T, 1+M->list[i].indent, c_row);
171 		for (size_t vofs = 0; vofs < cols - 2 && label[ofs]; vofs++){
172 			size_t end = ofs + 1;
173 			while (label[end] && (label[end] & 0xc0) == 0x80) end++;
174 			arcan_tui_writeu8(T, (const uint8_t*)&label[ofs], end - ofs, cattr);
175 			ofs = end;
176 		}
177 
178 /* anotate with symbols */
179 		if (lattr & LIST_CHECKED){
180 			arcan_tui_move_to(T, 0, c_row);
181 			arcan_tui_write(T, M->check_ch, cattr);
182 		}
183 
184 		if (lattr & LIST_HAS_SUB){
185 			arcan_tui_move_to(T, cols-1, c_row);
186 			arcan_tui_write(T, M->sub_ch, cattr);
187 		}
188 		c_row++;
189 	}
190 
191 	arcan_tui_defattr(T, &reset_def);
192 }
193 
arcan_tui_listwnd_setpos(struct tui_context * T,size_t n)194 void arcan_tui_listwnd_setpos(struct tui_context* T, size_t n)
195 {
196 	struct listwnd_meta* M;
197 	if (!validate(T, &M))
198 		return;
199 
200 	if (n < M->list_sz)
201 		M->list_pos = n;
202 
203 	redraw(T, M);
204 }
205 
select_current(struct tui_context * T,struct listwnd_meta * M)206 static void select_current(struct tui_context* T, struct listwnd_meta* M)
207 {
208 	int flags = M->list[M->list_pos].attributes;
209 	if (flags & INACTIVE_ITEM)
210 		return;
211 	M->entry_state = 1;
212 	M->entry_pos = M->list_pos;
213 }
214 
cancel(struct tui_context * T,struct listwnd_meta * M)215 static void cancel(struct tui_context* T, struct listwnd_meta* M)
216 {
217 	M->entry_state = -1;
218 }
219 
step_page_s(struct tui_context * T,struct listwnd_meta * M)220 static void step_page_s(struct tui_context* T, struct listwnd_meta* M)
221 {
222 	size_t rows, cols;
223 	arcan_tui_dimensions(T, &rows, &cols);
224 
225 /* increment offset half- a page */
226 	rows = (rows >> 1) + 1;
227 	size_t c_row;
228 	for (c_row = M->list_ofs; c_row < M->list_sz && rows; c_row++){
229 		if (M->list[c_row].attributes & INACTIVE_ITEM)
230 			continue;
231 
232 		rows--;
233 	}
234 
235 /* couldn't be done */
236 	if (c_row == M->list_sz || M->list_pos == M->list_sz - 1){
237 		M->list_ofs = 0;
238 		M->list_pos = 0;
239 	}
240 	else{
241 		M->list_ofs = c_row;
242 		M->list_pos++;
243 	}
244 
245 /* step cursor to next sane */
246 	for (c_row = M->list_pos; c_row < M->list_sz; c_row++){
247 		if (!(M->list[c_row].attributes & INACTIVE_ITEM)){
248 			M->list_pos = c_row;
249 			break;
250 		}
251 	}
252 
253 	redraw(T, M);
254 }
255 
step_page_n(struct tui_context * T,struct listwnd_meta * M)256 static void step_page_n(struct tui_context* T, struct listwnd_meta* M)
257 {
258 }
259 
step_cursor_n(struct tui_context * T,struct listwnd_meta * M)260 static void step_cursor_n(struct tui_context* T, struct listwnd_meta* M)
261 {
262 	size_t current = M->list_pos;
263 	size_t vis_step = 0;
264 	do {
265 		current = current > 0 ? current - 1 : M->list_sz - 1;
266 		if (!(M->list[current].attributes & HIDDEN_ITEM))
267 			vis_step++;
268 
269 		if (!(M->list[current].attributes & INACTIVE_ITEM))
270 			break;
271 
272 	} while (current != M->list_pos);
273 
274 	if (M->list_row < vis_step){
275 		step_page_n(T, M);
276 		return;
277 	}
278 
279 	M->list_pos = current;
280 	redraw(T, M);
281 }
282 
step_cursor_s(struct tui_context * T,struct listwnd_meta * M)283 static void step_cursor_s(struct tui_context* T, struct listwnd_meta* M)
284 {
285 	size_t rows, cols;
286 	arcan_tui_dimensions(T, &rows, &cols);
287 
288 /* find the next selectable item, and detect if it is on this page or not */
289 	size_t current = M->list_pos;
290 	size_t vis_step = 0, prev_vis = current;
291 	bool new_page = false;
292 
293 	do {
294 		current = (current + 1) % M->list_sz;
295 		if (!(M->list[current].attributes & HIDDEN_ITEM)){
296 			vis_step++;
297 
298 /* track the first visible on the next page */
299 			if (vis_step + M->list_row >= rows && !new_page){
300 				new_page = true;
301 				prev_vis = current;
302 			}
303 		}
304 
305 		if (!(M->list[current].attributes & INACTIVE_ITEM))
306 			break;
307 
308 /* end condition is wrap */
309 	} while (current != M->list_pos);
310 
311 /* outside window, need to find the new list ofset as well, that is
312  * why we need to track the previous visible */
313 	if (new_page){
314 		M->list_ofs = prev_vis;
315 	}
316 	else if (current < M->list_pos){
317 		if (prev_vis < rows)
318 			prev_vis = 0;
319 		M->list_ofs = prev_vis;
320 	}
321 
322 	M->list_pos = current;
323 
324 	redraw(T, M);
325 }
326 
arcan_tui_listwnd_dirty(struct tui_context * T)327 void arcan_tui_listwnd_dirty(struct tui_context* T)
328 {
329 	struct listwnd_meta* M;
330 	if (!validate(T, &M))
331 		return;
332 
333 	redraw(T, M);
334 }
335 
u8(struct tui_context * T,const char * u8,size_t len,void * tag)336 static bool u8(struct tui_context* T, const char* u8, size_t len, void* tag)
337 {
338 /* not necessarily terminated, so terminate */
339 	char cp[len+1];
340 	memcpy(cp, u8, len);
341 	cp[len] = '\0';
342 
343 	struct listwnd_meta* M = tag;
344 	for (size_t i = 0; i < M->list_sz; i++){
345 		if (M->list[i].shortcut && strcmp(M->list[i].shortcut, cp) == 0){
346 			if (M->list[i].attributes & ~(INACTIVE_ITEM)){
347 				M->list_pos = i;
348 				redraw(T, M);
349 			}
350 			return true;
351 		}
352 	}
353 
354 	return false;
355 }
356 
arcan_tui_listwnd_status(struct tui_context * T,struct tui_list_entry ** out)357 bool arcan_tui_listwnd_status(struct tui_context* T, struct tui_list_entry** out)
358 {
359 	struct listwnd_meta* M;
360 	if (!validate(T, &M))
361 		return false;
362 
363 	if (!M->entry_state)
364 		return false;
365 
366 /* user requested cancellation */
367 	else if (M->entry_state == -1 && out)
368 		*out = NULL;
369 /* or selected a real item */
370 	else if (M->entry_state == 1 && out)
371 		*out = &M->list[M->entry_pos];
372 
373 	M->entry_state = 0;
374 	return true;
375 }
376 
377 struct labelent {
378 	void (* handler)(struct tui_context* T, struct listwnd_meta* M);
379 	struct tui_labelent ent;
380 	int alt;
381 };
382 
383 static struct labelent labels[] = {
384 	{
385 		.handler = select_current,
386 		.ent =
387 		{
388 			.label = "SELECT",
389 			.descr = "Activate/Toggle the currently selected item",
390 			.initial = TUIK_RETURN
391 		},
392 		.alt = TUIK_RIGHT
393 	},
394 	{
395 		.handler = step_cursor_s,
396 		.ent =
397 		{
398 			.label = "NEXT",
399 			.descr = "Move the cursor to the next valid entry",
400 			.initial = TUIK_DOWN
401 		}
402 	},
403 	{
404 		.handler = step_cursor_n,
405 		.ent =
406 		{
407 			.label = "PREV",
408 			.descr = "Move the cursor to the previous valid entry",
409 			.initial = TUIK_UP
410 		}
411 	},
412 	{
413 		.handler = step_page_s,
414 		.ent =
415 		{
416 			.label = "NEXT_PAGE",
417 			.descr = "Step the list to the next page",
418 			.initial = TUIK_PAGEDOWN
419 		}
420 	},
421 	{
422 		.handler = step_page_n,
423 		.ent =
424 		{
425 			.label = "PREV_PAGE",
426 			.descr = "Step the list to the previous page",
427 			.initial = TUIK_PAGEUP
428 		}
429 	},
430 	{
431 		.handler = cancel,
432 		.ent =
433 		{
434 			.label = "CANCEL",
435 			.descr = "Exit the list view state",
436 			.initial = TUIK_ESCAPE
437 		},
438 		.alt = TUIK_LEFT
439 	}
440 };
441 
on_label_input(struct tui_context * T,const char * label,bool active,void * tag)442 static bool on_label_input(
443 	struct tui_context* T, const char* label, bool active, void* tag)
444 {
445 	if (!active)
446 		return true;
447 
448 	for (size_t i = 0; i < COUNT_OF(labels); i++){
449 		if (strcmp(label, labels[i].ent.label) == 0)
450 			return labels[i].handler(T, (struct listwnd_meta*) tag), true;
451 	}
452 
453 	return false;
454 }
455 
key_input(struct tui_context * T,uint32_t keysym,uint8_t scancode,uint8_t mods,uint16_t subid,void * tag)456 static void key_input(struct tui_context* T, uint32_t keysym,
457 	uint8_t scancode, uint8_t mods, uint16_t subid, void* tag)
458 {
459 	struct listwnd_meta* M = tag;
460 	for (size_t i = 0; i < COUNT_OF(labels); i++){
461 		if ((keysym && keysym == labels[i].alt) ||
462 			keysym == labels[i].ent.initial)
463 			return labels[i].handler(T, (struct listwnd_meta*) tag);
464 	}
465 }
466 
arcan_tui_listwnd_release(struct tui_context * T)467 void arcan_tui_listwnd_release(struct tui_context* T)
468 {
469 	struct listwnd_meta* M;
470 	if (!validate(T, &M))
471 		return;
472 
473 /* restore old flags */
474 	arcan_tui_reset_flags(T, ~0);
475 	arcan_tui_set_flags(T, M->old_flags);
476 
477 /* requery label through original handles */
478 	arcan_tui_update_handlers(T,
479 		&M->old_handlers, NULL, sizeof(struct tui_cbcfg));
480 
481 	arcan_tui_reset_labels(T);
482 
483 /* it would make sense to 'fake' a resize here as well, but from some design
484  * oversights with the event, that requires tracking or exposing shmif_ context
485  * contents, or breaking ABI - so assume the caller actually has the sense to
486  * refresh on release() */
487 
488 /* LTO could possibly do something about this, but basically just safeguard
489  * on a safeguard (UAF detection) for the listwnd_meta after freeing it */
490 	*M = (struct listwnd_meta){
491 		.magic = 0xdeadbeef
492 	};
493 
494 	arcan_tui_wndhint(T, NULL,
495 		(struct tui_constraints){
496 			.min_cols = -1, .min_rows = -1,
497 			.max_cols = M->orig_w, .max_rows = M->orig_h,
498 			.anch_row = -1, .anch_col = -1
499 		}
500 	);
501 
502 	free(M);
503 }
504 
resized(struct tui_context * T,size_t neww,size_t newh,size_t col,size_t row,void * t)505 static void resized(struct tui_context* T,
506 	size_t neww, size_t newh, size_t col, size_t row, void* t)
507 {
508 	struct listwnd_meta* M = t;
509 	redraw(T, M);
510 	if (M->old_handlers.resized){
511 		M->old_handlers.resized(T, neww, newh, col, row, M->old_handlers.tag);
512 	}
513 }
514 
subwindow(struct tui_context * T,arcan_tui_conn * conn,uint32_t id,uint8_t type,void * t)515 static bool subwindow(struct tui_context* T,
516 	arcan_tui_conn* conn, uint32_t id, uint8_t type, void* t)
517 {
518 	struct listwnd_meta* M = t;
519 	if (M->old_handlers.subwindow){
520 		return M->old_handlers.subwindow(T, conn, id, type, M->old_handlers.tag);
521 	}
522 	else
523 		return false;
524 }
525 
resize(struct tui_context * T,size_t neww,size_t newh,size_t col,size_t row,void * t)526 static void resize(struct tui_context* T,
527 	size_t neww, size_t newh, size_t col, size_t row, void* t)
528 {
529 	struct listwnd_meta* M = t;
530 	if (M->old_handlers.resize){
531 		M->old_handlers.resize(T, neww, newh, col, row, M->old_handlers.tag);
532 	}
533 }
534 
tick(struct tui_context * T,void * t)535 static void tick(struct tui_context* T, void* t)
536 {
537 /* if current item is cropped, scroll it */
538 }
539 
geohint(struct tui_context * T,float lat,float longit,float elev,const char * cnt,const char * lang,void * t)540 static void geohint(struct tui_context* T,
541 	float lat, float longit, float elev, const char* cnt, const char* lang, void* t)
542 {
543 	struct listwnd_meta* M = t;
544 	if (M->old_handlers.geohint){
545 		M->old_handlers.geohint(T, lat, longit, elev, cnt, lang, M->old_handlers.tag);
546 	}
547 }
548 
bchunk(struct tui_context * T,bool input,uint64_t size,int fd,const char * msg,void * tag)549 static void bchunk(struct tui_context* T,
550 	bool input, uint64_t size, int fd, const char* msg, void* tag)
551 {
552 	struct listwnd_meta* M = tag;
553 	if (M->old_handlers.bchunk){
554 		M->old_handlers.bchunk(T, input, size, fd, msg, M->old_handlers.tag);
555 	}
556 }
557 
mouse_motion(struct tui_context * T,bool relative,int mouse_x,int mouse_y,int modifiers,void * tag)558 static void mouse_motion(struct tui_context* T,
559 	bool relative, int mouse_x, int mouse_y, int modifiers, void* tag)
560 {
561 	struct listwnd_meta* M = tag;
562 
563 /* uncommon but not impossible */
564 	if (relative){
565 		if (!mouse_y)
566 			return;
567 		if (mouse_y < 0){
568 			for (int i = mouse_y; i != 0; i++)
569 				step_cursor_n(T, M);
570 		}
571 		else{
572 			for (int i = mouse_y; i != 0; i--)
573 				step_cursor_s(T, M);
574 		}
575 		return;
576 	}
577 
578 	for (size_t i = M->list_ofs, yp = 0; i < M->list_sz; i++){
579 		if (M->list[i].attributes & HIDDEN_ITEM)
580 			continue;
581 
582 /* find matching position */
583 		if (yp == mouse_y){
584 /* and move selection if it has changed */
585 			if (M->list_pos != i){
586 				M->list_pos = i;
587 				redraw(T, M);
588 			}
589 			break;
590 		}
591 		yp++;
592 	}
593 }
594 
mouse_button(struct tui_context * T,int last_x,int last_y,int button,bool active,int modifiers,void * tag)595 static void mouse_button(struct tui_context* T,
596 	int last_x, int last_y, int button, bool active, int modifiers, void* tag)
597 {
598 	struct listwnd_meta* M = tag;
599 	if (!active)
600 		return;
601 
602 /* mouse motion preceeds the button, so we can just trigger */
603 	if (button == TUIBTN_LEFT){
604 		select_current(T, M);
605 	}
606 	else if (button == TUIBTN_RIGHT){
607 		cancel(T, M);
608 	}
609 	else if (button == TUIBTN_MIDDLE){
610 		step_page_s(T, M);
611 	}
612 	else if (button == TUIBTN_WHEEL_UP){
613 		step_cursor_n(T, M);
614 	}
615 	else if (button == TUIBTN_WHEEL_DOWN){
616 		step_cursor_s(T, M);
617 	}
618 }
619 
recolor(struct tui_context * T,void * t)620 static void recolor(struct tui_context* T, void* t)
621 {
622 	struct listwnd_meta* M = t;
623 	redraw(T, M);
624 }
625 
on_label_query(struct tui_context * T,size_t index,const char * country,const char * lang,struct tui_labelent * dstlbl,void * t)626 static bool on_label_query(struct tui_context* T,
627 	size_t index, const char* country, const char* lang,
628 	struct tui_labelent* dstlbl, void* t)
629 {
630 	struct listwnd_meta* M = t;
631 
632 	if (COUNT_OF(labels) < index + 1){
633 		return false;
634 	}
635 
636 	memcpy(dstlbl, &labels[index].ent, sizeof(struct tui_labelent));
637 	return true;
638 }
639 
arcan_tui_listwnd_setup(struct tui_context * T,struct tui_list_entry * L,size_t n_entries)640 bool arcan_tui_listwnd_setup(
641 	struct tui_context* T, struct tui_list_entry* L, size_t n_entries)
642 {
643 	if (!T || !L || n_entries == 0)
644 		return false;
645 
646 	struct listwnd_meta* meta = malloc(sizeof(struct listwnd_meta));
647 	if (!meta)
648 		return false;
649 
650 	*meta = (struct listwnd_meta){
651 		.magic = LISTWND_MAGIC,
652 		.list_sz = n_entries,
653 		.list = L,
654 	};
655 
656 /* save old flags and just set clean + ALTERNATE */
657 	meta->old_flags = arcan_tui_set_flags(T, 0);
658 	arcan_tui_reset_flags(T, ~0);
659 	arcan_tui_set_flags(T, TUI_ALTERNATE | TUI_HIDE_CURSOR | TUI_MOUSE);
660 
661 	struct tui_cbcfg cbcfg = {
662 		.tag = meta,
663 		.resize = resize,
664 		.resized = resized,
665 		.recolor = recolor,
666 		.tick = tick,
667 		.geohint = geohint,
668 		.query_label = on_label_query,
669 		.input_label = on_label_input,
670 		.input_key = key_input,
671 		.input_mouse_motion = mouse_motion,
672 		.input_mouse_button = mouse_button,
673 		.input_utf8 = u8,
674 		.subwindow = subwindow,
675 		.bchunk = bchunk
676 	};
677 
678 	/* BOX DRAWINGS LIGHT HORIZONTAL U+2500 */
679 	meta->line_ch = arcan_tui_hasglyph(T, 0x2500) ? 0x2500 : '-';
680 	/* MODIFIER LETTER RIGHT ARROWHEAD U+02c3 */
681 	meta->sub_ch = arcan_tui_hasglyph(T, 0x02c3) ? 0x02c3 : '>';
682 	/* CHECK MARK U+2713 */
683 	meta->check_ch = arcan_tui_hasglyph(T, 0x2713) ? 0x2713 : '*';
684 
685 /* save old handlers and set new ones */
686 	arcan_tui_update_handlers(T,
687 		&cbcfg, &meta->old_handlers, sizeof(struct tui_cbcfg));
688 
689 /* and check for misuse */
690 	assert(meta->old_handlers.resize != resize);
691 
692 /* rough utf8-len based on labels alone */
693 	size_t max_w = 0;
694 	for (size_t i = 0; i < n_entries; i++){
695 		size_t j = 0, w = 0;
696 		while (L[i].label[j]){
697 			w += (L[i].label[j++] & 0xc0) != 0x80;
698 		}
699 		if (w > max_w)
700 			max_w = w;
701 	}
702 
703 	arcan_tui_dimensions(T, &meta->orig_h, &meta->orig_w);
704 	arcan_tui_wndhint(T, NULL,
705 		(struct tui_constraints){
706 			.min_cols = -1, .min_rows = -1,
707 			.max_cols = max_w + 4, .max_rows = n_entries + 1,
708 			.anch_row = -1, .anch_col = -1
709 		}
710 	);
711 
712 /* requery label through new handles (removes existing ones) */
713 	arcan_tui_reset_labels(T);
714 	redraw(T, meta);
715 
716 	return true;
717 }
718 
719 #ifdef EXAMPLE
720 
721 static struct tui_list_entry test_easy[] = {
722 	{
723 		.label = "hi",
724 		.attributes = LIST_CHECKED,
725 		.tag = 0,
726 	},
727 	{
728 		.label = "there",
729 		.attributes = LIST_PASSIVE,
730 		.shortcut = "t",
731 		.tag = 1
732 	},
733 	{
734 		.attributes = LIST_SEPARATOR,
735 	},
736 	{
737 		.label = "lolita",
738 		.attributes = LIST_HAS_SUB,
739 		.shortcut = "l",
740 		.tag = 2
741 	}
742 };
743 
main(int argc,char ** argv)744 int main(int argc, char** argv)
745 {
746 	struct tui_cbcfg cbcfg = {0};
747 	arcan_tui_conn* conn = arcan_tui_open_display("test", "");
748 	struct tui_context* tui = arcan_tui_setup(conn, NULL, &cbcfg, sizeof(cbcfg));
749 	size_t test_cases = 2;
750 	size_t index = 0;
751 
752 	if (argc > 1){
753 		index = strtoull(argv[1], NULL, 10);
754 	}
755 
756 	if (index > test_cases - 1){
757 		fprintf(stderr, "Index (%zu) out of bounds\n", index);
758 		return EXIT_FAILURE;
759 	}
760 
761 	switch(index){
762 	case 0:
763 		arcan_tui_listwnd_setup(tui, test_easy, COUNT_OF(test_easy));
764 	break;
765 	case 1:{
766 		struct tui_list_entry* ent = malloc(256 * sizeof(struct tui_list_entry));
767 		for (size_t i = 0; i < 256; i++){
768 			char buf[8];
769 			snprintf(buf, 8, "%zu %c", i, (char)('a' + i % 10));
770 			ent[i] = (struct tui_list_entry){
771 				.label = strdup((char*)buf),
772 				.tag = i
773 			};
774 			if (i % 5 == 0)
775 				ent[i].attributes = LIST_HIDE;
776 			if (i % 3 == 0 || i % 4 == 0)
777 				ent[i].attributes = LIST_PASSIVE;
778 		}
779 		arcan_tui_listwnd_setup(tui, ent, 256);
780 	}
781 	break;
782 	}
783 
784 	while(1){
785 		struct tui_process_res res = arcan_tui_process(&tui, 1, NULL, 0, -1);
786 		if (res.errc == TUI_ERRC_OK){
787 			if (-1 == arcan_tui_refresh(tui) && errno == EINVAL)
788 				break;
789 		}
790 		else
791 			break;
792 
793 		struct tui_list_entry* ent;
794 		if (arcan_tui_listwnd_status(tui, &ent)){
795 			if (ent)
796 				printf("user picked: %s\n", ent->label);
797 			else
798 				printf("user cancelled\n");
799 			break;
800 		}
801 	}
802 	arcan_tui_destroy(tui, NULL);
803 	return 0;
804 }
805 #endif
806