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_errno(errno, "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 return log_error_errno(len, "error: cannot write to TTY (%zd): %m", len);
136 static int output_write(Output *o, const void *buf, size_t size) {
140 assert_return(o, -EINVAL);
141 assert_return(buf || size < 1, -EINVAL);
146 if (o->n_obuf + size > o->n_obuf && o->n_obuf + size <= sizeof(o->obuf)) {
147 memcpy(o->obuf + o->n_obuf, buf, size);
156 len = loop_write(o->fd, buf, size, false);
158 return log_error_errno(len, "error: cannot write to TTY (%zd): %m", len);
164 static int output_vnprintf(Output *o, size_t max, const char *format, va_list args) {
168 assert_return(o, -EINVAL);
169 assert_return(format, -EINVAL);
170 assert_return(max <= sizeof(buf), -EINVAL);
172 r = vsnprintf(buf, max, format, args);
173 if (r > (ssize_t)max)
176 return output_write(o, buf, r);
180 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
184 va_start(args, format);
185 r = output_vnprintf(o, max, format, args);
192 static int output_vprintf(Output *o, const char *format, va_list args) {
196 assert_return(o, -EINVAL);
197 assert_return(format, -EINVAL);
199 r = vsnprintf(buf, sizeof(buf), format, args);
201 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
203 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));
260 static int output_frame_printl(Output *o, const char *format, ...) {
268 if (o->cursor_y < 3 || o->cursor_y >= o->height - 3)
271 va_start(args, format);
272 r = output_vnprintf(o, o->width - 2, format, args);
278 return output_move_to(o, 1, o->cursor_y + 1);
281 static Output *output_free(Output *o) {
285 /* re-enable cursor */
286 output_printf(o, "\e[?25h");
287 /* disable alternate screen buffer */
288 output_printf(o, "\e[?1049l");
291 /* o->fd is owned by the caller */
297 static int output_new(Output **out, int fd) {
301 assert_return(out, -EINVAL);
313 /* enable alternate screen buffer */
314 r = output_printf(o, "\e[?1049h");
318 /* always hide cursor */
319 r = output_printf(o, "\e[?25l");
335 static void output_draw_frame(Output *o) {
340 /* print header-frame */
342 output_printf(o, BORDER_DOWN_RIGHT);
343 output_print_line(o, o->width - 2);
344 output_printf(o, BORDER_DOWN_LEFT
347 "\e[2;%uH" /* cursor-position: 2/x */
352 output_print_line(o, o->width - 2);
353 output_printf(o, BORDER_VERT_LEFT
356 /* print body-frame */
358 for (i = 0; i < o->in_height; ++i) {
359 output_printf(o, BORDER_VERT
360 "\e[%u;%uH" /* cursor-position: 2/x */
366 /* print footer-frame */
368 output_printf(o, BORDER_VERT_RIGHT);
369 output_print_line(o, o->width - 2);
370 output_printf(o, BORDER_VERT_LEFT
373 "\e[%u;%uH" /* cursor-position: 2/x */
377 o->height - 1, o->width);
378 output_print_line(o, o->width - 2);
379 output_printf(o, BORDER_UP_LEFT);
381 /* print header/footer text */
383 output_printf(o, "\e[2;3H");
384 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
385 output_printf(o, "\e[%u;3H", o->height - 1);
386 output_nprintf(o, o->width - 4, "press ^C to enter menu");
389 static void output_draw_menu(Output *o) {
392 output_frame_printl(o, "%s", "");
393 output_frame_printl(o, " Menu: (the following keys are recognized)");
394 output_frame_printl(o, " q: quit");
395 output_frame_printl(o, " ^C: send ^C to the PTY");
398 static int output_draw_cell_fn(term_screen *screen,
402 const term_attr *attr,
405 unsigned int ch_width) {
406 Output *o = userdata;
410 if (x >= o->in_width || y >= o->in_height)
413 if (x == 0 && y != 0)
414 output_printf(o, "\e[m\r\n" BORDER_VERT);
416 switch (attr->fg.ccode) {
417 case TERM_CCODE_DEFAULT:
418 output_printf(o, "\e[39m");
421 output_printf(o, "\e[38;5;%um", attr->fg.c256);
424 output_printf(o, "\e[38;2;%u;%u;%um", attr->fg.red, attr->fg.green, attr->fg.blue);
426 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
427 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 30);
429 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
430 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
434 switch (attr->bg.ccode) {
435 case TERM_CCODE_DEFAULT:
436 output_printf(o, "\e[49m");
439 output_printf(o, "\e[48;5;%um", attr->bg.c256);
442 output_printf(o, "\e[48;2;%u;%u;%um", attr->bg.red, attr->bg.green, attr->bg.blue);
444 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
445 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_BLACK + 40);
447 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
448 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
452 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
454 attr->italic ? 3 : 23,
455 attr->underline ? 4 : 24,
456 attr->inverse ? 7 : 27,
457 attr->blink ? 5 : 25,
458 attr->hidden ? 8 : 28);
461 output_printf(o, " ");
463 for (k = 0; k < n_ch; ++k) {
464 ulen = term_utf8_encode(utf8, ch[k]);
465 output_write(o, utf8, ulen);
472 static void output_draw_screen(Output *o, term_screen *s) {
476 term_screen_draw(s, output_draw_cell_fn, o, NULL);
478 output_printf(o, "\e[m");
481 static void output_draw(Output *o, bool menu, term_screen *screen) {
485 * This renders the contenst of the terminal. The layout contains a
486 * header, the main body and a footer. Around all areas we draw a
487 * border. It looks something like this:
489 * +----------------------------------------------------+
491 * +----------------------------------------------------+
500 * +----------------------------------------------------+
502 * +----------------------------------------------------+
504 * The body is the part that grows vertically.
506 * We need at least 6 vertical lines to render the screen. This would
507 * leave 0 lines for the body. Therefore, we require 7 lines so there's
508 * at least one body line. Similarly, we need 2 horizontal cells for the
509 * frame, so we require 3.
510 * If the window is too small, we print an error message instead.
513 if (o->in_width < 1 || o->in_height < 1) {
514 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
515 "\e[H"); /* cursor-position: home */
516 output_printf(o, "error: screen too small, need at least 3x7 cells");
522 output_printf(o, "\e[?25l");
524 /* frame-content is contant; only resizes can change it */
525 if (o->resized || o->in_menu != menu) {
526 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
527 "\e[H"); /* cursor-position: home */
528 output_draw_frame(o);
533 /* move cursor to child's position */
534 output_move_to(o, 1, 3);
539 output_draw_screen(o, screen);
542 * Hack: sd-term was not written to support TTY as output-objects, thus
543 * expects callers to use term_screen_feed_keyboard(). However, we
544 * forward TTY input directly. Hence, we're not notified about keypad
545 * changes. Update the related modes djring redraw to keep them at least
548 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
549 output_printf(o, "\e[?1h");
551 output_printf(o, "\e[?1l");
553 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
554 output_printf(o, "\e=");
556 output_printf(o, "\e>");
565 static void terminal_dirty(Terminal *t) {
571 if (t->is_scheduled) {
577 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
580 usec += 16 * USEC_PER_MSEC;
581 r = sd_event_source_set_time(t->frame_timer, usec);
583 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
585 t->is_scheduled = true;
589 output_draw(t->output, t->is_menu, t->screen);
592 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
593 Terminal *t = userdata;
595 t->is_scheduled = false;
602 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
603 Terminal *t = userdata;
606 output_winch(t->output);
609 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
611 log_error_errno(r, "error: pty_resize() (%d): %m", r);
614 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
616 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
623 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
630 len = term_utf8_encode(buf, ucs4);
634 r = ring_push(&t->out_ring, buf, len);
641 static int terminal_write_tmp(Terminal *t) {
648 num = ring_peek(&t->out_ring, vec);
653 for (i = 0; i < num; ++i) {
654 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
656 return log_error_errno(r, "error: cannot write to PTY (%d): %m", r);
660 ring_flush(&t->out_ring);
664 static void terminal_discard_tmp(Terminal *t) {
667 ring_flush(&t->out_ring);
670 static int terminal_menu(Terminal *t, const term_seq *seq) {
672 case TERM_SEQ_IGNORE:
674 case TERM_SEQ_GRAPHIC:
675 switch (seq->terminator) {
677 sd_event_exit(t->event, 0);
682 case TERM_SEQ_CONTROL:
683 switch (seq->terminator) {
685 terminal_push_tmp(t, 0x03);
686 terminal_write_tmp(t);
699 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
700 Terminal *t = userdata;
705 len = read(fd, buf, sizeof(buf));
707 if (errno == EAGAIN || errno == EINTR)
710 log_error_errno(errno, "error: cannot read from TTY (%d): %m", -errno);
714 for (i = 0; i < len; ++i) {
719 n_str = term_utf8_decode(&t->utf8, &str, buf[i]);
720 for (j = 0; j < n_str; ++j) {
721 type = term_parser_feed(t->parser, &seq, str[j]);
723 return log_error_errno(type, "error: term_parser_feed() (%d): %m", 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 return log_error_errno(r, "error: term_screen_feed_text() (%d): %m", r);
782 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
783 Terminal *t = userdata;
789 r = ring_push(&t->out_ring, buf, size);
796 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
800 static Terminal *terminal_free(Terminal *t) {
804 ring_clear(&t->out_ring);
805 term_screen_unref(t->screen);
806 term_parser_free(t->parser);
807 output_free(t->output);
808 sd_event_source_unref(t->frame_timer);
809 sd_event_unref(t->event);
810 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
811 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
817 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
818 struct termios in_attr, out_attr;
822 assert_return(out, -EINVAL);
824 r = tcgetattr(in_fd, &in_attr);
826 log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
830 r = tcgetattr(out_fd, &out_attr);
832 log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
836 t = new0(Terminal, 1);
842 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
843 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
846 cfmakeraw(&out_attr);
848 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
850 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
854 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
856 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
860 r = sd_event_default(&t->event);
862 log_error_errno(r, "error: sd_event_default() (%d): %m", r);
866 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
868 log_error_errno(r, "error: sigprocmask_many() (%d): %m", r);
872 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
874 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
878 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
880 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
884 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
886 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
890 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
892 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
896 /* force initial redraw on event-loop enter */
898 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
900 log_error_errno(r, "error: sd_event_add_time() (%d): %m", r);
904 r = output_new(&t->output, out_fd);
908 r = term_parser_new(&t->parser, true);
912 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
916 r = term_screen_set_answerback(t->screen, "systemd-subterm");
920 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
922 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
926 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
938 static int terminal_run(Terminal *t) {
941 assert_return(t, -EINVAL);
943 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
945 return log_error_errno(pid, "error: cannot fork PTY (%d): %m", pid);
949 char **argv = (char*[]){
950 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
954 setenv("TERM", "xterm-256color", 1);
955 setenv("COLORTERM", "systemd-subterm", 1);
957 execve(argv[0], argv, environ);
958 log_error_errno(errno, "error: cannot exec %s (%d): %m", argv[0], -errno);
964 return sd_event_loop(t->event);
971 int main(int argc, char *argv[]) {
975 r = terminal_new(&t, 0, 1);
985 log_error_errno(r, "error: terminal failed (%d): %m", r);