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));
168 static int output_vnprintf(Output *o, size_t max, const char *format, va_list args) {
172 assert_return(o, -EINVAL);
173 assert_return(format, -EINVAL);
174 assert_return(max <= sizeof(buf), -EINVAL);
176 r = vsnprintf(buf, max, format, args);
177 if (r > (ssize_t)max)
180 return output_write(o, buf, r);
184 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
188 va_start(args, format);
189 r = output_vnprintf(o, max, format, args);
196 static int output_vprintf(Output *o, const char *format, va_list args) {
200 assert_return(o, -EINVAL);
201 assert_return(format, -EINVAL);
203 r = vsnprintf(buf, sizeof(buf), format, args);
205 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
207 return output_write(o, buf, r);
211 static int output_printf(Output *o, const char *format, ...) {
215 va_start(args, format);
216 r = output_vprintf(o, format, args);
222 static int output_move_to(Output *o, unsigned int x, unsigned int y) {
225 assert_return(o, -EINVAL);
227 /* force the \e[H code as o->cursor_x/y might be out-of-date */
229 r = output_printf(o, "\e[%u;%uH", y + 1, x + 1);
238 static int output_print_line(Output *o, size_t len) {
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 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
243 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
244 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
245 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
246 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ;
247 const size_t max = (sizeof(line) - 1) / (sizeof(BORDER_HORIZ) - 1);
251 assert_return(o, -EINVAL);
253 for ( ; len > 0; len -= i) {
254 i = (len > max) ? max : len;
255 r = output_write(o, line, i * (sizeof(BORDER_HORIZ) - 1));
264 static int output_frame_printl(Output *o, const char *format, ...) {
272 if (o->cursor_y < 3 || o->cursor_y >= o->height - 3)
275 va_start(args, format);
276 r = output_vnprintf(o, o->width - 2, format, args);
282 return output_move_to(o, 1, o->cursor_y + 1);
285 static Output *output_free(Output *o) {
289 /* re-enable cursor */
290 output_printf(o, "\e[?25h");
291 /* disable alternate screen buffer */
292 output_printf(o, "\e[?1049l");
295 /* o->fd is owned by the caller */
301 static int output_new(Output **out, int fd) {
305 assert_return(out, -EINVAL);
317 /* enable alternate screen buffer */
318 r = output_printf(o, "\e[?1049h");
322 /* always hide cursor */
323 r = output_printf(o, "\e[?25l");
339 static void output_draw_frame(Output *o) {
344 /* print header-frame */
346 output_printf(o, BORDER_DOWN_RIGHT);
347 output_print_line(o, o->width - 2);
348 output_printf(o, BORDER_DOWN_LEFT
351 "\e[2;%uH" /* cursor-position: 2/x */
356 output_print_line(o, o->width - 2);
357 output_printf(o, BORDER_VERT_LEFT
360 /* print body-frame */
362 for (i = 0; i < o->in_height; ++i) {
363 output_printf(o, BORDER_VERT
364 "\e[%u;%uH" /* cursor-position: 2/x */
370 /* print footer-frame */
372 output_printf(o, BORDER_VERT_RIGHT);
373 output_print_line(o, o->width - 2);
374 output_printf(o, BORDER_VERT_LEFT
377 "\e[%u;%uH" /* cursor-position: 2/x */
381 o->height - 1, o->width);
382 output_print_line(o, o->width - 2);
383 output_printf(o, BORDER_UP_LEFT);
385 /* print header/footer text */
387 output_printf(o, "\e[2;3H");
388 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
389 output_printf(o, "\e[%u;3H", o->height - 1);
390 output_nprintf(o, o->width - 4, "press ^C to enter menu");
393 static void output_draw_menu(Output *o) {
396 output_frame_printl(o, "%s", "");
397 output_frame_printl(o, " Menu: (the following keys are recognized)");
398 output_frame_printl(o, " q: quit");
399 output_frame_printl(o, " ^C: send ^C to the PTY");
402 static int output_draw_cell_fn(term_screen *screen,
406 const term_attr *attr,
409 unsigned int ch_width) {
410 Output *o = userdata;
414 if (x >= o->in_width || y >= o->in_height)
417 if (x == 0 && y != 0)
418 output_printf(o, "\e[m\r\n" BORDER_VERT);
420 switch (attr->fg.ccode) {
421 case TERM_CCODE_DEFAULT:
422 output_printf(o, "\e[39m");
425 output_printf(o, "\e[38;5;%um", attr->fg.c256);
428 output_printf(o, "\e[38;2;%u;%u;%um", attr->fg.red, attr->fg.green, attr->fg.blue);
430 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
432 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 90);
434 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 30);
436 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
437 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
441 switch (attr->bg.ccode) {
442 case TERM_CCODE_DEFAULT:
443 output_printf(o, "\e[49m");
446 output_printf(o, "\e[48;5;%um", attr->bg.c256);
449 output_printf(o, "\e[48;2;%u;%u;%um", attr->bg.red, attr->bg.green, attr->bg.blue);
451 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
452 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_BLACK + 40);
454 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
455 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
459 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
461 attr->italic ? 3 : 23,
462 attr->underline ? 4 : 24,
463 attr->inverse ? 7 : 27,
464 attr->blink ? 5 : 25,
465 attr->hidden ? 8 : 28);
468 output_printf(o, " ");
470 for (k = 0; k < n_ch; ++k) {
471 ulen = term_utf8_encode(utf8, ch[k]);
472 output_write(o, utf8, ulen);
479 static void output_draw_screen(Output *o, term_screen *s) {
483 term_screen_draw(s, output_draw_cell_fn, o, NULL);
485 output_move_to(o, s->cursor_x + 1, s->cursor_y + 3);
486 output_printf(o, "\e[m");
489 static void output_draw(Output *o, bool menu, term_screen *screen) {
493 * This renders the contenst of the terminal. The layout contains a
494 * header, the main body and a footer. Around all areas we draw a
495 * border. It looks something like this:
497 * +----------------------------------------------------+
499 * +----------------------------------------------------+
508 * +----------------------------------------------------+
510 * +----------------------------------------------------+
512 * The body is the part that grows vertically.
514 * We need at least 6 vertical lines to render the screen. This would
515 * leave 0 lines for the body. Therefore, we require 7 lines so there's
516 * at least one body line. Similarly, we need 2 horizontal cells for the
517 * frame, so we require 3.
518 * If the window is too small, we print an error message instead.
521 if (o->in_width < 1 || o->in_height < 1) {
522 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
523 "\e[H"); /* cursor-position: home */
524 output_printf(o, "error: screen too small, need at least 3x7 cells");
530 output_printf(o, "\e[?25l");
532 /* frame-content is contant; only resizes can change it */
533 if (o->resized || o->in_menu != menu) {
534 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
535 "\e[H"); /* cursor-position: home */
536 output_draw_frame(o);
541 /* move cursor to child's position */
542 output_move_to(o, 1, 3);
547 output_draw_screen(o, screen);
550 * Hack: sd-term was not written to support TTY as output-objects, thus
551 * expects callers to use term_screen_feed_keyboard(). However, we
552 * forward TTY input directly. Hence, we're not notified about keypad
553 * changes. Update the related modes djring redraw to keep them at least
556 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
557 output_printf(o, "\e[?1h");
559 output_printf(o, "\e[?1l");
561 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
562 output_printf(o, "\e=");
564 output_printf(o, "\e>");
573 static void terminal_dirty(Terminal *t) {
579 if (t->is_scheduled) {
585 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
588 usec += 16 * USEC_PER_MSEC;
589 r = sd_event_source_set_time(t->frame_timer, usec);
591 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
593 t->is_scheduled = true;
597 output_draw(t->output, t->is_menu, t->screen);
600 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
601 Terminal *t = userdata;
603 t->is_scheduled = false;
610 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
611 Terminal *t = userdata;
614 output_winch(t->output);
617 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
619 log_error("error: pty_resize() (%d): %s", r, strerror(-r));
622 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
624 log_error("error: term_screen_resize() (%d): %s", r, strerror(-r));
631 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
638 len = term_utf8_encode(buf, ucs4);
642 r = ring_push(&t->out_ring, buf, len);
649 static int terminal_write_tmp(Terminal *t) {
656 num = ring_peek(&t->out_ring, vec);
661 for (i = 0; i < num; ++i) {
662 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
664 log_error("error: cannot write to PTY (%d): %s", r, strerror(-r));
670 ring_flush(&t->out_ring);
674 static void terminal_discard_tmp(Terminal *t) {
677 ring_flush(&t->out_ring);
680 static int terminal_menu(Terminal *t, const term_seq *seq) {
682 case TERM_SEQ_IGNORE:
684 case TERM_SEQ_GRAPHIC:
685 switch (seq->terminator) {
687 sd_event_exit(t->event, 0);
692 case TERM_SEQ_CONTROL:
693 switch (seq->terminator) {
695 terminal_push_tmp(t, 0x03);
696 terminal_write_tmp(t);
709 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
710 Terminal *t = userdata;
715 len = read(fd, buf, sizeof(buf));
717 if (errno == EAGAIN || errno == EINTR)
720 log_error("error: cannot read from TTY (%d): %m", -errno);
724 for (i = 0; i < len; ++i) {
729 n_str = term_utf8_decode(&t->utf8, &str, buf[i]);
730 for (j = 0; j < n_str; ++j) {
731 type = term_parser_feed(t->parser, &seq, str[j]);
733 log_error("error: term_parser_feed() (%d): %s", type, strerror(-type));
738 r = terminal_push_tmp(t, str[j]);
743 if (type == TERM_SEQ_NONE) {
744 /* We only intercept one-char sequences, so in
745 * case term_parser_feed() couldn't parse a
746 * sequence, it is waiting for more data. We
747 * know it can never be a one-char sequence
748 * then, so we can safely forward the data.
749 * This avoids withholding ESC or other values
750 * that may be one-shot depending on the
752 r = terminal_write_tmp(t);
755 } else if (t->is_menu) {
756 r = terminal_menu(t, seq);
759 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
760 terminal_discard_tmp(t);
764 r = terminal_write_tmp(t);
774 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
775 Terminal *t = userdata;
780 sd_event_exit(t->event, 0);
783 r = term_screen_feed_text(t->screen, ptr, size);
785 log_error("error: term_screen_feed_text() (%d): %s", r, strerror(-r));
796 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
797 Terminal *t = userdata;
803 r = ring_push(&t->out_ring, buf, size);
810 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
814 static Terminal *terminal_free(Terminal *t) {
818 ring_clear(&t->out_ring);
819 term_screen_unref(t->screen);
820 term_parser_free(t->parser);
821 output_free(t->output);
822 sd_event_source_unref(t->frame_timer);
823 sd_event_unref(t->event);
824 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
825 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
831 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
832 struct termios in_attr, out_attr;
836 assert_return(out, -EINVAL);
838 r = tcgetattr(in_fd, &in_attr);
840 log_error("error: tcgetattr() (%d): %m", -errno);
844 r = tcgetattr(out_fd, &out_attr);
846 log_error("error: tcgetattr() (%d): %m", -errno);
850 t = new0(Terminal, 1);
856 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
857 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
860 cfmakeraw(&out_attr);
862 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
864 log_error("error: tcsetattr() (%d): %s", r, strerror(-r));
868 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
870 log_error("error: tcsetattr() (%d): %s", r, strerror(-r));
874 r = sd_event_default(&t->event);
876 log_error("error: sd_event_default() (%d): %s", r, strerror(-r));
880 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
882 log_error("error: sigprocmask_many() (%d): %s", r, strerror(-r));
886 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
888 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
892 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
894 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
898 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
900 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
904 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
906 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
910 /* force initial redraw on event-loop enter */
912 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
914 log_error("error: sd_event_add_time() (%d): %s", r, strerror(-r));
918 r = output_new(&t->output, out_fd);
922 r = term_parser_new(&t->parser, true);
926 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
930 r = term_screen_set_answerback(t->screen, "systemd-subterm");
934 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
936 log_error("error: term_screen_resize() (%d): %s", r, strerror(-r));
940 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
952 static int terminal_run(Terminal *t) {
955 assert_return(t, -EINVAL);
957 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
959 log_error("error: cannot fork PTY (%d): %s", pid, strerror(-pid));
961 } else if (pid == 0) {
964 char **argv = (char*[]){
965 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
969 setenv("TERM", "xterm-256color", 1);
970 setenv("COLORTERM", "systemd-subterm", 1);
972 execve(argv[0], argv, environ);
973 log_error("error: cannot exec %s (%d): %m", argv[0], -errno);
979 return sd_event_loop(t->event);
986 int main(int argc, char *argv[]) {
990 r = terminal_new(&t, 0, 1);
1000 log_error("error: terminal failed (%d): %s", r, strerror(-r));