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 log_error("error: cannot read window-size: %m");
110 if (wsz.ws_col != o->width || wsz.ws_row != o->height) {
111 o->width = wsz.ws_col;
112 o->height = wsz.ws_row;
113 o->in_width = MAX(o->width, 2U) - 2;
114 o->in_height = MAX(o->height, 6U) - 6;
121 static int output_flush(Output *o) {
127 len = loop_write(o->fd, o->obuf, o->n_obuf, false);
129 log_error("error: cannot write to TTY (%zd): %s", len, strerror(-len));
138 static int output_write(Output *o, const void *buf, size_t size) {
142 assert_return(o, -EINVAL);
143 assert_return(buf || size < 1, -EINVAL);
148 if (o->n_obuf + size > o->n_obuf && o->n_obuf + size <= sizeof(o->obuf)) {
149 memcpy(o->obuf + o->n_obuf, buf, size);
158 len = loop_write(o->fd, buf, size, false);
160 log_error("error: cannot write to TTY (%zd): %s", len, strerror(-len));
167 static int output_vnprintf(Output *o, size_t max, const char *format, va_list args) {
171 assert_return(o, -EINVAL);
172 assert_return(format, -EINVAL);
173 assert_return(max <= sizeof(buf), -EINVAL);
175 r = vsnprintf(buf, max, format, args);
176 if (r > (ssize_t)max)
179 return output_write(o, buf, r);
182 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
186 va_start(args, format);
187 r = output_vnprintf(o, max, format, args);
193 static int output_vprintf(Output *o, const char *format, va_list args) {
197 assert_return(o, -EINVAL);
198 assert_return(format, -EINVAL);
200 r = vsnprintf(buf, sizeof(buf), format, args);
202 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
204 return output_write(o, buf, r);
207 static int output_printf(Output *o, const char *format, ...) {
211 va_start(args, format);
212 r = output_vprintf(o, format, args);
218 static int output_move_to(Output *o, unsigned int x, unsigned int y) {
221 assert_return(o, -EINVAL);
223 /* force the \e[H code as o->cursor_x/y might be out-of-date */
225 r = output_printf(o, "\e[%u;%uH", y + 1, x + 1);
234 static int output_print_line(Output *o, size_t len) {
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 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
241 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
242 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ;
243 const size_t max = (sizeof(line) - 1) / (sizeof(BORDER_HORIZ) - 1);
247 assert_return(o, -EINVAL);
249 for ( ; len > 0; len -= i) {
250 i = (len > max) ? max : len;
251 r = output_write(o, line, i * (sizeof(BORDER_HORIZ) - 1));
259 static int output_frame_printl(Output *o, const char *format, ...) {
267 if (o->cursor_y < 3 || o->cursor_y >= o->height - 3)
270 va_start(args, format);
271 r = output_vnprintf(o, o->width - 2, format, args);
277 return output_move_to(o, 1, o->cursor_y + 1);
280 static Output *output_free(Output *o) {
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");
327 static void output_draw_frame(Output *o) {
332 /* print header-frame */
334 output_printf(o, BORDER_DOWN_RIGHT);
335 output_print_line(o, o->width - 2);
336 output_printf(o, BORDER_DOWN_LEFT
339 "\e[2;%uH" /* cursor-position: 2/x */
344 output_print_line(o, o->width - 2);
345 output_printf(o, BORDER_VERT_LEFT
348 /* print body-frame */
350 for (i = 0; i < o->in_height; ++i) {
351 output_printf(o, BORDER_VERT
352 "\e[%u;%uH" /* cursor-position: 2/x */
358 /* print footer-frame */
360 output_printf(o, BORDER_VERT_RIGHT);
361 output_print_line(o, o->width - 2);
362 output_printf(o, BORDER_VERT_LEFT
365 "\e[%u;%uH" /* cursor-position: 2/x */
369 o->height - 1, o->width);
370 output_print_line(o, o->width - 2);
371 output_printf(o, BORDER_UP_LEFT);
373 /* print header/footer text */
375 output_printf(o, "\e[2;3H");
376 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
377 output_printf(o, "\e[%u;3H", o->height - 1);
378 output_nprintf(o, o->width - 4, "press ^C to enter menu");
381 static void output_draw_menu(Output *o) {
384 output_frame_printl(o, "");
385 output_frame_printl(o, " Menu: (the following keys are recognized)");
386 output_frame_printl(o, " q: quit");
387 output_frame_printl(o, " ^C: send ^C to the PTY");
390 static void output_draw_screen(Output *o, term_screen *s) {
397 for (j = 0; j < s->page->height && j < o->in_height; ++j) {
399 output_printf(o, "\e[m\r\n" BORDER_VERT);
402 for (i = 0; i < s->page->width && i < o->in_width; ++i) {
404 term_cell *cell = &s->page->lines[j]->cells[i];
409 switch (cell->attr.fg.ccode) {
410 case TERM_CCODE_DEFAULT:
411 output_printf(o, "\e[39m");
414 output_printf(o, "\e[38;5;%um", cell->attr.fg.c256);
417 output_printf(o, "\e[38;2;%u;%u;%um", cell->attr.fg.red, cell->attr.fg.green, cell->attr.fg.blue);
419 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
421 output_printf(o, "\e[%um", cell->attr.fg.ccode - TERM_CCODE_BLACK + 90);
423 output_printf(o, "\e[%um", cell->attr.fg.ccode - TERM_CCODE_BLACK + 30);
425 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
426 output_printf(o, "\e[%um", cell->attr.fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
430 switch (cell->attr.bg.ccode) {
431 case TERM_CCODE_DEFAULT:
432 output_printf(o, "\e[49m");
435 output_printf(o, "\e[48;5;%um", cell->attr.bg.c256);
438 output_printf(o, "\e[48;2;%u;%u;%um", cell->attr.bg.red, cell->attr.bg.green, cell->attr.bg.blue);
440 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
441 output_printf(o, "\e[%um", cell->attr.bg.ccode - TERM_CCODE_BLACK + 40);
443 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
444 output_printf(o, "\e[%um", cell->attr.bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
448 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
449 cell->attr.bold ? 1 : 22,
450 cell->attr.italic ? 3 : 23,
451 cell->attr.underline ? 4 : 24,
452 cell->attr.inverse ? 7 : 27,
453 cell->attr.blink ? 5 : 25,
454 cell->attr.hidden ? 8 : 28);
456 str = term_char_resolve(cell->ch, &len, &buf);
459 output_printf(o, " ");
461 for (k = 0; k < len; ++k) {
462 ulen = term_utf8_encode(utf8, str[k]);
463 output_write(o, utf8, ulen);
469 output_move_to(o, s->cursor_x + 1, s->cursor_y + 3);
470 output_printf(o, "\e[m");
473 static void output_draw(Output *o, bool menu, term_screen *screen) {
477 * This renders the contenst of the terminal. The layout contains a
478 * header, the main body and a footer. Around all areas we draw a
479 * border. It looks something like this:
481 * +----------------------------------------------------+
483 * +----------------------------------------------------+
492 * +----------------------------------------------------+
494 * +----------------------------------------------------+
496 * The body is the part that grows vertically.
498 * We need at least 6 vertical lines to render the screen. This would
499 * leave 0 lines for the body. Therefore, we require 7 lines so there's
500 * at least one body line. Similarly, we need 2 horizontal cells for the
501 * frame, so we require 3.
502 * If the window is too small, we print an error message instead.
505 if (o->in_width < 1 || o->in_height < 1) {
506 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
507 "\e[H"); /* cursor-position: home */
508 output_printf(o, "error: screen too small, need at least 3x7 cells");
514 output_printf(o, "\e[?25l");
516 /* frame-content is contant; only resizes can change it */
517 if (o->resized || o->in_menu != menu) {
518 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
519 "\e[H"); /* cursor-position: home */
520 output_draw_frame(o);
525 /* move cursor to child's position */
526 output_move_to(o, 1, 3);
531 output_draw_screen(o, screen);
534 if (!(screen->flags & TERM_FLAG_HIDE_CURSOR))
535 output_printf(o, "\e[?25h");
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("error: pty_resize() (%d): %s", r, strerror(-r));
610 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
612 log_error("error: term_screen_resize() (%d): %s", r, strerror(-r));
619 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
626 len = term_utf8_encode(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 log_error("error: cannot write to PTY (%d): %s", r, strerror(-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("error: cannot read from TTY (%d): %m", -errno);
712 for (i = 0; i < len; ++i) {
717 str = term_utf8_decode(&t->utf8, &n_str, buf[i]);
718 for (j = 0; j < n_str; ++j) {
719 type = term_parser_feed(t->parser, &seq, str[j]);
721 log_error("error: term_parser_feed() (%d): %s", type, strerror(-type));
726 r = terminal_push_tmp(t, str[j]);
731 if (type == TERM_SEQ_NONE) {
732 /* We only intercept one-char sequences, so in
733 * case term_parser_feed() couldn't parse a
734 * sequence, it is waiting for more data. We
735 * know it can never be a one-char sequence
736 * then, so we can safely forward the data.
737 * This avoids withholding ESC or other values
738 * that may be one-shot depending on the
740 r = terminal_write_tmp(t);
743 } else if (t->is_menu) {
744 r = terminal_menu(t, seq);
747 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
748 terminal_discard_tmp(t);
752 r = terminal_write_tmp(t);
762 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
763 Terminal *t = userdata;
768 sd_event_exit(t->event, 0);
771 r = term_screen_feed_text(t->screen, ptr, size);
773 log_error("error: term_screen_feed_text() (%d): %s", r, strerror(-r));
784 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
785 Terminal *t = userdata;
791 r = ring_push(&t->out_ring, buf, size);
798 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
802 static Terminal *terminal_free(Terminal *t) {
806 ring_clear(&t->out_ring);
807 term_screen_unref(t->screen);
808 term_parser_free(t->parser);
809 output_free(t->output);
810 sd_event_source_unref(t->frame_timer);
811 sd_event_unref(t->event);
812 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
813 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
819 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
820 struct termios in_attr, out_attr;
824 assert_return(out, -EINVAL);
826 r = tcgetattr(in_fd, &in_attr);
828 log_error("error: tcgetattr() (%d): %m", -errno);
832 r = tcgetattr(out_fd, &out_attr);
834 log_error("error: tcgetattr() (%d): %m", -errno);
838 t = new0(Terminal, 1);
844 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
845 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
848 cfmakeraw(&out_attr);
850 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
852 log_error("error: tcsetattr() (%d): %s", r, strerror(-r));
856 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
858 log_error("error: tcsetattr() (%d): %s", r, strerror(-r));
862 r = sd_event_default(&t->event);
864 log_error("error: sd_event_default() (%d): %s", r, strerror(-r));
868 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
870 log_error("error: sigprocmask_many() (%d): %s", r, strerror(-r));
874 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
876 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
880 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
882 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
886 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
888 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
892 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
894 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
898 /* force initial redraw on event-loop enter */
900 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
902 log_error("error: sd_event_add_time() (%d): %s", r, strerror(-r));
906 r = output_new(&t->output, out_fd);
910 r = term_parser_new(&t->parser, true);
914 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
918 r = term_screen_set_answerback(t->screen, "systemd-subterm");
922 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
924 log_error("error: term_screen_resize() (%d): %s", r, strerror(-r));
928 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
940 static int terminal_run(Terminal *t) {
943 assert_return(t, -EINVAL);
945 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
947 log_error("error: cannot fork PTY (%d): %s", pid, strerror(-pid));
949 } else if (pid == 0) {
952 char **argv = (char*[]){
953 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
957 setenv("TERM", "xterm-256color", 1);
958 setenv("COLORTERM", "systemd-subterm", 1);
960 execve(argv[0], argv, environ);
961 log_error("error: cannot exec %s (%d): %m", argv[0], -errno);
967 return sd_event_loop(t->event);
974 int main(int argc, char *argv[]) {
978 r = terminal_new(&t, 0, 1);
988 log_error("error: terminal failed (%d): %s", r, strerror(-r));