1 #include <arcan_shmif.h>
2 #include <stdio.h>
3 #include <arcan_tui.h>
4 #include <inttypes.h>
5 #include <stdarg.h>
6 #include <sys/types.h>
7 #include <sys/stat.h>
8 #include <sys/socket.h>
9 #include <pwd.h>
10 #include <ctype.h>
11 #include <signal.h>
12 #include <pthread.h>
13 #include <fcntl.h>
14 #include <poll.h>
15 #include <unistd.h>
16 
17 #include "cli.h"
18 #include "cli_builtin.h"
19 
20 #include "tsm/libtsm.h"
21 #include "tsm/libtsm_int.h"
22 #include "tsm/shl-pty.h"
23 #include "b64dec.h"
24 
25 struct line {
26 	size_t count;
27 	struct tui_cell* cells;
28 };
29 
30 enum pipe_modes {
31 /* just forward from in-out and draw the visible characters into screen */
32 	PIPE_RAW = 0,
33 
34 /* pipe-raw but respect line-feed characters */
35 	PIPE_PLAIN_LF = 1,
36 
37 /* treat as utf8 with a sliding window on decode fail */
38 	PIPE_UTF8 = 2,
39 
40 /* only show minimal information about the transfer itself */
41 	PIPE_STATS_BASIC = 3
42 };
43 
44 static struct {
45 	struct tui_context* screen;
46 
47 /* only main one and debug window */
48 	struct tui_context* screens[2];
49 
50 	struct tsm_vte* vte;
51 	struct shl_pty* pty;
52 	struct arg_arr* args;
53 
54 	pthread_mutex_t synch;
55 	pthread_mutex_t hold;
56 
57 	pid_t child;
58 
59 	_Atomic volatile bool alive;
60 
61 /* track re-execute (reset) as well as working with stdin/stdout forwarding */
62 	bool die_on_term;
63 	bool complete_signal;
64 	bool pipe;
65 	size_t bytes_in;
66 	size_t bytes_out;
67 	uint8_t u8_buf[4];
68 	volatile uint8_t pipe_mode;
69 
70 /* if the terminal has 'died' and 'fit' is set - we crop the stored
71  * buffer to bounding box of 'real' (non-whitespace content) cells */
72 	bool fit_contents;
73 	struct {
74 		size_t rows;
75 		size_t cols;
76 	} initial_hint;
77 
78 /* when the terminal has died and !die_on_term, we need to be able
79  * to re-populate the contents on resize since the terminal state machine
80  * itself won't be able to anymore - so save this here */
81 	struct line** volatile _Atomic restore;
82 	size_t restore_cxy[2];
83 
84 /* if the client provides large b64 encoded data through OSC */
85 	volatile struct {
86 		uint8_t* buf;
87 		size_t buf_sz;
88 	} pending_bout;
89 
90 /* sockets to communicate between terminal thread and render thread */
91 	int dirtyfd;
92 	int signalfd;
93 
94 } term = {
95 	.die_on_term = true,
96 	.synch = PTHREAD_MUTEX_INITIALIZER,
97 	.hold = PTHREAD_MUTEX_INITIALIZER
98 };
99 
reset_restore_buffer()100 static void reset_restore_buffer()
101 {
102 	struct line** cur = atomic_load(&term.restore);
103 	atomic_store(&term.restore, NULL);
104 
105 	if (!cur)
106 		return;
107 
108 	for(size_t i = 0; cur[i]; i++){
109 		if (cur[i]->count)
110 			free(cur[i]->cells);
111 		free(cur[i]);
112 	}
113 
114 	free(cur);
115 }
116 
apply_restore_buffer()117 static void apply_restore_buffer()
118 {
119 	arcan_tui_erase_screen(term.screen, false);
120 	struct line** cur = atomic_load(&term.restore);
121 	if (!cur)
122 		return;
123 
124 	size_t rows, cols;
125 	arcan_tui_dimensions(term.screen, &rows, &cols);
126 
127 	for (size_t row = 0; row < rows && cur[row]; row++){
128 		arcan_tui_move_to(term.screen, 0, row);
129 		struct tui_cell* cells = cur[row]->cells;
130 		size_t n = cur[row]->count;
131 		bool dirty = false;
132 
133 		for (size_t i = 0; i < n && i < cols; i++){
134 			struct tui_cell* c= &cells[i];
135 			uint32_t ch = c->draw_ch ? c->draw_ch : c->ch;
136 			arcan_tui_write(term.screen, c->ch, &c->attr);
137 		}
138 	}
139 
140 	arcan_tui_move_to(term.screen, term.restore_cxy[0], term.restore_cxy[1]);
141 }
142 
create_restore_buffer(bool refit)143 static void create_restore_buffer(bool refit)
144 {
145 	reset_restore_buffer();
146 	size_t rows, cols;
147 	if (!term.screen)
148 		return;
149 
150 	arcan_tui_dimensions(term.screen, &rows, &cols);
151 	size_t bufsz = sizeof(struct line*) * (rows + 1);
152 	struct line** buffer = malloc(bufsz);
153 	memset(buffer, '\0', bufsz);
154 
155 	arcan_tui_cursorpos(term.screen, &term.restore_cxy[0], &term.restore_cxy[1]);
156 	size_t max_row = 0;
157 	size_t max_col = 0;
158 
159 	for (size_t row = 0; row < rows; row++){
160 		buffer[row] = malloc(sizeof(struct line));
161 		if (!buffer[row])
162 			goto fail;
163 
164 		bufsz = sizeof(struct tui_cell) * cols;
165 		struct tui_cell* cells = malloc(bufsz);
166 
167 		if (!cells)
168 			goto fail;
169 
170 		buffer[row]->cells = cells;
171 		buffer[row]->count = cols;
172 
173 		memset(cells, '\0', bufsz);
174 		bool row_dirty = false;
175 
176 		for (size_t col = 0; col < cols; col++){
177 			struct tui_cell cell = arcan_tui_getxy(term.screen, col, row, true);
178 
179 			uint32_t ch = cell.draw_ch ? cell.draw_ch : cell.ch;
180 			row_dirty |= ch && ch != ' ';
181 
182 			if (ch && ch != ' ' && max_col < col+1)
183 					max_col = col+1;
184 
185 			cells[col] = cell;
186 		}
187 
188 		if (row_dirty)
189 			max_row = row+1;
190 	}
191 
192 	if (refit){
193 		if (max_row && max_col){
194 /* so that we can 're-fit' on a reset when fit_contents has already been sent */
195 			if (!term.initial_hint.rows || !term.initial_hint.cols){
196 				term.initial_hint.rows = rows;
197 				term.initial_hint.cols = cols;
198 			}
199 
200 			arcan_tui_wndhint(term.screen, NULL, (struct tui_constraints){
201 				.max_rows = max_row, .min_rows = max_row,
202 				.max_cols = max_col, .min_cols = max_col
203 			});
204 		}
205 
206 /* should we mark that the buffer is empty? */
207 	}
208 
209 	atomic_store(&term.restore, buffer);
210 	return;
211 
212 fail:
213 	atomic_store(&term.restore, buffer);
214 	reset_restore_buffer();
215 	term.die_on_term = true;
216 }
217 
trace(const char * msg,...)218 static inline void trace(const char* msg, ...)
219 {
220 #ifdef TRACE_ENABLE
221 	va_list args;
222 	va_start( args, msg );
223 		vfprintf(stderr,  msg, args );
224 	va_end( args);
225 	fprintf(stderr, "\n");
226 #endif
227 }
228 
229 extern int arcan_tuiint_dirty(struct tui_context* tui);
230 
flush_buffer(int fd,char dst[static4096])231 static ssize_t flush_buffer(int fd, char dst[static 4096])
232 {
233 	ssize_t nr = read(fd, dst, 4096);
234 	if (-1 == nr){
235 		if (errno == EAGAIN || errno == EINTR)
236 			return -1;
237 
238 		atomic_store(&term.alive, false);
239 		arcan_tui_set_flags(term.screen, TUI_HIDE_CURSOR);
240 
241 		return -1;
242 	}
243 	return nr;
244 }
245 
synch_quit(int fd)246 static bool synch_quit(int fd)
247 {
248 	char buf[256];
249 	ssize_t nr = read(fd, buf, 256);
250 	for (ssize_t i = 0; i < nr; i++)
251 		if (buf[i] == 'q')
252 			return true;
253 
254 	return false;
255 }
256 
flush_ascii(uint8_t * buf,size_t nb,bool raw)257 static void flush_ascii(uint8_t* buf, size_t nb, bool raw)
258 {
259 	size_t pos = 0;
260 
261 	while (pos < nb){
262 		if (isascii(buf[pos])){
263 			if (!raw && buf[pos] == '\n')
264 				arcan_tui_newline(term.screen);
265 			else
266 				arcan_tui_write(term.screen, buf[pos], NULL);
267 		}
268 		pos++;
269 	}
270 }
271 
write_number(size_t num)272 static void write_number(size_t num)
273 {
274 	arcan_tui_printf(term.screen, NULL, "MISSING");
275 }
276 
flush_stats()277 static void flush_stats()
278 {
279 	arcan_tui_erase_screen(term.screen, false);
280 	arcan_tui_move_to(term.screen, 0, 0);
281 	arcan_tui_printf(term.screen, NULL, "In: ");
282 	write_number(term.bytes_in);
283 	arcan_tui_move_to(term.screen, 0, 1);
284 	arcan_tui_printf(term.screen, NULL, "Out: ");
285 	write_number(term.bytes_out);
286 }
287 
flush_utf8(uint8_t * buf,size_t nb)288 static void flush_utf8(uint8_t* buf, size_t nb)
289 {
290 	arcan_tui_move_to(term.screen, 0, 0);
291 	arcan_tui_printf(term.screen, NULL, "MISSING-U8");
292 }
293 
flush_out(uint8_t * buf,size_t * left,size_t * ofs,int fdout,bool * die)294 static void flush_out(uint8_t* buf, size_t* left, size_t* ofs, int fdout, bool* die)
295 {
296 	struct pollfd set[] = {
297 		{.fd = fdout, .events = POLLOUT},
298 		{.fd = term.dirtyfd, .events = POLLIN}
299 	};
300 
301 	if (!*left)
302 		return;
303 
304 	if (-1 == poll(set, 2, *die ? 0 : -1))
305 		return;
306 
307 	if (set[1].revents && synch_quit(term.dirtyfd)){
308 		*die = true;
309 		return;
310 	}
311 
312 /* flushing out is allowed to be blocking unless >die< is already set */
313 	ssize_t nw = 0;
314 	if (set[0].revents)
315 		nw = write(fdout, &buf[*ofs], *left);
316 
317 	if (nw < 0){
318 		if (errno != EAGAIN && errno != EINTR){
319 			*die = true;
320 		}
321 		return;
322 	}
323 
324 	term.bytes_out += nw;
325 	if (term.pipe_mode == PIPE_STATS_BASIC)
326 		flush_stats();
327 
328 	*left -= nw;
329 	*ofs += nw;
330 
331 /* tail recurse until flushed or the >die< trigger gets set */
332 	return flush_out(buf, left, ofs, fdout, die);
333 }
334 
readout_pty(int fd)335 static bool readout_pty(int fd)
336 {
337 	char buf[4096];
338 	bool got_hold = false;
339 	ssize_t nr = flush_buffer(fd, buf);
340 
341 	if (nr < 0)
342 		return false;
343 
344 	if (nr == 0)
345 		return true;
346 
347 	if (0 != pthread_mutex_trylock(&term.synch)){
348 		pthread_mutex_lock(&term.hold);
349 		write(term.dirtyfd, &(char){'1'}, 1);
350 		pthread_mutex_lock(&term.synch);
351 		got_hold = true;
352 	}
353 
354 	tsm_vte_input(term.vte, buf, nr);
355 
356 /* We could possibly also match against parser state, or specific total timeout
357  * before breaking out and releasing the terminal - the reason for this
358  * complicated scheme is to try and balance latency vs throughput vs tearing */
359 	size_t w, h;
360 	arcan_tui_dimensions(term.screen, &h, &w);
361 	ssize_t cap = w * h * 4;
362 	while (nr > 0 && cap > 0 && 1 == poll(
363 		(struct pollfd[]){ {.fd = fd, .events = POLLIN } }, 1, 0)){
364 		nr = flush_buffer(fd, buf);
365 		if (nr > 0){
366 			tsm_vte_input(term.vte, buf, nr);
367 			cap -= nr;
368 		}
369 	}
370 
371 	if (got_hold){
372 		pthread_mutex_unlock(&term.hold);
373 	}
374 	pthread_mutex_unlock(&term.synch);
375 
376 	return true;
377 }
378 
379 /*
380  * In 'pipe' mode we just buffer in from the fake-pty (exec) and from STDIN,
381  * flush to STDOUT and update the 'view' with either statistics or some
382  * filtered version of the data itself.
383  */
pump_pipe()384 static void* pump_pipe()
385 {
386 	uint8_t buffer[4096];
387 	size_t left = 0;
388 	size_t ofs = 0;
389 	bool die = false;
390 	int out_dst = STDOUT_FILENO;
391 
392 	struct pollfd set[] = {
393 		{.fd = shl_pty_get_fd(term.pty, false), .events = POLLIN | POLLERR | POLLNVAL | POLLHUP},
394 		{.fd = STDIN_FILENO, .events = POLLIN},
395 		{.fd = term.dirtyfd, .events = POLLIN},
396 	};
397 
398 /* The reason for having the signaling socket alive here still is also to allow
399  * for a future 'detach' mode where an input label allows us to let the stdio
400  * mapping remain and drop the ability to inspect - i.e. just fork / splice */
401 	while (atomic_load(&term.alive) && !die){
402 		if (left){
403 /* we can get here if the incoming pipe or pty dies, then we should still flush */
404 			flush_out(buffer, &left, &ofs, out_dst, &die);
405 			continue;
406 		}
407 
408 		if (-1 == poll(set, sizeof(set) / sizeof(set[0]), -1))
409 			continue;
410 
411 /* Signal to quit takes priority as it might mean reset and then the data is
412  * considered outdated and the pty will be closed */
413 		if (set[2].revents && synch_quit(term.dirtyfd)){
414 			die = true;
415 			break;
416 		}
417 
418 		ssize_t nr = 0;
419 
420 /* Then flushing pty takes priority so that we don't block on backpressure,
421  * and we show the output of the pty routing */
422 		if (set[0].revents & POLLIN){
423 			nr = read(set[0].fd, buffer, 4096);
424 
425 			if (nr > 0){
426 				term.bytes_in += nr;
427 
428 /* different pipe modes have different agendas here, and we might want to
429  * be able to toggle representation (RAW, UTF8/whitespace, statistics) */
430 				switch(term.pipe_mode){
431 					case PIPE_RAW:
432 						flush_ascii(buffer, nr, true);
433 					break;
434 					case PIPE_PLAIN_LF:
435 						flush_ascii(buffer, nr, false);
436 					break;
437 					case PIPE_STATS_BASIC:
438 						flush_stats(buffer, nr);
439 					break;
440 					case PIPE_UTF8:
441 						flush_utf8(buffer, nr);
442 					break;
443 				}
444 				left = nr;
445 				ofs = 0;
446 				out_dst = STDOUT_FILENO;
447 				continue;
448 			}
449 		}
450 
451 /* If the data >COMES< from STDIN we have to route to the PTY, which may bounce
452  * back or it may perform some kind of processing - this could've safely been a
453  * separate thread, but then would face problems with 'reset' state synch in the
454  * same way we use the signalling socket now */
455 		if (nr <= 0 && (set[1].revents & POLLIN)){
456 			nr = read(set[1].fd, buffer, 4096);
457 			if (nr > 0){
458 				left = nr;
459 				ofs = 0;
460 				out_dst = shl_pty_get_fd(term.pty, true);
461 				continue;
462 			}
463 		}
464 
465 /* Then if the dies we should give up, not if STDIN does as the pty can
466  * still emit data as a function of previous input (decompression for instance).
467  * STDOUT is checked in the flush out routine below */
468 		if (nr <= 0){
469 			if (set[1].revents & (POLLERR | POLLNVAL | POLLHUP)){
470 				die = true;
471 				break;
472 			}
473 			continue;
474 		}
475 	}
476 
477 /* allow flush out unless we have received a 'quit now' */
478 	flush_out(buffer, &left, &ofs, out_dst, &die);
479 
480 /* could possibly check what is mapped on stdin, proc-scrape the backing store
481  * and figure out if it is possible to grok the size - but hardly worth it */
482 	arcan_tui_progress(term.screen, TUI_PROGRESS_INTERNAL, 1.0);
483 
484 	write(term.dirtyfd, &(char){'Q'}, 1);
485 	atomic_store(&term.alive, false);
486 
487 	return NULL;
488 }
489 
pump_pty()490 static void* pump_pty()
491 {
492 	int fd = shl_pty_get_fd(term.pty, false);
493 	short pollev = POLLIN | POLLERR | POLLNVAL | POLLHUP;
494 
495 	struct pollfd set[2] = {
496 		{.fd = fd, .events = pollev},
497 		{.fd = term.dirtyfd, pollev},
498 	};
499 
500 	while (atomic_load(&term.alive)){
501 /* dispatch might just flush whatever is queued for writing, which can come from
502  * the callbacks in the UI thread */
503 		shl_pty_dispatch(term.pty);
504 
505 		if (-1 == poll(set, 2, -1))
506 			continue;
507 
508 /* tty determines lifecycle */
509 		if (set[0].revents){
510 			if (!readout_pty(fd) || (set[0].revents & POLLHUP))
511 				break;
512 		}
513 
514 /* flush signal / wakeup, quit if we got that value as we might need to reset */
515 		if (set[1].revents && synch_quit(set[1].fd))
516 			break;
517 	}
518 
519 	write(term.dirtyfd, &(char){'Q'}, 1);
520 	return NULL;
521 }
522 
dump_help()523 static void dump_help()
524 {
525 	fprintf(stdout, "Environment variables: \nARCAN_CONNPATH=path_to_server\n"
526 		"ARCAN_TERMINAL_EXEC=value : run value through /bin/sh -c instead of shell\n"
527 		"ARCAN_TERMINAL_ARGV : exec will route through execv instead of execvp\n"
528 		"ARCAN_TERMINAL_PIDFD_OUT : writes exec pid into pidfd\n"
529 		"ARCAN_TERMINAL_PIDFD_IN  : exec continues on incoming data\n\n"
530 	  "ARCAN_ARG=packed_args (key1=value:key2:key3=value)\n\n"
531 		"Accepted packed_args:\n"
532 		"    key      \t   value   \t   description\n"
533 		"-------------\t-----------\t-----------------\n"
534 		" env         \t key=val   \t override default environment (repeatable)\n"
535 		" chdir       \t dir       \t change working dir before spawning shell\n"
536 		" bgalpha     \t rv(0..255)\t opacity (default: 255, opaque) - deprecated\n"
537 		" ci          \t ind,r,g,b \t override palette at index\n"
538 		" blink       \t ticks     \t set blink period, 0 to disable (default: 12)\n"
539 		" login       \t [user]    \t login (optional: user, only works for root)\n"
540 #ifndef FSRV_TERMINAL_NOEXEC
541 		" exec        \t cmd       \t allows arcan scripts to run shell commands\n"
542 #endif
543 		" keep_alive  \t           \t don't exit if the terminal or shell terminates\n"
544 		" autofit     \t           \t (with exec, keep_alive) shrink window to fit\n"
545 		" pipe        \t [mode]    \t map stdin-stdout (mode: raw, lf)\n"
546 		" palette     \t name      \t use built-in palette (below)\n"
547 		" cli         \t           \t switch to non-vt cli/builtin shell mode\n"
548 		"Built-in palettes:\n"
549 		"default, solarized, solarized-black, solarized-white, srcery\n"
550 		"-------------\t-----------\t----------------\n\n"
551 		"Cli mode (pty-less) specific args:\n"
552 		"    key      \t   value   \t   description\n"
553 		"-------------\t-----------\t-----------------\n"
554 		" env         \t key=val   \t override default environment (repeatable)\n"
555 		" mode        \t exec_mode \t arcan, wayland, x11, vt100 (default: vt100)\n"
556 #ifndef FSRV_TERMINAL_NOEXEC
557 		" oneshot     \t           \t use with exec, shut down after evaluating command\n"
558 		"-------------\t-----------\t----------------\n"
559 #endif
560 	);
561 }
562 
tsm_log(void * data,const char * file,int line,const char * func,const char * subs,unsigned int sev,const char * fmt,va_list arg)563 static void tsm_log(void* data, const char* file, int line,
564 	const char* func, const char* subs, unsigned int sev,
565 	const char* fmt, va_list arg)
566 {
567 	fprintf(stderr, "[%d] %s:%d - %s, %s()\n", sev, file, line, subs, func);
568 	vfprintf(stderr, fmt, arg);
569 }
570 
sighuph(int num)571 static void sighuph(int num)
572 {
573 	if (term.pty)
574 		term.pty = (shl_pty_close(term.pty), NULL);
575 }
576 
on_subwindow(struct tui_context * c,arcan_tui_conn * newconn,uint32_t id,uint8_t type,void * tag)577 static bool on_subwindow(struct tui_context* c,
578 	arcan_tui_conn* newconn, uint32_t id, uint8_t type, void* tag)
579 {
580 	struct tui_cbcfg cbcfg = {};
581 
582 	if (term.screens[1] || type != TUI_WND_DEBUG)
583 		return false;
584 
585 	return tsm_vte_debug(term.vte, &term.screens[1], newconn, c);
586 }
587 
on_mouse_motion(struct tui_context * c,bool relative,int x,int y,int modifiers,void * t)588 static void on_mouse_motion(struct tui_context* c,
589 	bool relative, int x, int y, int modifiers, void* t)
590 {
591 	trace("mouse motion(%d:%d, mods:%d, rel: %d",
592 		x, y, modifiers, (int) relative);
593 
594 	if (!relative){
595 		tsm_vte_mouse_motion(term.vte, x, y, modifiers);
596 	}
597 }
598 
on_mouse_button(struct tui_context * c,int last_x,int last_y,int button,bool active,int modifiers,void * t)599 static void on_mouse_button(struct tui_context* c,
600 	int last_x, int last_y, int button, bool active, int modifiers, void* t)
601 {
602 	trace("mouse button(%d:%d - @%d,%d (mods: %d)\n",
603 		button, (int)active, last_x, last_y, modifiers);
604 	tsm_vte_mouse_button(term.vte, button, active, modifiers);
605 }
606 
on_key(struct tui_context * c,uint32_t keysym,uint8_t scancode,uint8_t mods,uint16_t subid,void * t)607 static void on_key(struct tui_context* c, uint32_t keysym,
608 	uint8_t scancode, uint8_t mods, uint16_t subid, void* t)
609 {
610 	trace("on_key(%"PRIu32",%"PRIu8",%"PRIu16")", keysym, scancode, subid);
611 	if (term.pipe)
612 		return;
613 
614 	tsm_vte_handle_keyboard(term.vte,
615 		keysym, isascii(keysym) ? keysym : 0, mods, subid);
616 }
617 
on_u8(struct tui_context * c,const char * u8,size_t len,void * t)618 static bool on_u8(struct tui_context* c, const char* u8, size_t len, void* t)
619 {
620 /* special little nuance here is that this goes straight to the descriptor,
621  * so there might be some conflict with queueing if we paste a large block */
622 	if (write(shl_pty_get_fd(term.pty, true), u8, len) < 0
623 		&& errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR){
624 		atomic_store(&term.alive, false);
625 		arcan_tui_set_flags(c, TUI_HIDE_CURSOR);
626 	}
627 
628 	return true;
629 }
630 
on_utf8_paste(struct tui_context * c,const uint8_t * str,size_t len,bool cont,void * t)631 static void on_utf8_paste(struct tui_context* c,
632 	const uint8_t* str, size_t len, bool cont, void* t)
633 {
634 	trace("utf8-paste(%s):%d", str, (int) cont);
635 	tsm_vte_paste(term.vte, (char*)str, len);
636 }
637 
638 static unsigned long long last_frame;
639 
on_resize(struct tui_context * c,size_t neww,size_t newh,size_t col,size_t row,void * t)640 static void on_resize(struct tui_context* c,
641 	size_t neww, size_t newh, size_t col, size_t row, void* t)
642 {
643 	create_restore_buffer(false);
644 }
645 
on_resized(struct tui_context * c,size_t neww,size_t newh,size_t col,size_t row,void * t)646 static void on_resized(struct tui_context* c,
647 	size_t neww, size_t newh, size_t col, size_t row, void* t)
648 {
649 	trace("resize(%zu(%zu),%zu(%zu))", neww, col, newh, row);
650 	if (atomic_load(&term.restore) && !term.alive){
651 		apply_restore_buffer();
652 	}
653 
654 	if (term.pty){
655 		shl_pty_resize(term.pty, col, row);
656 	}
657 
658 	last_frame = 0;
659 }
660 
write_callback(struct tsm_vte * vte,const char * u8,size_t len,void * data)661 static void write_callback(struct tsm_vte* vte,
662 	const char* u8, size_t len, void* data)
663 {
664 	shl_pty_write(term.pty, u8, len);
665 }
666 
str_callback(struct tsm_vte * vte,enum tsm_vte_group group,const char * msg,size_t len,bool crop,void * data)667 static void str_callback(struct tsm_vte* vte, enum tsm_vte_group group,
668 	const char* msg, size_t len, bool crop, void* data)
669 {
670 /* parse and see if we should set title */
671 	if (!msg || len < 3 || crop){
672 		debug_log(vte,
673 			"bad OSC sequence, len = %zu (%s)\n", len, msg ? msg : "");
674 		return;
675 	}
676 
677 /* 0, 1, 2 : set title */
678 	if ((msg[0] == '0' || msg[0] == '1' || msg[0] == '2') && msg[1] == ';'){
679 		arcan_tui_ident(term.screen, &msg[2]);
680 		return;
681 	}
682 
683 /* Clipboard controls
684  *
685  * Ps = 5 2
686  *
687  * param 1 : (zero or more of cpqs0..7), ignore and default to c
688  * param 2 : ? (paste) or base64encoded content, otherwise 'reset'
689  *
690  * this (should) come base64 */
691 	if (len > 5 && msg[0] == '5' && msg[1] == '2' && msg[2] == ';'){
692 		size_t i = 3;
693 
694 /* skip to second argument */
695 		for (; i < len-1 && msg[i] != ';'; i++){}
696 		i++;
697 
698 		if (i >= len){
699 			debug_log(vte, "OSC 5 2 sequence overflow\n");
700 			return;
701 		}
702 
703 /* won't be shared with other clients so it is rather pointless to have paste,
704  * we could practically announce any bin-io as capable input, keep it around
705  * and on paste- request deal with it - but serial transfer this way is pain */
706 		if (msg[i] == '?'){
707 			debug_log(vte, "OSC 5 2 paste unsupported\n");
708 			return;
709 		}
710 
711 		size_t outlen;
712 		uint8_t* outb = from_base64((const uint8_t*)&msg[i], &outlen);
713 		if (!outb){
714 			debug_log(vte, "OSC 5 2 bad base64 encoding\n");
715 			return;
716 		}
717 
718 /* there are multiple paths we might need to take dependent on the contents,
719  * if it is not a proper string or too long we can only really announce it as
720  * a bchunk */
721 		size_t pos = 0;
722 		for (;outb[pos] && pos < outlen; pos++){}
723 		bool is_terminated = pos < outlen && !outb[pos];
724 
725 /* if it actually behaves and looks like short utf8- go with that */
726 		if (is_terminated && outlen < 8192){
727 			if (arcan_tui_copy(term.screen, (const char*) outb)){
728 				free(outb);
729 				return;
730 			}
731 		}
732 
733 /* from_base64 always adds null-termination here, even when we might not want it */
734 		outlen--;
735 
736 /* otherwise send as an immediate file transfer request rather than clipboard */
737 		if (term.pending_bout.buf)
738 			free(term.pending_bout.buf);
739 		term.pending_bout.buf = outb;
740 		term.pending_bout.buf_sz = outlen;
741 
742 		arcan_tui_announce_io(term.screen, true, NULL, "bin");
743 		return;
744 	}
745 
746 	debug_log(vte,
747 		"%d:unhandled OSC command (PS: %d), len: %zu\n",
748 		vte->log_ctr++, (int)msg[0], len
749 	);
750 }
751 
get_shellenv()752 static char* get_shellenv()
753 {
754 	char* shell = getenv("SHELL");
755 
756 	if (!getenv("PATH"))
757 		setenv("PATH", "/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin", 1);
758 
759 	const struct passwd* pass = getpwuid( getuid() );
760 	if (pass){
761 		setenv("LOGNAME", pass->pw_name, 1);
762 		setenv("USER", pass->pw_name, 1);
763 		setenv("SHELL", pass->pw_shell, 0);
764 		setenv("HOME", pass->pw_dir, 0);
765 		shell = pass->pw_shell;
766 	}
767 
768 	/* some safe default should it be needed */
769 	if (!shell)
770 		shell = "/bin/sh";
771 
772 /* will be exec:ed so don't worry to much about leak or mgmt */
773 	return shell;
774 }
775 
group_expand(struct group_ent * group,const char * in)776 static char* group_expand(struct group_ent* group, const char* in)
777 {
778 	return strdup(in);
779 }
780 
build_argv(char * appname,char * instr)781 static char** build_argv(char* appname, char* instr)
782 {
783 	struct group_ent groups[] = {
784 		{.enter = '"', .leave = '"', .expand = group_expand},
785 		{.enter = '\0', .leave = '\0', .expand = NULL}
786 	};
787 
788 	struct argv_parse_opt opts = {
789 		.prepad = 1,
790 		.groups = groups,
791 		.sep = ' '
792 	};
793 
794 	ssize_t err_ofs = -1;
795 	char** res = extract_argv(instr, opts, &err_ofs);
796 	if (res)
797 		res[0] = appname;
798 
799 	return res;
800 }
801 
setup_shell(struct arg_arr * argarr,char * const args[])802 static void setup_shell(struct arg_arr* argarr, char* const args[])
803 {
804 	static const char* unset[] = {
805 		"COLUMNS", "LINES", "TERMCAP",
806 		"ARCAN_ARG", "ARCAN_APPLPATH", "ARCAN_APPLTEMPPATH",
807 		"ARCAN_FRAMESERVER_LOGDIR", "ARCAN_RESOURCEPATH",
808 		"ARCAN_SHMKEY", "ARCAN_SOCKIN_FD", "ARCAN_STATEPATH"
809 	};
810 
811 	int ind = 0;
812 	const char* val;
813 
814 	for (int i=0; i < sizeof(unset)/sizeof(unset[0]); i++)
815 		unsetenv(unset[i]);
816 
817 /* set some of the common UTF-8 default envs, shell overrides if needed */
818 	setenv("LANG", "en_GB.UTF-8", 0);
819 	setenv("LC_CTYPE", "en_GB.UTF-8", 0);
820 
821 /* FIXME: check what we should do with PWD, SHELL, TMPDIR, TERM, TZ,
822  * DATEMSK, LINES, LOGNAME(portable set), MSGVERB, PATH */
823 
824 /* might get overridden with putenv below, or if we are exec:ing /bin/login */
825 #ifdef __OpenBSD__
826 	setenv("TERM", "wsvt25", 1);
827 #else
828 	setenv("TERM", "xterm-256color", 1);
829 #endif
830 
831 	while (arg_lookup(argarr, "env", ind++, &val))
832 		putenv(strdup(val));
833 
834 	if (arg_lookup(argarr, "chdir", 0, &val)){
835 		chdir(val);
836 	}
837 
838 #ifndef NSIG
839 #define NSIG 32
840 #endif
841 
842 /* so many different contexts and handover methods needed here and not really a
843  * clean 'ok we can get away with only doing this', the arcan-launch setups
844  * need argument passing in env, the afsrv_cli need re-exec with argv in argv,
845  * and some specialized features like debug handover may need both */
846 	char* exec_arg = getenv("ARCAN_TERMINAL_EXEC");
847 
848 #ifdef FSRV_TERMINAL_NOEXEC
849 	if (arg_lookup(argarr, "exec", 0, &val)){
850 		LOG("permission denied, noexec compiled in");
851 	}
852 #else
853 	if (arg_lookup(argarr, "exec", 0, &val)){
854 		exec_arg = strdup(val);
855 		arcan_tui_ident(term.screen, exec_arg);
856 	}
857 #endif
858 
859 	sigset_t sigset;
860 	sigemptyset(&sigset);
861 	pthread_sigmask(SIG_SETMASK, &sigset, NULL);
862 
863 	for (size_t i = 1; i < NSIG; i++)
864 		signal(i, SIG_DFL);
865 
866 /* special case, ARCAN_TERMINAL_EXEC skips the normal shell setup */
867 	if (exec_arg){
868 		char* inarg = getenv("ARCAN_TERMINAL_ARGV");
869 		char* args[] = {"/bin/sh", "-c" , exec_arg, NULL};
870 
871 		const char* pidfd_in = getenv("ARCAN_TERMINAL_PIDFD_IN");
872 		const char* pidfd_out = getenv("ARCAN_TERMINAL_PIDFD_OUT");
873 
874 /* forward our new child pid to the _out fd, and then blockread garbage */
875 		if (pidfd_in && pidfd_out){
876 			int infd = strtol(pidfd_in, NULL, 10);
877 			int outfd = strtol(pidfd_out, NULL, 10);
878 			pid_t pid = getpid();
879 			write(outfd, &pid, sizeof(pid));
880 			read(infd, &pid, 1);
881 			close(infd);
882 			close(outfd);
883 		}
884 
885 /* inherit some environment, filter the things we used */
886 		unsetenv("ARCAN_TERMINAL_EXEC");
887 		unsetenv("ARCAN_TERMINAL_PIDFD_IN");
888 		unsetenv("ARCAN_TERMINAL_PIDFD_OUT");
889 		unsetenv("ARCAN_TERMINAL_ARGV");
890 
891 /* two different forms of this, one uses the /bin/sh -c route with all the
892  * arguments in the packed exec string, the other splits into a binary and
893  * an argument, the latter matters */
894 		if (inarg)
895 			execvp(exec_arg, build_argv(exec_arg, inarg));
896 		else
897 			execv("/bin/sh", args);
898 
899 		exit(EXIT_FAILURE);
900 	}
901 
902 	execvp(args[0], args);
903 	exit(EXIT_FAILURE);
904 }
905 
on_subst(struct tui_context * tui,struct tui_cell * cells,size_t n_cells,size_t row,void * t)906 static bool on_subst(struct tui_context* tui,
907 	struct tui_cell* cells, size_t n_cells, size_t row, void* t)
908 {
909 	bool res = false;
910 	for (size_t i = 0; i < n_cells-1; i++){
911 /* far from an optimal shaping rule, but check for special forms of continuity,
912  * 3+ of (+_-) like shapes horizontal or vertical, n- runs of whitespace or
913  * vertical similarities in terms of whitespace+character
914  */
915 		if ( (isspace(cells[i].ch) && isspace(cells[i+1].ch)) ){
916 			cells[i].attr.aflags |= TUI_ATTR_SHAPE_BREAK;
917 			res = true;
918 		}
919 	}
920 
921 	return res;
922 }
923 
on_exec_state(struct tui_context * tui,int state,void * tag)924 static void on_exec_state(struct tui_context* tui, int state, void* tag)
925 {
926 	if (state == 0)
927 		shl_pty_signal(term.pty, SIGCONT);
928 	else if (state == 1)
929 		shl_pty_signal(term.pty, SIGSTOP);
930 	else if (state == 2)
931 		shl_pty_signal(term.pty, SIGHUP);
932 }
933 
setup_build_term()934 static bool setup_build_term()
935 {
936 	size_t rows = 0, cols = 0;
937 	arcan_tui_reset(term.screen);
938 	tsm_vte_hard_reset(term.vte);
939 	arcan_tui_dimensions(term.screen, &rows, &cols);
940 	term.complete_signal = false;
941 
942 /* just to re-use the same interfaces in the case where we want exec and
943  * just control stdin/stdout versus a full posix_openpt */
944 	if (term.pipe)
945 		term.child = shl_pipe_open(&term.pty, true);
946 	else
947 		term.child = shl_pty_open(&term.pty, NULL, NULL, cols, rows);
948 
949 	if (term.child < 0){
950 		arcan_tui_destroy(term.screen, "Shell process died unexpectedly");
951 		return false;
952 	}
953 
954 /*
955  * and lastly, spawn the pseudo-terminal
956  */
957 /* we're inside child */
958 	if (term.child == 0){
959 		const char* val;
960 		char* argv[] = {get_shellenv(), "-i", NULL, NULL};
961 
962 		if (arg_lookup(term.args, "cmd", 0, &val) && val){
963 			argv[2] = strdup(val);
964 		}
965 
966 /* special case handling for "login", this requires root */
967 		if (arg_lookup(term.args, "login", 0, &val)){
968 			struct stat buf;
969 			argv[1] = "-p";
970 			if (stat("/bin/login", &buf) == 0 && S_ISREG(buf.st_mode))
971 				argv[0] = "/bin/login";
972 			else if (stat("/usr/bin/login", &buf) == 0 && S_ISREG(buf.st_mode))
973 				argv[0] = "/usr/bin/login";
974 			else{
975 				LOG("login prompt requested but none was found\n");
976 				return EXIT_FAILURE;
977 			}
978 		}
979 
980 		setup_shell(term.args, argv);
981 		return EXIT_FAILURE;
982 	}
983 
984 /* spawn a thread that deals with feeding the tsm specifically, then we run
985  * our normal event look constantly in the normal process / refresh style. */
986 	pthread_t pth;
987 	pthread_attr_t pthattr;
988 	pthread_attr_init(&pthattr);
989 	pthread_attr_setdetachstate(&pthattr, PTHREAD_CREATE_DETACHED);
990 	atomic_store(&term.alive, true);
991 
992 /* with pipe- mode, we want stdout to be non-blocking */
993 	if (term.pipe){
994 		if (-1 == pthread_create(&pth, &pthattr, pump_pipe, NULL)){
995 			int flags = fcntl(STDOUT_FILENO, F_GETFL);
996 			fcntl(STDOUT_FILENO, flags | O_NONBLOCK);
997 			atomic_store(&term.alive, false);
998 			return EXIT_FAILURE;
999 		}
1000 	}
1001 	else {
1002 		if (-1 == pthread_create(&pth, &pthattr, pump_pty, NULL)){
1003 			atomic_store(&term.alive, false);
1004 			return EXIT_FAILURE;
1005 		}
1006 	}
1007 
1008 	return true;
1009 }
1010 
on_reset(struct tui_context * tui,int state,void * tag)1011 static void on_reset(struct tui_context* tui, int state, void* tag)
1012 {
1013 /* this state needs to be verified against pledge etc. as well since some
1014  * of the foreplay might become impossible after privsep */
1015 
1016 	switch (state){
1017 /* soft, just state machine + tui */
1018 	case 0:
1019 		arcan_tui_reset(tui);
1020 		tsm_vte_hard_reset(term.vte);
1021 	break;
1022 
1023 /* hard, try to re-execute command, send HUP if still alive then mark as dead */
1024 	case 1:
1025 		reset_restore_buffer();
1026 		tsm_vte_hard_reset(term.vte);
1027 
1028 /* hang-up any existing terminal, tell the current process thread to give up
1029  * and wait for that to be acknowledged. */
1030 		if (atomic_load(&term.alive)){
1031 			on_exec_state(tui, 2, tag);
1032 			char q = 'q';
1033 			write(term.signalfd, &q, 1);
1034 			while (q != 'Q'){
1035 				read(term.signalfd, &q, 1);
1036 			}
1037 			atomic_store(&term.alive, false);
1038 		}
1039 
1040 		if (!term.die_on_term){
1041 			arcan_tui_progress(term.screen, TUI_PROGRESS_INTERNAL, 0.0);
1042 		}
1043 
1044 		if (term.initial_hint.rows && term.initial_hint.cols){
1045 			arcan_tui_wndhint(term.screen, NULL, (struct tui_constraints){
1046 				.max_rows = term.initial_hint.rows, .min_rows = term.initial_hint.rows,
1047 				.max_cols = term.initial_hint.cols, .min_cols = term.initial_hint.cols
1048 			});
1049 		}
1050 
1051 		shl_pty_close(term.pty);
1052 		setup_build_term();
1053 	break;
1054 
1055 /* crash, ... ? do nothing */
1056 	default:
1057 	break;
1058 	}
1059 
1060 /* reset vte state */
1061 }
1062 
1063 struct labelent;
1064 struct labelent {
1065 	void (* handler)(struct labelent* self);
1066 	bool disabled;
1067 	int tag;
1068 	struct tui_labelent ent;
1069 };
1070 
force_autofit(struct labelent * S)1071 static void force_autofit(struct labelent* S)
1072 {
1073 	bool old_fit = term.fit_contents;
1074 	term.fit_contents = true;
1075 	create_restore_buffer(term.fit_contents);
1076 	term.fit_contents = old_fit;
1077 }
1078 
set_pipe(struct labelent * S)1079 static void set_pipe(struct labelent* S)
1080 {
1081 	arcan_tui_erase_screen(term.screen, false);
1082 	term.pipe_mode = S->tag;
1083 }
1084 
1085 static struct labelent labels[] =
1086 {
1087 	{
1088 		.handler = force_autofit,
1089 		.ent =
1090 		{
1091 			.label = "AUTOFIT",
1092 			.descr = "Resize window to buffer contents",
1093 			.initial = TUIK_F1,
1094 			.modifiers = TUIM_LSHIFT
1095 		}
1096 	},
1097 	{
1098 		.handler = set_pipe,
1099 		.disabled = true,
1100 		.tag = PIPE_RAW,
1101 		.ent =
1102 		{
1103 			.label = "VIEW_RAW",
1104 			.descr = "Show pipe output in window as unfiltered",
1105 			.initial = TUIK_F1
1106 		},
1107 	},
1108 	{
1109 		.handler = set_pipe,
1110 		.disabled = true,
1111 		.tag = PIPE_RAW,
1112 		.ent =
1113 		{
1114 			.label = "VIEW_RAW_LF",
1115 			.descr = "Show pipe output in window as unfiltered, respect linefeed",
1116 			.initial = TUIK_F1
1117 		},
1118 	},
1119 	{
1120 		.handler = set_pipe,
1121 		.disabled = true,
1122 		.tag = PIPE_PLAIN_LF,
1123 		.ent =
1124 		{
1125 			.label = "VIEW_UTF8",
1126 			.descr = "Convert input to UTF8, mark invalid values",
1127 			.initial = TUIK_F2
1128 		}
1129 	},
1130 	{
1131 		.handler = set_pipe,
1132 		.disabled = true,
1133 		.tag = PIPE_STATS_BASIC,
1134 		.ent =
1135 		{
1136 			.label = "VIEW_STATS",
1137 			.descr = "Only show read/written/pending",
1138 			.initial = TUIK_F3
1139 		}
1140 	},
1141 };
1142 
on_label_query(struct tui_context * T,size_t index,const char * country,const char * lang,struct tui_labelent * dstlbl,void * t)1143 static bool on_label_query(struct tui_context* T,
1144 	size_t index, const char* country, const char* lang,
1145 	struct tui_labelent* dstlbl, void* t)
1146 {
1147 	struct bufferwnd_meta* M = t;
1148 	size_t current = 0;
1149 
1150 /* poor complexity in this search but few entries */
1151 	do {
1152 		while(current < COUNT_OF(labels) && labels[current].disabled)
1153 			current++;
1154 	} while (index-- > 0 && current++ < COUNT_OF(labels));
1155 
1156 	if (current < COUNT_OF(labels) && !labels[current].disabled){
1157 		*dstlbl = labels[current].ent;
1158 		return true;
1159 	}
1160 	return false;
1161 }
1162 
on_label_input(struct tui_context * T,const char * label,bool active,void * tag)1163 static bool on_label_input(
1164 	struct tui_context* T, const char* label, bool active, void* tag)
1165 {
1166 	if (!active)
1167 		return true;
1168 
1169 	for (size_t i = 0; i < COUNT_OF(labels); i++){
1170 		if (strcmp(label, labels[i].ent.label) == 0 && !labels[i].disabled){
1171 			labels[i].handler(&labels[i]);
1172 			return true;
1173 		}
1174 	}
1175 
1176 	return false;
1177 }
1178 
on_bchunk(struct tui_context * c,bool input,uint64_t size,int fd,const char * tag,void * t)1179 static void on_bchunk(struct tui_context* c,
1180 	bool input, uint64_t size, int fd, const char* tag, void* t)
1181 {
1182 	if (input || !term.pending_bout.buf)
1183 		return;
1184 
1185 /* blocking rather dumb, but it is really just for OSC 5 2 */
1186 	FILE* fpek = fdopen(fd, "w+");
1187 	fwrite(term.pending_bout.buf, term.pending_bout.buf_sz, 1, fpek);
1188 	fclose(fpek);
1189 	free(term.pending_bout.buf);
1190 	term.pending_bout.buf = NULL;
1191 }
1192 
parse_color(const char * inv,uint8_t outv[4])1193 static int parse_color(const char* inv, uint8_t outv[4])
1194 {
1195 	return sscanf(inv, "%"SCNu8",%"SCNu8",%"SCNu8",%"SCNu8,
1196 		&outv[0], &outv[1], &outv[2], &outv[3]);
1197 }
1198 
copy_palette(struct tui_context * tc,uint8_t * out)1199 static bool copy_palette(struct tui_context* tc, uint8_t* out)
1200 {
1201 	uint8_t ref[3] = {0, 0, 0};
1202 	for (size_t i = TUI_COL_TBASE; i < TUI_COL_LIMIT; i++){
1203 		size_t ofs = (i - TUI_COL_TBASE) * 3;
1204 		arcan_tui_get_color(tc, i, &out[ofs]);
1205 	}
1206 
1207 /* special hack, the bg color for the reserved-1 slot will be set if we
1208  * have received an upstream palette */
1209 	arcan_tui_get_bgcolor(tc, 1, ref);
1210 	return ref[0] == 255;
1211 }
1212 
afsrv_terminal(struct arcan_shmif_cont * con,struct arg_arr * args)1213 int afsrv_terminal(struct arcan_shmif_cont* con, struct arg_arr* args)
1214 {
1215 	if (!con)
1216 		return EXIT_FAILURE;
1217 
1218 	const char* val;
1219 /* more possible modes needed here, UTF8, stats */
1220 	if (arg_lookup(args, "pipe", 0, &val)){
1221 		term.pipe = true;
1222 		if (val && strcmp(val, "lf") == 0)
1223 			term.pipe_mode = PIPE_PLAIN_LF;
1224 	}
1225 
1226 /*
1227  * this is the first migration part we have out of the normal vt- legacy,
1228  * see cli.c
1229  */
1230 	if (arg_lookup(args, "cli", 0, NULL)){
1231 		return arcterm_cli_run(con, args);
1232 	}
1233 
1234 	if (arg_lookup(args, "help", 0, &val)){
1235 		dump_help();
1236 		return EXIT_SUCCESS;
1237 	}
1238 
1239 /*
1240  * this table act as both callback- entry points and a list of features that we
1241  * actually use. So binary chunk transfers, video/audio paste, geohint etc.
1242  * are all ignored and disabled
1243  */
1244 	struct tui_cbcfg cbcfg = {
1245 		.input_mouse_motion = on_mouse_motion,
1246 		.input_mouse_button = on_mouse_button,
1247 		.query_label = on_label_query,
1248 		.input_label = on_label_input,
1249 		.input_utf8 = on_u8,
1250 		.input_key = on_key,
1251 		.bchunk = on_bchunk,
1252 		.utf8 = on_utf8_paste,
1253 		.resize = on_resize,
1254 		.resized = on_resized,
1255 		.subwindow = on_subwindow,
1256 		.exec_state = on_exec_state,
1257 		.reset = on_reset,
1258 /*
1259  * for advanced rendering, but not that interesting
1260  * .substitute = on_subst
1261  */
1262 	};
1263 
1264 	term.screen = arcan_tui_setup(con, NULL, &cbcfg, sizeof(cbcfg));
1265 
1266 	if (!term.screen){
1267 		fprintf(stderr, "failed to setup TUI connection\n");
1268 		return EXIT_FAILURE;
1269 	}
1270 
1271 /* make a preroll- state copy of legacy-palette range */
1272 	uint8_t palette_copy[TUI_COL_LIMIT * 3];
1273 	bool custom_palette = copy_palette(term.screen, palette_copy);
1274 
1275 	term.args = args;
1276 
1277 /*
1278  * Now we have the display server connection and the abstract screen,
1279  * configure the terminal state machine. This will override the palette
1280  * that might be inside tui, so rebuild from our copy.
1281  */
1282 	if (tsm_vte_new(&term.vte, term.screen, write_callback, NULL) < 0){
1283 		arcan_tui_destroy(term.screen, "Couldn't setup terminal emulator");
1284 		return EXIT_FAILURE;
1285 	}
1286 
1287 /*
1288  * allow the window state to survive, terminal won't be updated but
1289  * other tui behaviors are still valid
1290  */
1291 	if (arg_lookup(args, "keep_alive", 0, NULL)){
1292 		term.die_on_term = false;
1293 		arcan_tui_progress(term.screen, TUI_PROGRESS_INTERNAL, 0.0);
1294 	}
1295 
1296 /*
1297  * when the keep_alive state is entered, try and shrink the window
1298  * to the bounding box of the contents itself
1299  */
1300 	if (arg_lookup(args, "autofit", 0, NULL)){
1301 		term.fit_contents = true;
1302 	}
1303 
1304 /* if a command-line palette override is set, apply that - BUT if there was
1305  * custom color overrides defined during preroll (tui_setup) those take
1306  * precedence */
1307 	if (arg_lookup(args, "palette", 0, &val)){
1308 		tsm_vte_set_palette(term.vte, val);
1309 	}
1310 
1311 /* synch back custom colors */
1312 	if (custom_palette){
1313 		for (size_t i = 0; i < VTE_COLOR_NUM; i++){
1314 			tsm_vte_set_color(term.vte, i, &palette_copy[i * 3]);
1315 		}
1316 	}
1317 
1318 /* DEPRECATED - custom command-line color overrides */
1319 	int ind = 0;
1320 	uint8_t ccol[4];
1321 	while(arg_lookup(args, "ci", ind++, &val)){
1322 		if (4 == parse_color(val, ccol))
1323 			tsm_vte_set_color(term.vte, ccol[0], &ccol[1]);
1324 	}
1325 
1326 /* Immediately reset / draw so that we get a window before the shell wakes */
1327 	arcan_tui_reset_flags(term.screen, TUI_ALTERNATE);
1328 	arcan_tui_erase_screen(term.screen, NULL);
1329 	arcan_tui_refresh(term.screen);
1330 
1331 	tsm_set_strhandler(term.vte, str_callback, 256, NULL);
1332 
1333 	signal(SIGHUP, sighuph);
1334 
1335 /* socket pair used to signal between the threads, this will be kept
1336  * alive even between reset/re-execute on a terminated terminal */
1337 	int pair[2];
1338 	if (-1 == socketpair(AF_UNIX, SOCK_STREAM, 0, pair))
1339 		return EXIT_FAILURE;
1340 
1341 	term.dirtyfd = pair[0];
1342 	term.signalfd = pair[1];
1343 
1344 	if (!setup_build_term())
1345 		return EXIT_FAILURE;
1346 
1347 #ifdef __OpenBSD__
1348 	pledge(SHMIF_PLEDGE_PREFIX " tty", NULL);
1349 #endif
1350 
1351 /* re-fetch fg/bg from the vte so the palette can be considered, slated
1352  * to be removed when the builtin/ scripts cover the color definition bits */
1353 	uint8_t fgc[3], bgc[3];
1354 	tsm_vte_get_color(term.vte, VTE_COLOR_BACKGROUND, bgc);
1355 	tsm_vte_get_color(term.vte, VTE_COLOR_FOREGROUND, fgc);
1356 	arcan_tui_set_color(term.screen, TUI_COL_BG, bgc);
1357 	arcan_tui_set_color(term.screen, TUI_COL_TEXT, fgc);
1358 
1359 	term.screens[0] = term.screen;
1360 
1361 	bool alive;
1362 	while((alive = atomic_load(&term.alive)) || !term.die_on_term){
1363 		pthread_mutex_lock(&term.synch);
1364 		tsm_vte_update_debug(term.vte);
1365 
1366 		struct tui_process_res res = arcan_tui_process(
1367 			term.screens, term.screens[1] ? 2 : 1, &term.signalfd, 1, -1);
1368 
1369 		if (res.errc < TUI_ERRC_OK){
1370 			break;
1371 		}
1372 
1373 /* indicate that we are finished so the user has the option to reset rather
1374  * than terminate, make sure this is done only once per running cycle */
1375 		if (!term.alive && !term.die_on_term && !term.complete_signal){
1376 			arcan_tui_progress(term.screen, TUI_PROGRESS_INTERNAL, 1.0);
1377 			term.complete_signal = true;
1378 		}
1379 
1380 		arcan_tui_refresh(term.screens[0]);
1381 		if (term.screens[1])
1382 			arcan_tui_refresh(term.screens[1]);
1383 
1384 /* screen contents have been synched and updated, but we don't have a
1385  * restore spot for dealing with resize or contents boundary */
1386 		if (!alive && !atomic_load(&term.restore)){
1387 			create_restore_buffer(term.fit_contents);
1388 		}
1389 
1390 	/* flush out the signal pipe, don't care about contents, assume
1391 	 * it is about unlocking for now */
1392 		pthread_mutex_unlock(&term.synch);
1393 		if (res.ok){
1394 			char buf[256];
1395 			read(term.signalfd, buf, 256);
1396 			pthread_mutex_lock(&term.hold);
1397 			pthread_mutex_unlock(&term.hold);
1398 		}
1399 	}
1400 
1401 	arcan_tui_destroy(term.screen, NULL);
1402 	return EXIT_SUCCESS;
1403 }
1404