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