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"
45 typedef struct Output Output;
46 typedef struct Terminal Terminal;
52 unsigned int in_width;
53 unsigned int in_height;
54 unsigned int cursor_x;
55 unsigned int cursor_y;
66 sd_event_source *frame_timer;
74 struct termios saved_in_attr;
75 struct termios saved_out_attr;
80 bool is_scheduled : 1;
89 #define BORDER_HORIZ "\xe2\x94\x81"
90 #define BORDER_VERT "\xe2\x94\x83"
91 #define BORDER_VERT_RIGHT "\xe2\x94\xa3"
92 #define BORDER_VERT_LEFT "\xe2\x94\xab"
93 #define BORDER_DOWN_RIGHT "\xe2\x94\x8f"
94 #define BORDER_DOWN_LEFT "\xe2\x94\x93"
95 #define BORDER_UP_RIGHT "\xe2\x94\x97"
96 #define BORDER_UP_LEFT "\xe2\x94\x9b"
98 static int output_winch(Output *o) {
99 struct winsize wsz = { };
102 assert_return(o, -EINVAL);
104 r = ioctl(o->fd, TIOCGWINSZ, &wsz);
106 return log_error_errno(errno, "error: cannot read window-size: %m");
108 if (wsz.ws_col != o->width || wsz.ws_row != o->height) {
109 o->width = wsz.ws_col;
110 o->height = wsz.ws_row;
111 o->in_width = MAX(o->width, 2U) - 2;
112 o->in_height = MAX(o->height, 6U) - 6;
119 static int output_flush(Output *o) {
125 len = loop_write(o->fd, o->obuf, o->n_obuf, false);
127 return log_error_errno(len, "error: cannot write to TTY (%zd): %m", len);
134 static int output_write(Output *o, const void *buf, size_t size) {
138 assert_return(o, -EINVAL);
139 assert_return(buf || size < 1, -EINVAL);
144 if (o->n_obuf + size > o->n_obuf && o->n_obuf + size <= sizeof(o->obuf)) {
145 memcpy(o->obuf + o->n_obuf, buf, size);
154 len = loop_write(o->fd, buf, size, false);
156 return log_error_errno(len, "error: cannot write to TTY (%zd): %m", len);
162 static int output_vnprintf(Output *o, size_t max, const char *format, va_list args) {
166 assert_return(o, -EINVAL);
167 assert_return(format, -EINVAL);
168 assert_return(max <= sizeof(buf), -EINVAL);
170 r = vsnprintf(buf, max, format, args);
171 if (r > (ssize_t)max)
174 return output_write(o, buf, r);
178 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
182 va_start(args, format);
183 r = output_vnprintf(o, max, format, args);
190 static int output_vprintf(Output *o, const char *format, va_list args) {
194 assert_return(o, -EINVAL);
195 assert_return(format, -EINVAL);
197 r = vsnprintf(buf, sizeof(buf), format, args);
199 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
201 return output_write(o, buf, r);
205 static int output_printf(Output *o, const char *format, ...) {
209 va_start(args, format);
210 r = output_vprintf(o, format, args);
216 static int output_move_to(Output *o, unsigned int x, unsigned int y) {
219 assert_return(o, -EINVAL);
221 /* force the \e[H code as o->cursor_x/y might be out-of-date */
223 r = output_printf(o, "\e[%u;%uH", y + 1, x + 1);
232 static int output_print_line(Output *o, size_t len) {
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 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
240 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ;
241 const size_t max = (sizeof(line) - 1) / (sizeof(BORDER_HORIZ) - 1);
245 assert_return(o, -EINVAL);
247 for ( ; len > 0; len -= i) {
248 i = (len > max) ? max : len;
249 r = output_write(o, line, i * (sizeof(BORDER_HORIZ) - 1));
258 static int output_frame_printl(Output *o, const char *format, ...) {
266 if (o->cursor_y < 3 || o->cursor_y >= o->height - 3)
269 va_start(args, format);
270 r = output_vnprintf(o, o->width - 2, format, args);
276 return output_move_to(o, 1, o->cursor_y + 1);
279 static Output *output_free(Output *o) {
283 /* re-enable cursor */
284 output_printf(o, "\e[?25h");
285 /* disable alternate screen buffer */
286 output_printf(o, "\e[?1049l");
289 /* o->fd is owned by the caller */
295 static int output_new(Output **out, int fd) {
299 assert_return(out, -EINVAL);
311 /* enable alternate screen buffer */
312 r = output_printf(o, "\e[?1049h");
316 /* always hide cursor */
317 r = output_printf(o, "\e[?25l");
333 static void output_draw_frame(Output *o) {
338 /* print header-frame */
340 output_printf(o, BORDER_DOWN_RIGHT);
341 output_print_line(o, o->width - 2);
342 output_printf(o, BORDER_DOWN_LEFT
345 "\e[2;%uH" /* cursor-position: 2/x */
350 output_print_line(o, o->width - 2);
351 output_printf(o, BORDER_VERT_LEFT
354 /* print body-frame */
356 for (i = 0; i < o->in_height; ++i) {
357 output_printf(o, BORDER_VERT
358 "\e[%u;%uH" /* cursor-position: 2/x */
364 /* print footer-frame */
366 output_printf(o, BORDER_VERT_RIGHT);
367 output_print_line(o, o->width - 2);
368 output_printf(o, BORDER_VERT_LEFT
371 "\e[%u;%uH" /* cursor-position: 2/x */
375 o->height - 1, o->width);
376 output_print_line(o, o->width - 2);
377 output_printf(o, BORDER_UP_LEFT);
379 /* print header/footer text */
381 output_printf(o, "\e[2;3H");
382 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
383 output_printf(o, "\e[%u;3H", o->height - 1);
384 output_nprintf(o, o->width - 4, "press ^C to enter menu");
387 static void output_draw_menu(Output *o) {
390 output_frame_printl(o, "%s", "");
391 output_frame_printl(o, " Menu: (the following keys are recognized)");
392 output_frame_printl(o, " q: quit");
393 output_frame_printl(o, " ^C: send ^C to the PTY");
396 static int output_draw_cell_fn(term_screen *screen,
400 const term_attr *attr,
403 unsigned int ch_width) {
404 Output *o = userdata;
408 if (x >= o->in_width || y >= o->in_height)
411 if (x == 0 && y != 0)
412 output_printf(o, "\e[m\r\n" BORDER_VERT);
414 switch (attr->fg.ccode) {
415 case TERM_CCODE_DEFAULT:
416 output_printf(o, "\e[39m");
419 output_printf(o, "\e[38;5;%um", attr->fg.c256);
422 output_printf(o, "\e[38;2;%u;%u;%um", attr->fg.red, attr->fg.green, attr->fg.blue);
424 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
425 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 30);
427 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
428 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
432 switch (attr->bg.ccode) {
433 case TERM_CCODE_DEFAULT:
434 output_printf(o, "\e[49m");
437 output_printf(o, "\e[48;5;%um", attr->bg.c256);
440 output_printf(o, "\e[48;2;%u;%u;%um", attr->bg.red, attr->bg.green, attr->bg.blue);
442 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
443 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_BLACK + 40);
445 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
446 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
450 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
452 attr->italic ? 3 : 23,
453 attr->underline ? 4 : 24,
454 attr->inverse ? 7 : 27,
455 attr->blink ? 5 : 25,
456 attr->hidden ? 8 : 28);
459 output_printf(o, " ");
461 for (k = 0; k < n_ch; ++k) {
462 ulen = term_utf8_encode(utf8, ch[k]);
463 output_write(o, utf8, ulen);
470 static void output_draw_screen(Output *o, term_screen *s) {
474 term_screen_draw(s, output_draw_cell_fn, o, NULL);
476 output_printf(o, "\e[m");
479 static void output_draw(Output *o, bool menu, term_screen *screen) {
483 * This renders the contenst of the terminal. The layout contains a
484 * header, the main body and a footer. Around all areas we draw a
485 * border. It looks something like this:
487 * +----------------------------------------------------+
489 * +----------------------------------------------------+
498 * +----------------------------------------------------+
500 * +----------------------------------------------------+
502 * The body is the part that grows vertically.
504 * We need at least 6 vertical lines to render the screen. This would
505 * leave 0 lines for the body. Therefore, we require 7 lines so there's
506 * at least one body line. Similarly, we need 2 horizontal cells for the
507 * frame, so we require 3.
508 * If the window is too small, we print an error message instead.
511 if (o->in_width < 1 || o->in_height < 1) {
512 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
513 "\e[H"); /* cursor-position: home */
514 output_printf(o, "error: screen too small, need at least 3x7 cells");
520 output_printf(o, "\e[?25l");
522 /* frame-content is contant; only resizes can change it */
523 if (o->resized || o->in_menu != menu) {
524 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
525 "\e[H"); /* cursor-position: home */
526 output_draw_frame(o);
531 /* move cursor to child's position */
532 output_move_to(o, 1, 3);
537 output_draw_screen(o, screen);
540 * Hack: sd-term was not written to support TTY as output-objects, thus
541 * expects callers to use term_screen_feed_keyboard(). However, we
542 * forward TTY input directly. Hence, we're not notified about keypad
543 * changes. Update the related modes djring redraw to keep them at least
546 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
547 output_printf(o, "\e[?1h");
549 output_printf(o, "\e[?1l");
551 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
552 output_printf(o, "\e=");
554 output_printf(o, "\e>");
563 static void terminal_dirty(Terminal *t) {
569 if (t->is_scheduled) {
575 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
578 usec += 16 * USEC_PER_MSEC;
579 r = sd_event_source_set_time(t->frame_timer, usec);
581 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
583 t->is_scheduled = true;
587 output_draw(t->output, t->is_menu, t->screen);
590 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
591 Terminal *t = userdata;
593 t->is_scheduled = false;
600 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
601 Terminal *t = userdata;
604 output_winch(t->output);
607 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
609 log_error_errno(r, "error: pty_resize() (%d): %m", r);
612 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
614 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
621 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
628 len = term_utf8_encode(buf, ucs4);
632 r = ring_push(&t->out_ring, buf, len);
639 static int terminal_write_tmp(Terminal *t) {
646 num = ring_peek(&t->out_ring, vec);
651 for (i = 0; i < num; ++i) {
652 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
654 return log_error_errno(r, "error: cannot write to PTY (%d): %m", r);
658 ring_flush(&t->out_ring);
662 static void terminal_discard_tmp(Terminal *t) {
665 ring_flush(&t->out_ring);
668 static int terminal_menu(Terminal *t, const term_seq *seq) {
670 case TERM_SEQ_IGNORE:
672 case TERM_SEQ_GRAPHIC:
673 switch (seq->terminator) {
675 sd_event_exit(t->event, 0);
680 case TERM_SEQ_CONTROL:
681 switch (seq->terminator) {
683 terminal_push_tmp(t, 0x03);
684 terminal_write_tmp(t);
697 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
698 Terminal *t = userdata;
703 len = read(fd, buf, sizeof(buf));
705 if (errno == EAGAIN || errno == EINTR)
708 log_error_errno(errno, "error: cannot read from TTY (%d): %m", -errno);
712 for (i = 0; i < len; ++i) {
717 n_str = term_utf8_decode(&t->utf8, &str, buf[i]);
718 for (j = 0; j < n_str; ++j) {
719 type = term_parser_feed(t->parser, &seq, str[j]);
721 return log_error_errno(type, "error: term_parser_feed() (%d): %m", type);
724 r = terminal_push_tmp(t, str[j]);
729 if (type == TERM_SEQ_NONE) {
730 /* We only intercept one-char sequences, so in
731 * case term_parser_feed() couldn't parse a
732 * sequence, it is waiting for more data. We
733 * know it can never be a one-char sequence
734 * then, so we can safely forward the data.
735 * This avoids withholding ESC or other values
736 * that may be one-shot depending on the
738 r = terminal_write_tmp(t);
741 } else if (t->is_menu) {
742 r = terminal_menu(t, seq);
745 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
746 terminal_discard_tmp(t);
750 r = terminal_write_tmp(t);
760 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
761 Terminal *t = userdata;
766 sd_event_exit(t->event, 0);
769 r = term_screen_feed_text(t->screen, ptr, size);
771 return log_error_errno(r, "error: term_screen_feed_text() (%d): %m", r);
780 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
781 Terminal *t = userdata;
787 r = ring_push(&t->out_ring, buf, size);
794 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
798 static Terminal *terminal_free(Terminal *t) {
802 ring_clear(&t->out_ring);
803 term_screen_unref(t->screen);
804 term_parser_free(t->parser);
805 output_free(t->output);
806 sd_event_source_unref(t->frame_timer);
807 sd_event_unref(t->event);
808 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
809 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
815 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
816 struct termios in_attr, out_attr;
820 assert_return(out, -EINVAL);
822 r = tcgetattr(in_fd, &in_attr);
824 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
826 r = tcgetattr(out_fd, &out_attr);
828 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
830 t = new0(Terminal, 1);
836 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
837 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
840 cfmakeraw(&out_attr);
842 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
844 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
848 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
850 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
854 r = sd_event_default(&t->event);
856 log_error_errno(r, "error: sd_event_default() (%d): %m", r);
860 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
862 log_error_errno(r, "error: sigprocmask_many() (%d): %m", r);
866 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
868 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
872 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
874 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
878 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
880 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
884 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
886 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
890 /* force initial redraw on event-loop enter */
892 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
894 log_error_errno(r, "error: sd_event_add_time() (%d): %m", r);
898 r = output_new(&t->output, out_fd);
902 r = term_parser_new(&t->parser, true);
906 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
910 r = term_screen_set_answerback(t->screen, "systemd-subterm");
914 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
916 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
920 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
932 static int terminal_run(Terminal *t) {
935 assert_return(t, -EINVAL);
937 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
939 return log_error_errno(pid, "error: cannot fork PTY (%d): %m", pid);
943 char **argv = (char*[]){
944 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
948 setenv("TERM", "xterm-256color", 1);
949 setenv("COLORTERM", "systemd-subterm", 1);
951 execve(argv[0], argv, environ);
952 log_error_errno(errno, "error: cannot exec %s (%d): %m", argv[0], -errno);
958 return sd_event_loop(t->event);
965 int main(int argc, char *argv[]) {
969 r = terminal_new(&t, 0, 1);
979 log_error_errno(r, "error: terminal failed (%d): %m", r);