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.
35 #include <sys/ioctl.h>
41 #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 r = loop_write(o->fd, o->obuf, o->n_obuf, false);
127 return log_error_errno(r, "error: cannot write to TTY: %m");
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 <= 4096, -EINVAL);
170 r = MIN(vsnprintf(buf, max, format, args), (int) max);
172 return output_write(o, buf, r);
176 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
180 va_start(args, format);
181 r = output_vnprintf(o, max, format, args);
188 static int output_vprintf(Output *o, const char *format, va_list args) {
192 assert_return(o, -EINVAL);
193 assert_return(format, -EINVAL);
195 r = vsnprintf(buf, sizeof(buf), format, args);
197 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
199 return output_write(o, buf, r);
203 static int output_printf(Output *o, const char *format, ...) {
207 va_start(args, format);
208 r = output_vprintf(o, format, args);
214 static int output_move_to(Output *o, unsigned int x, unsigned int y) {
217 assert_return(o, -EINVAL);
219 /* force the \e[H code as o->cursor_x/y might be out-of-date */
221 r = output_printf(o, "\e[%u;%uH", y + 1, x + 1);
230 static int output_print_line(Output *o, size_t len) {
232 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
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;
239 const size_t max = (sizeof(line) - 1) / (sizeof(BORDER_HORIZ) - 1);
243 assert_return(o, -EINVAL);
245 for ( ; len > 0; len -= i) {
246 i = (len > max) ? max : len;
247 r = output_write(o, line, i * (sizeof(BORDER_HORIZ) - 1));
256 static int output_frame_printl(Output *o, const char *format, ...) {
264 if (o->cursor_y < 3 || o->cursor_y >= o->height - 3)
267 va_start(args, format);
268 r = output_vnprintf(o, o->width - 2, format, args);
274 return output_move_to(o, 1, o->cursor_y + 1);
277 static Output *output_free(Output *o) {
281 /* re-enable cursor */
282 output_printf(o, "\e[?25h");
283 /* disable alternate screen buffer */
284 output_printf(o, "\e[?1049l");
287 /* o->fd is owned by the caller */
293 static int output_new(Output **out, int fd) {
297 assert_return(out, -EINVAL);
309 /* enable alternate screen buffer */
310 r = output_printf(o, "\e[?1049h");
314 /* always hide cursor */
315 r = output_printf(o, "\e[?25l");
331 static void output_draw_frame(Output *o) {
336 /* print header-frame */
338 output_printf(o, BORDER_DOWN_RIGHT);
339 output_print_line(o, o->width - 2);
340 output_printf(o, BORDER_DOWN_LEFT
343 "\e[2;%uH" /* cursor-position: 2/x */
348 output_print_line(o, o->width - 2);
349 output_printf(o, BORDER_VERT_LEFT
352 /* print body-frame */
354 for (i = 0; i < o->in_height; ++i) {
355 output_printf(o, BORDER_VERT
356 "\e[%u;%uH" /* cursor-position: 2/x */
362 /* print footer-frame */
364 output_printf(o, BORDER_VERT_RIGHT);
365 output_print_line(o, o->width - 2);
366 output_printf(o, BORDER_VERT_LEFT
369 "\e[%u;%uH" /* cursor-position: 2/x */
373 o->height - 1, o->width);
374 output_print_line(o, o->width - 2);
375 output_printf(o, BORDER_UP_LEFT);
377 /* print header/footer text */
379 output_printf(o, "\e[2;3H");
380 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
381 output_printf(o, "\e[%u;3H", o->height - 1);
382 output_nprintf(o, o->width - 4, "press ^C to enter menu");
385 static void output_draw_menu(Output *o) {
388 output_frame_printl(o, "%s", "");
389 output_frame_printl(o, " Menu: (the following keys are recognized)");
390 output_frame_printl(o, " q: quit");
391 output_frame_printl(o, " ^C: send ^C to the PTY");
394 static int output_draw_cell_fn(term_screen *screen,
398 const term_attr *attr,
401 unsigned int ch_width) {
402 Output *o = userdata;
406 if (x >= o->in_width || y >= o->in_height)
409 if (x == 0 && y != 0)
410 output_printf(o, "\e[m\r\n" BORDER_VERT);
412 switch (attr->fg.ccode) {
413 case TERM_CCODE_DEFAULT:
414 output_printf(o, "\e[39m");
417 output_printf(o, "\e[38;5;%um", attr->fg.c256);
420 output_printf(o, "\e[38;2;%u;%u;%um", attr->fg.red, attr->fg.green, attr->fg.blue);
422 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
423 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 30);
425 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
426 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
430 switch (attr->bg.ccode) {
431 case TERM_CCODE_DEFAULT:
432 output_printf(o, "\e[49m");
435 output_printf(o, "\e[48;5;%um", attr->bg.c256);
438 output_printf(o, "\e[48;2;%u;%u;%um", attr->bg.red, attr->bg.green, attr->bg.blue);
440 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
441 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_BLACK + 40);
443 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
444 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
448 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
450 attr->italic ? 3 : 23,
451 attr->underline ? 4 : 24,
452 attr->inverse ? 7 : 27,
453 attr->blink ? 5 : 25,
454 attr->hidden ? 8 : 28);
457 output_printf(o, " ");
459 for (k = 0; k < n_ch; ++k) {
460 ulen = utf8_encode_unichar(utf8, ch[k]);
461 output_write(o, utf8, ulen);
468 static void output_draw_screen(Output *o, term_screen *s) {
472 term_screen_draw(s, output_draw_cell_fn, o, NULL);
474 output_printf(o, "\e[m");
477 static void output_draw(Output *o, bool menu, term_screen *screen) {
481 * This renders the contenst of the terminal. The layout contains a
482 * header, the main body and a footer. Around all areas we draw a
483 * border. It looks something like this:
485 * +----------------------------------------------------+
487 * +----------------------------------------------------+
496 * +----------------------------------------------------+
498 * +----------------------------------------------------+
500 * The body is the part that grows vertically.
502 * We need at least 6 vertical lines to render the screen. This would
503 * leave 0 lines for the body. Therefore, we require 7 lines so there's
504 * at least one body line. Similarly, we need 2 horizontal cells for the
505 * frame, so we require 3.
506 * If the window is too small, we print an error message instead.
509 if (o->in_width < 1 || o->in_height < 1) {
510 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
511 "\e[H"); /* cursor-position: home */
512 output_printf(o, "error: screen too small, need at least 3x7 cells");
518 output_printf(o, "\e[?25l");
520 /* frame-content is contant; only resizes can change it */
521 if (o->resized || o->in_menu != menu) {
522 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
523 "\e[H"); /* cursor-position: home */
524 output_draw_frame(o);
529 /* move cursor to child's position */
530 output_move_to(o, 1, 3);
535 output_draw_screen(o, screen);
538 * Hack: sd-term was not written to support TTY as output-objects, thus
539 * expects callers to use term_screen_feed_keyboard(). However, we
540 * forward TTY input directly. Hence, we're not notified about keypad
541 * changes. Update the related modes djring redraw to keep them at least
544 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
545 output_printf(o, "\e[?1h");
547 output_printf(o, "\e[?1l");
549 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
550 output_printf(o, "\e=");
552 output_printf(o, "\e>");
561 static void terminal_dirty(Terminal *t) {
567 if (t->is_scheduled) {
573 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
576 usec += 16 * USEC_PER_MSEC;
577 r = sd_event_source_set_time(t->frame_timer, usec);
579 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
581 t->is_scheduled = true;
585 output_draw(t->output, t->is_menu, t->screen);
588 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
589 Terminal *t = userdata;
591 t->is_scheduled = false;
598 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
599 Terminal *t = userdata;
602 output_winch(t->output);
605 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
607 log_error_errno(r, "error: pty_resize() (%d): %m", r);
610 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
612 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
619 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
626 len = utf8_encode_unichar(buf, ucs4);
630 r = ring_push(&t->out_ring, buf, len);
637 static int terminal_write_tmp(Terminal *t) {
644 num = ring_peek(&t->out_ring, vec);
649 for (i = 0; i < num; ++i) {
650 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
652 return log_error_errno(r, "error: cannot write to PTY (%d): %m", r);
656 ring_flush(&t->out_ring);
660 static void terminal_discard_tmp(Terminal *t) {
663 ring_flush(&t->out_ring);
666 static int terminal_menu(Terminal *t, const term_seq *seq) {
668 case TERM_SEQ_IGNORE:
670 case TERM_SEQ_GRAPHIC:
671 switch (seq->terminator) {
673 sd_event_exit(t->event, 0);
678 case TERM_SEQ_CONTROL:
679 switch (seq->terminator) {
681 terminal_push_tmp(t, 0x03);
682 terminal_write_tmp(t);
695 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
696 Terminal *t = userdata;
701 len = read(fd, buf, sizeof(buf));
703 if (errno == EAGAIN || errno == EINTR)
706 log_error_errno(errno, "error: cannot read from TTY (%d): %m", -errno);
710 for (i = 0; i < len; ++i) {
715 n_str = term_utf8_decode(&t->utf8, &str, buf[i]);
716 for (j = 0; j < n_str; ++j) {
717 type = term_parser_feed(t->parser, &seq, str[j]);
719 return log_error_errno(type, "error: term_parser_feed() (%d): %m", type);
722 r = terminal_push_tmp(t, str[j]);
727 if (type == TERM_SEQ_NONE) {
728 /* We only intercept one-char sequences, so in
729 * case term_parser_feed() couldn't parse a
730 * sequence, it is waiting for more data. We
731 * know it can never be a one-char sequence
732 * then, so we can safely forward the data.
733 * This avoids withholding ESC or other values
734 * that may be one-shot depending on the
736 r = terminal_write_tmp(t);
739 } else if (t->is_menu) {
740 r = terminal_menu(t, seq);
743 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
744 terminal_discard_tmp(t);
748 r = terminal_write_tmp(t);
758 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
759 Terminal *t = userdata;
764 sd_event_exit(t->event, 0);
767 r = term_screen_feed_text(t->screen, ptr, size);
769 return log_error_errno(r, "error: term_screen_feed_text() (%d): %m", r);
778 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
779 Terminal *t = userdata;
785 r = ring_push(&t->out_ring, buf, size);
792 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
796 static Terminal *terminal_free(Terminal *t) {
800 ring_clear(&t->out_ring);
801 term_screen_unref(t->screen);
802 term_parser_free(t->parser);
803 output_free(t->output);
804 sd_event_source_unref(t->frame_timer);
805 sd_event_unref(t->event);
806 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
807 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
813 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
814 struct termios in_attr, out_attr;
818 assert_return(out, -EINVAL);
820 r = tcgetattr(in_fd, &in_attr);
822 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
824 r = tcgetattr(out_fd, &out_attr);
826 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
828 t = new0(Terminal, 1);
834 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
835 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
838 cfmakeraw(&out_attr);
840 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
842 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
846 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
848 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
852 r = sd_event_default(&t->event);
854 log_error_errno(r, "error: sd_event_default() (%d): %m", r);
858 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
860 log_error_errno(r, "error: sigprocmask_many() (%d): %m", r);
864 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
866 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
870 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
872 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
876 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
878 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
882 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
884 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
888 /* force initial redraw on event-loop enter */
890 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
892 log_error_errno(r, "error: sd_event_add_time() (%d): %m", r);
896 r = output_new(&t->output, out_fd);
900 r = term_parser_new(&t->parser, true);
904 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
908 r = term_screen_set_answerback(t->screen, "systemd-subterm");
912 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
914 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
918 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
930 static int terminal_run(Terminal *t) {
933 assert_return(t, -EINVAL);
935 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
937 return log_error_errno(pid, "error: cannot fork PTY (%d): %m", pid);
941 char **argv = (char*[]){
942 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
946 setenv("TERM", "xterm-256color", 1);
947 setenv("COLORTERM", "systemd-subterm", 1);
949 execve(argv[0], argv, environ);
950 log_error_errno(errno, "error: cannot exec %s (%d): %m", argv[0], -errno);
956 return sd_event_loop(t->event);
963 int main(int argc, char *argv[]) {
967 r = terminal_new(&t, 0, 1);
977 log_error_errno(r, "error: terminal failed (%d): %m", r);