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"
46 typedef struct Output Output;
47 typedef struct Terminal Terminal;
53 unsigned int in_width;
54 unsigned int in_height;
55 unsigned int cursor_x;
56 unsigned int cursor_y;
67 sd_event_source *frame_timer;
75 struct termios saved_in_attr;
76 struct termios saved_out_attr;
81 bool is_scheduled : 1;
90 #define BORDER_HORIZ "\xe2\x94\x81"
91 #define BORDER_VERT "\xe2\x94\x83"
92 #define BORDER_VERT_RIGHT "\xe2\x94\xa3"
93 #define BORDER_VERT_LEFT "\xe2\x94\xab"
94 #define BORDER_DOWN_RIGHT "\xe2\x94\x8f"
95 #define BORDER_DOWN_LEFT "\xe2\x94\x93"
96 #define BORDER_UP_RIGHT "\xe2\x94\x97"
97 #define BORDER_UP_LEFT "\xe2\x94\x9b"
99 static int output_winch(Output *o) {
100 struct winsize wsz = { };
103 assert_return(o, -EINVAL);
105 r = ioctl(o->fd, TIOCGWINSZ, &wsz);
107 return log_error_errno(errno, "error: cannot read window-size: %m");
109 if (wsz.ws_col != o->width || wsz.ws_row != o->height) {
110 o->width = wsz.ws_col;
111 o->height = wsz.ws_row;
112 o->in_width = MAX(o->width, 2U) - 2;
113 o->in_height = MAX(o->height, 6U) - 6;
120 static int output_flush(Output *o) {
126 r = loop_write(o->fd, o->obuf, o->n_obuf, false);
128 return log_error_errno(r, "error: cannot write to TTY: %m");
135 static int output_write(Output *o, const void *buf, size_t size) {
139 assert_return(o, -EINVAL);
140 assert_return(buf || size < 1, -EINVAL);
145 if (o->n_obuf + size > o->n_obuf && o->n_obuf + size <= sizeof(o->obuf)) {
146 memcpy(o->obuf + o->n_obuf, buf, size);
155 len = loop_write(o->fd, buf, size, false);
157 return log_error_errno(len, "error: cannot write to TTY (%zd): %m", len);
163 static int output_vnprintf(Output *o, size_t max, const char *format, va_list args) {
167 assert_return(o, -EINVAL);
168 assert_return(format, -EINVAL);
169 assert_return(max <= sizeof(buf), -EINVAL);
171 r = vsnprintf(buf, max, format, args);
172 if (r > (ssize_t)max)
175 return output_write(o, buf, r);
179 static int output_nprintf(Output *o, size_t max, const char *format, ...) {
183 va_start(args, format);
184 r = output_vnprintf(o, max, format, args);
191 static int output_vprintf(Output *o, const char *format, va_list args) {
195 assert_return(o, -EINVAL);
196 assert_return(format, -EINVAL);
198 r = vsnprintf(buf, sizeof(buf), format, args);
200 assert_return(r < (ssize_t)sizeof(buf), -ENOBUFS);
202 return output_write(o, buf, r);
206 static int output_printf(Output *o, const char *format, ...) {
210 va_start(args, format);
211 r = output_vprintf(o, format, args);
217 static int output_move_to(Output *o, unsigned int x, unsigned int y) {
220 assert_return(o, -EINVAL);
222 /* force the \e[H code as o->cursor_x/y might be out-of-date */
224 r = output_printf(o, "\e[%u;%uH", y + 1, x + 1);
233 static int output_print_line(Output *o, size_t len) {
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 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ
241 BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ BORDER_HORIZ;
242 const size_t max = (sizeof(line) - 1) / (sizeof(BORDER_HORIZ) - 1);
246 assert_return(o, -EINVAL);
248 for ( ; len > 0; len -= i) {
249 i = (len > max) ? max : len;
250 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 /* re-enable cursor */
285 output_printf(o, "\e[?25h");
286 /* disable alternate screen buffer */
287 output_printf(o, "\e[?1049l");
290 /* o->fd is owned by the caller */
296 static int output_new(Output **out, int fd) {
300 assert_return(out, -EINVAL);
312 /* enable alternate screen buffer */
313 r = output_printf(o, "\e[?1049h");
317 /* always hide cursor */
318 r = output_printf(o, "\e[?25l");
334 static void output_draw_frame(Output *o) {
339 /* print header-frame */
341 output_printf(o, BORDER_DOWN_RIGHT);
342 output_print_line(o, o->width - 2);
343 output_printf(o, BORDER_DOWN_LEFT
346 "\e[2;%uH" /* cursor-position: 2/x */
351 output_print_line(o, o->width - 2);
352 output_printf(o, BORDER_VERT_LEFT
355 /* print body-frame */
357 for (i = 0; i < o->in_height; ++i) {
358 output_printf(o, BORDER_VERT
359 "\e[%u;%uH" /* cursor-position: 2/x */
365 /* print footer-frame */
367 output_printf(o, BORDER_VERT_RIGHT);
368 output_print_line(o, o->width - 2);
369 output_printf(o, BORDER_VERT_LEFT
372 "\e[%u;%uH" /* cursor-position: 2/x */
376 o->height - 1, o->width);
377 output_print_line(o, o->width - 2);
378 output_printf(o, BORDER_UP_LEFT);
380 /* print header/footer text */
382 output_printf(o, "\e[2;3H");
383 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
384 output_printf(o, "\e[%u;3H", o->height - 1);
385 output_nprintf(o, o->width - 4, "press ^C to enter menu");
388 static void output_draw_menu(Output *o) {
391 output_frame_printl(o, "%s", "");
392 output_frame_printl(o, " Menu: (the following keys are recognized)");
393 output_frame_printl(o, " q: quit");
394 output_frame_printl(o, " ^C: send ^C to the PTY");
397 static int output_draw_cell_fn(term_screen *screen,
401 const term_attr *attr,
404 unsigned int ch_width) {
405 Output *o = userdata;
409 if (x >= o->in_width || y >= o->in_height)
412 if (x == 0 && y != 0)
413 output_printf(o, "\e[m\r\n" BORDER_VERT);
415 switch (attr->fg.ccode) {
416 case TERM_CCODE_DEFAULT:
417 output_printf(o, "\e[39m");
420 output_printf(o, "\e[38;5;%um", attr->fg.c256);
423 output_printf(o, "\e[38;2;%u;%u;%um", attr->fg.red, attr->fg.green, attr->fg.blue);
425 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
426 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_BLACK + 30);
428 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
429 output_printf(o, "\e[%um", attr->fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
433 switch (attr->bg.ccode) {
434 case TERM_CCODE_DEFAULT:
435 output_printf(o, "\e[49m");
438 output_printf(o, "\e[48;5;%um", attr->bg.c256);
441 output_printf(o, "\e[48;2;%u;%u;%um", attr->bg.red, attr->bg.green, attr->bg.blue);
443 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
444 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_BLACK + 40);
446 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
447 output_printf(o, "\e[%um", attr->bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
451 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
453 attr->italic ? 3 : 23,
454 attr->underline ? 4 : 24,
455 attr->inverse ? 7 : 27,
456 attr->blink ? 5 : 25,
457 attr->hidden ? 8 : 28);
460 output_printf(o, " ");
462 for (k = 0; k < n_ch; ++k) {
463 ulen = utf8_encode_unichar(utf8, ch[k]);
464 output_write(o, utf8, ulen);
471 static void output_draw_screen(Output *o, term_screen *s) {
475 term_screen_draw(s, output_draw_cell_fn, o, NULL);
477 output_printf(o, "\e[m");
480 static void output_draw(Output *o, bool menu, term_screen *screen) {
484 * This renders the contenst of the terminal. The layout contains a
485 * header, the main body and a footer. Around all areas we draw a
486 * border. It looks something like this:
488 * +----------------------------------------------------+
490 * +----------------------------------------------------+
499 * +----------------------------------------------------+
501 * +----------------------------------------------------+
503 * The body is the part that grows vertically.
505 * We need at least 6 vertical lines to render the screen. This would
506 * leave 0 lines for the body. Therefore, we require 7 lines so there's
507 * at least one body line. Similarly, we need 2 horizontal cells for the
508 * frame, so we require 3.
509 * If the window is too small, we print an error message instead.
512 if (o->in_width < 1 || o->in_height < 1) {
513 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
514 "\e[H"); /* cursor-position: home */
515 output_printf(o, "error: screen too small, need at least 3x7 cells");
521 output_printf(o, "\e[?25l");
523 /* frame-content is contant; only resizes can change it */
524 if (o->resized || o->in_menu != menu) {
525 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
526 "\e[H"); /* cursor-position: home */
527 output_draw_frame(o);
532 /* move cursor to child's position */
533 output_move_to(o, 1, 3);
538 output_draw_screen(o, screen);
541 * Hack: sd-term was not written to support TTY as output-objects, thus
542 * expects callers to use term_screen_feed_keyboard(). However, we
543 * forward TTY input directly. Hence, we're not notified about keypad
544 * changes. Update the related modes djring redraw to keep them at least
547 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
548 output_printf(o, "\e[?1h");
550 output_printf(o, "\e[?1l");
552 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
553 output_printf(o, "\e=");
555 output_printf(o, "\e>");
564 static void terminal_dirty(Terminal *t) {
570 if (t->is_scheduled) {
576 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
579 usec += 16 * USEC_PER_MSEC;
580 r = sd_event_source_set_time(t->frame_timer, usec);
582 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
584 t->is_scheduled = true;
588 output_draw(t->output, t->is_menu, t->screen);
591 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
592 Terminal *t = userdata;
594 t->is_scheduled = false;
601 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
602 Terminal *t = userdata;
605 output_winch(t->output);
608 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
610 log_error_errno(r, "error: pty_resize() (%d): %m", r);
613 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
615 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
622 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
629 len = utf8_encode_unichar(buf, ucs4);
633 r = ring_push(&t->out_ring, buf, len);
640 static int terminal_write_tmp(Terminal *t) {
647 num = ring_peek(&t->out_ring, vec);
652 for (i = 0; i < num; ++i) {
653 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
655 return log_error_errno(r, "error: cannot write to PTY (%d): %m", r);
659 ring_flush(&t->out_ring);
663 static void terminal_discard_tmp(Terminal *t) {
666 ring_flush(&t->out_ring);
669 static int terminal_menu(Terminal *t, const term_seq *seq) {
671 case TERM_SEQ_IGNORE:
673 case TERM_SEQ_GRAPHIC:
674 switch (seq->terminator) {
676 sd_event_exit(t->event, 0);
681 case TERM_SEQ_CONTROL:
682 switch (seq->terminator) {
684 terminal_push_tmp(t, 0x03);
685 terminal_write_tmp(t);
698 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
699 Terminal *t = userdata;
704 len = read(fd, buf, sizeof(buf));
706 if (errno == EAGAIN || errno == EINTR)
709 log_error_errno(errno, "error: cannot read from TTY (%d): %m", -errno);
713 for (i = 0; i < len; ++i) {
718 n_str = term_utf8_decode(&t->utf8, &str, buf[i]);
719 for (j = 0; j < n_str; ++j) {
720 type = term_parser_feed(t->parser, &seq, str[j]);
722 return log_error_errno(type, "error: term_parser_feed() (%d): %m", type);
725 r = terminal_push_tmp(t, str[j]);
730 if (type == TERM_SEQ_NONE) {
731 /* We only intercept one-char sequences, so in
732 * case term_parser_feed() couldn't parse a
733 * sequence, it is waiting for more data. We
734 * know it can never be a one-char sequence
735 * then, so we can safely forward the data.
736 * This avoids withholding ESC or other values
737 * that may be one-shot depending on the
739 r = terminal_write_tmp(t);
742 } else if (t->is_menu) {
743 r = terminal_menu(t, seq);
746 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
747 terminal_discard_tmp(t);
751 r = terminal_write_tmp(t);
761 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
762 Terminal *t = userdata;
767 sd_event_exit(t->event, 0);
770 r = term_screen_feed_text(t->screen, ptr, size);
772 return log_error_errno(r, "error: term_screen_feed_text() (%d): %m", r);
781 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
782 Terminal *t = userdata;
788 r = ring_push(&t->out_ring, buf, size);
795 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
799 static Terminal *terminal_free(Terminal *t) {
803 ring_clear(&t->out_ring);
804 term_screen_unref(t->screen);
805 term_parser_free(t->parser);
806 output_free(t->output);
807 sd_event_source_unref(t->frame_timer);
808 sd_event_unref(t->event);
809 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
810 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
816 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
817 struct termios in_attr, out_attr;
821 assert_return(out, -EINVAL);
823 r = tcgetattr(in_fd, &in_attr);
825 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
827 r = tcgetattr(out_fd, &out_attr);
829 return log_error_errno(errno, "error: tcgetattr() (%d): %m", -errno);
831 t = new0(Terminal, 1);
837 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
838 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
841 cfmakeraw(&out_attr);
843 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
845 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
849 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
851 log_error_errno(r, "error: tcsetattr() (%d): %m", r);
855 r = sd_event_default(&t->event);
857 log_error_errno(r, "error: sd_event_default() (%d): %m", r);
861 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
863 log_error_errno(r, "error: sigprocmask_many() (%d): %m", r);
867 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
869 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
873 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
875 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
879 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
881 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
885 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
887 log_error_errno(r, "error: sd_event_add_signal() (%d): %m", r);
891 /* force initial redraw on event-loop enter */
893 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
895 log_error_errno(r, "error: sd_event_add_time() (%d): %m", r);
899 r = output_new(&t->output, out_fd);
903 r = term_parser_new(&t->parser, true);
907 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
911 r = term_screen_set_answerback(t->screen, "systemd-subterm");
915 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
917 log_error_errno(r, "error: term_screen_resize() (%d): %m", r);
921 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
933 static int terminal_run(Terminal *t) {
936 assert_return(t, -EINVAL);
938 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
940 return log_error_errno(pid, "error: cannot fork PTY (%d): %m", pid);
944 char **argv = (char*[]){
945 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
949 setenv("TERM", "xterm-256color", 1);
950 setenv("COLORTERM", "systemd-subterm", 1);
952 execve(argv[0], argv, environ);
953 log_error_errno(errno, "error: cannot exec %s (%d): %m", argv[0], -errno);
959 return sd_event_loop(t->event);
966 int main(int argc, char *argv[]) {
970 r = terminal_new(&t, 0, 1);
980 log_error_errno(r, "error: terminal failed (%d): %m", r);