1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
3 This file is part of systemd.
5 Copyright (C) 2014 David Herrmann <dh.herrmann@gmail.com>
7 systemd is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Lesser General Public License as published by
9 the Free Software Foundation; either version 2.1 of the License, or
10 (at your option) any later version.
12 systemd is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public License
18 along with systemd; If not, see <http://www.gnu.org/licenses/>.
22 * Stacked Terminal-Emulator
23 * This is an interactive test of the term_screen implementation. It runs a
24 * fully-fletched terminal-emulator inside of a parent TTY. That is, instead of
25 * rendering the terminal as X11-window, it renders it as sub-window in the
26 * parent TTY. Think of this like what "GNU-screen" does.
36 #include <sys/ioctl.h>
42 #include "term-internal.h"
46 typedef struct Output Output;
47 typedef struct Terminal Terminal;
53 unsigned int in_width;
54 unsigned int in_height;
55 unsigned int cursor_x;
56 unsigned int cursor_y;
67 sd_event_source *frame_timer;
75 struct termios saved_in_attr;
76 struct termios saved_out_attr;
81 bool is_scheduled : 1;
90 #define BORDER_HORIZ "\xe2\x94\x81"
91 #define BORDER_VERT "\xe2\x94\x83"
92 #define BORDER_VERT_RIGHT "\xe2\x94\xa3"
93 #define BORDER_VERT_LEFT "\xe2\x94\xab"
94 #define BORDER_DOWN_RIGHT "\xe2\x94\x8f"
95 #define BORDER_DOWN_LEFT "\xe2\x94\x93"
96 #define BORDER_UP_RIGHT "\xe2\x94\x97"
97 #define BORDER_UP_LEFT "\xe2\x94\x9b"
99 static int output_winch(Output *o) {
100 struct winsize wsz = { };
103 assert_return(o, -EINVAL);
105 r = ioctl(o->fd, TIOCGWINSZ, &wsz);
107 return log_error_errno(errno, "error: cannot read window-size: %m");
109 if (wsz.ws_col != o->width || wsz.ws_row != o->height) {
110 o->width = wsz.ws_col;
111 o->height = wsz.ws_row;
112 o->in_width = MAX(o->width, 2U) - 2;
113 o->in_height = MAX(o->height, 6U) - 6;
120 static int output_flush(Output *o) {
126 r = loop_write(o->fd, o->obuf, o->n_obuf, false);
128 return log_error_errno(r, "error: cannot write to TTY: %m");
135 static int output_write(Output *o, const void *buf, size_t size) {
139 assert_return(o, -EINVAL);
140 assert_return(buf || size < 1, -EINVAL);
145 if (o->n_obuf + size > o->n_obuf && o->n_obuf + size <= sizeof(o->obuf)) {
146 memcpy(o->obuf + o->n_obuf, buf, size);
155 len = loop_write(o->fd, buf, size, false);
157 return log_error_errno(len, "error: cannot write to TTY (%zd): %m", len);
163 static int output_vnprintf(Output *o, size_t max, const char *format, va_list args) {
167 assert_return(o, -EINVAL);
168 assert_return(format, -EINVAL);
169 assert_return(max <= 4096, -EINVAL);
171 r = MIN(vsnprintf(buf, max, format, args), (int) max);
173 return output_write(o, buf, r);
177 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
181 va_start(args, format);
182 r = output_vnprintf(o, max, format, args);
189 static int output_vprintf(Output *o, const char *format, va_list args) {
193 assert_return(o, -EINVAL);
194 assert_return(format, -EINVAL);
196 r = vsnprintf(buf, sizeof(buf), format, args);
198 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
200 return output_write(o, buf, r);
204 static int output_printf(Output *o, const char *format, ...) {
208 va_start(args, format);
209 r = output_vprintf(o, format, args);
215 static int output_move_to(Output *o, unsigned int x, unsigned int y) {
218 assert_return(o, -EINVAL);
220 /* force the \e[H code as o->cursor_x/y might be out-of-date */
222 r = output_printf(o, "\e[%u;%uH", y + 1, x + 1);
231 static int output_print_line(Output *o, size_t len) {
233 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
234 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
235 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
236 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
237 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
238 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
239 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ;
240 const size_t max = (sizeof(line) - 1) / (sizeof(BORDER_HORIZ) - 1);
244 assert_return(o, -EINVAL);
246 for ( ; len > 0; len -= i) {
247 i = (len > max) ? max : len;
248 r = output_write(o, line, i * (sizeof(BORDER_HORIZ) - 1));
257 static int output_frame_printl(Output *o, const char *format, ...) {
265 if (o->cursor_y < 3 || o->cursor_y >= o->height - 3)
268 va_start(args, format);
269 r = output_vnprintf(o, o->width - 2, format, args);
275 return output_move_to(o, 1, o->cursor_y + 1);
278 static Output *output_free(Output *o) {
282 /* re-enable cursor */
283 output_printf(o, "\e[?25h");
284 /* disable alternate screen buffer */
285 output_printf(o, "\e[?1049l");
288 /* o->fd is owned by the caller */
294 static int output_new(Output **out, int fd) {
298 assert_return(out, -EINVAL);
310 /* enable alternate screen buffer */
311 r = output_printf(o, "\e[?1049h");
315 /* always hide cursor */
316 r = output_printf(o, "\e[?25l");
332 static void output_draw_frame(Output *o) {
337 /* print header-frame */
339 output_printf(o, BORDER_DOWN_RIGHT);
340 output_print_line(o, o->width - 2);
341 output_printf(o, BORDER_DOWN_LEFT
344 "\e[2;%uH" /* cursor-position: 2/x */
349 output_print_line(o, o->width - 2);
350 output_printf(o, BORDER_VERT_LEFT
353 /* print body-frame */
355 for (i = 0; i < o->in_height; ++i) {
356 output_printf(o, BORDER_VERT
357 "\e[%u;%uH" /* cursor-position: 2/x */
363 /* print footer-frame */
365 output_printf(o, BORDER_VERT_RIGHT);
366 output_print_line(o, o->width - 2);
367 output_printf(o, BORDER_VERT_LEFT
370 "\e[%u;%uH" /* cursor-position: 2/x */
374 o->height - 1, o->width);
375 output_print_line(o, o->width - 2);
376 output_printf(o, BORDER_UP_LEFT);
378 /* print header/footer text */
380 output_printf(o, "\e[2;3H");
381 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
382 output_printf(o, "\e[%u;3H", o->height - 1);
383 output_nprintf(o, o->width - 4, "press ^C to enter menu");
386 static void output_draw_menu(Output *o) {
389 output_frame_printl(o, "%s", "");
390 output_frame_printl(o, " Menu: (the following keys are recognized)");
391 output_frame_printl(o, " q: quit");
392 output_frame_printl(o, " ^C: send ^C to the PTY");
395 static int output_draw_cell_fn(term_screen *screen,
399 const term_attr *attr,
402 unsigned int ch_width) {
403 Output *o = userdata;
407 if (x >= o->in_width || y >= o->in_height)
410 if (x == 0 && y != 0)
411 output_printf(o, "\e[m\r\n" BORDER_VERT);
413 switch (attr->fg.ccode) {
414 case TERM_CCODE_DEFAULT:
415 output_printf(o, "\e[39m");
418 output_printf(o, "\e[38;5;%um", attr->fg.c256);
421 output_printf(o, "\e[38;2;%u;%u;%um", attr->fg.red, attr->fg.green, attr->fg.blue);
423 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
424 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 30);
426 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
427 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
431 switch (attr->bg.ccode) {
432 case TERM_CCODE_DEFAULT:
433 output_printf(o, "\e[49m");
436 output_printf(o, "\e[48;5;%um", attr->bg.c256);
439 output_printf(o, "\e[48;2;%u;%u;%um", attr->bg.red, attr->bg.green, attr->bg.blue);
441 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
442 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_BLACK + 40);
444 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
445 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
449 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
451 attr->italic ? 3 : 23,
452 attr->underline ? 4 : 24,
453 attr->inverse ? 7 : 27,
454 attr->blink ? 5 : 25,
455 attr->hidden ? 8 : 28);
458 output_printf(o, " ");
460 for (k = 0; k < n_ch; ++k) {
461 ulen = utf8_encode_unichar(utf8, ch[k]);
462 output_write(o, utf8, ulen);
469 static void output_draw_screen(Output *o, term_screen *s) {
473 term_screen_draw(s, output_draw_cell_fn, o, NULL);
475 output_printf(o, "\e[m");
478 static void output_draw(Output *o, bool menu, term_screen *screen) {
482 * This renders the contenst of the terminal. The layout contains a
483 * header, the main body and a footer. Around all areas we draw a
484 * border. It looks something like this:
486 * +----------------------------------------------------+
488 * +----------------------------------------------------+
497 * +----------------------------------------------------+
499 * +----------------------------------------------------+
501 * The body is the part that grows vertically.
503 * We need at least 6 vertical lines to render the screen. This would
504 * leave 0 lines for the body. Therefore, we require 7 lines so there's
505 * at least one body line. Similarly, we need 2 horizontal cells for the
506 * frame, so we require 3.
507 * If the window is too small, we print an error message instead.
510 if (o->in_width < 1 || o->in_height < 1) {
511 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
512 "\e[H"); /* cursor-position: home */
513 output_printf(o, "error: screen too small, need at least 3x7 cells");
519 output_printf(o, "\e[?25l");
521 /* frame-content is contant; only resizes can change it */
522 if (o->resized || o->in_menu != menu) {
523 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
524 "\e[H"); /* cursor-position: home */
525 output_draw_frame(o);
530 /* move cursor to child's position */
531 output_move_to(o, 1, 3);
536 output_draw_screen(o, screen);
539 * Hack: sd-term was not written to support TTY as output-objects, thus
540 * expects callers to use term_screen_feed_keyboard(). However, we
541 * forward TTY input directly. Hence, we're not notified about keypad
542 * changes. Update the related modes djring redraw to keep them at least
545 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
546 output_printf(o, "\e[?1h");
548 output_printf(o, "\e[?1l");
550 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
551 output_printf(o, "\e=");
553 output_printf(o, "\e>");
562 static void terminal_dirty(Terminal *t) {
568 if (t->is_scheduled) {
574 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
577 usec += 16 * USEC_PER_MSEC;
578 r = sd_event_source_set_time(t->frame_timer, usec);
580 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
582 t->is_scheduled = true;
586 output_draw(t->output, t->is_menu, t->screen);
589 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
590 Terminal *t = userdata;
592 t->is_scheduled = false;
599 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
600 Terminal *t = userdata;
603 output_winch(t->output);
606 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
608 log_error_errno(r, "error: pty_resize() (%d): %m", r);
611 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
613 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
620 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
627 len = utf8_encode_unichar(buf, ucs4);
631 r = ring_push(&t->out_ring, buf, len);
638 static int terminal_write_tmp(Terminal *t) {
645 num = ring_peek(&t->out_ring, vec);
650 for (i = 0; i < num; ++i) {
651 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
653 return log_error_errno(r, "error: cannot write to PTY (%d): %m", r);
657 ring_flush(&t->out_ring);
661 static void terminal_discard_tmp(Terminal *t) {
664 ring_flush(&t->out_ring);
667 static int terminal_menu(Terminal *t, const term_seq *seq) {
669 case TERM_SEQ_IGNORE:
671 case TERM_SEQ_GRAPHIC:
672 switch (seq->terminator) {
674 sd_event_exit(t->event, 0);
679 case TERM_SEQ_CONTROL:
680 switch (seq->terminator) {
682 terminal_push_tmp(t, 0x03);
683 terminal_write_tmp(t);
696 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
697 Terminal *t = userdata;
702 len = read(fd, buf, sizeof(buf));
704 if (errno == EAGAIN || errno == EINTR)
707 log_error_errno(errno, "error: cannot read from TTY (%d): %m", -errno);
711 for (i = 0; i < len; ++i) {
716 n_str = term_utf8_decode(&t->utf8, &str, buf[i]);
717 for (j = 0; j < n_str; ++j) {
718 type = term_parser_feed(t->parser, &seq, str[j]);
720 return log_error_errno(type, "error: term_parser_feed() (%d): %m", type);
723 r = terminal_push_tmp(t, str[j]);
728 if (type == TERM_SEQ_NONE) {
729 /* We only intercept one-char sequences, so in
730 * case term_parser_feed() couldn't parse a
731 * sequence, it is waiting for more data. We
732 * know it can never be a one-char sequence
733 * then, so we can safely forward the data.
734 * This avoids withholding ESC or other values
735 * that may be one-shot depending on the
737 r = terminal_write_tmp(t);
740 } else if (t->is_menu) {
741 r = terminal_menu(t, seq);
744 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
745 terminal_discard_tmp(t);
749 r = terminal_write_tmp(t);
759 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
760 Terminal *t = userdata;
765 sd_event_exit(t->event, 0);
768 r = term_screen_feed_text(t->screen, ptr, size);
770 return log_error_errno(r, "error: term_screen_feed_text() (%d): %m", r);
779 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
780 Terminal *t = userdata;
786 r = ring_push(&t->out_ring, buf, size);
793 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
797 static Terminal *terminal_free(Terminal *t) {
801 ring_clear(&t->out_ring);
802 term_screen_unref(t->screen);
803 term_parser_free(t->parser);
804 output_free(t->output);
805 sd_event_source_unref(t->frame_timer);
806 sd_event_unref(t->event);
807 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
808 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
814 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
815 struct termios in_attr, out_attr;
819 assert_return(out, -EINVAL);
821 r = tcgetattr(in_fd, &in_attr);
823 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
825 r = tcgetattr(out_fd, &out_attr);
827 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
829 t = new0(Terminal, 1);
835 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
836 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
839 cfmakeraw(&out_attr);
841 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
843 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
847 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
849 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
853 r = sd_event_default(&t->event);
855 log_error_errno(r, "error: sd_event_default() (%d): %m", r);
859 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
861 log_error_errno(r, "error: sigprocmask_many() (%d): %m", r);
865 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
867 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
871 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
873 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
877 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
879 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
883 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
885 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
889 /* force initial redraw on event-loop enter */
891 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
893 log_error_errno(r, "error: sd_event_add_time() (%d): %m", r);
897 r = output_new(&t->output, out_fd);
901 r = term_parser_new(&t->parser, true);
905 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
909 r = term_screen_set_answerback(t->screen, "systemd-subterm");
913 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
915 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
919 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
931 static int terminal_run(Terminal *t) {
934 assert_return(t, -EINVAL);
936 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
938 return log_error_errno(pid, "error: cannot fork PTY (%d): %m", pid);
942 char **argv = (char*[]){
943 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
947 setenv("TERM", "xterm-256color", 1);
948 setenv("COLORTERM", "systemd-subterm", 1);
950 execve(argv[0], argv, environ);
951 log_error_errno(errno, "error: cannot exec %s (%d): %m", argv[0], -errno);
957 return sd_event_loop(t->event);
964 int main(int argc, char *argv[]) {
968 r = terminal_new(&t, 0, 1);
978 log_error_errno(r, "error: terminal failed (%d): %m", r);