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 /* disable alternate screen buffer */
290 output_printf(o, "\e[?1049l");
293 /* o->fd is owned by the caller */
299 static int output_new(Output **out, int fd) {
303 assert_return(out, -EINVAL);
315 /* enable alternate screen buffer */
316 r = output_printf(o, "\e[?1049h");
332 static void output_draw_frame(Output *o) {
337 /* print header-frame */
339 output_printf(o, BORDER_DOWN_RIGHT);
340 output_print_line(o, o->width - 2);
341 output_printf(o, BORDER_DOWN_LEFT
344 "\e[2;%uH" /* cursor-position: 2/x */
349 output_print_line(o, o->width - 2);
350 output_printf(o, BORDER_VERT_LEFT
353 /* print body-frame */
355 for (i = 0; i < o->in_height; ++i) {
356 output_printf(o, BORDER_VERT
357 "\e[%u;%uH" /* cursor-position: 2/x */
363 /* print footer-frame */
365 output_printf(o, BORDER_VERT_RIGHT);
366 output_print_line(o, o->width - 2);
367 output_printf(o, BORDER_VERT_LEFT
370 "\e[%u;%uH" /* cursor-position: 2/x */
374 o->height - 1, o->width);
375 output_print_line(o, o->width - 2);
376 output_printf(o, BORDER_UP_LEFT);
378 /* print header/footer text */
380 output_printf(o, "\e[2;3H");
381 output_nprintf(o, o->width - 4, "systemd - embedded terminal emulator");
382 output_printf(o, "\e[%u;3H", o->height - 1);
383 output_nprintf(o, o->width - 4, "press ^C to enter menu");
386 static void output_draw_menu(Output *o) {
389 output_frame_printl(o, "%s", "");
390 output_frame_printl(o, " Menu: (the following keys are recognized)");
391 output_frame_printl(o, " q: quit");
392 output_frame_printl(o, " ^C: send ^C to the PTY");
395 static void output_draw_screen(Output *o, term_screen *s) {
402 for (j = 0; j < s->page->height && j < o->in_height; ++j) {
404 output_printf(o, "\e[m\r\n" BORDER_VERT);
407 for (i = 0; i < s->page->width && i < o->in_width; ++i) {
409 term_cell *cell = &s->page->lines[j]->cells[i];
414 switch (cell->attr.fg.ccode) {
415 case TERM_CCODE_DEFAULT:
416 output_printf(o, "\e[39m");
419 output_printf(o, "\e[38;5;%um", cell->attr.fg.c256);
422 output_printf(o, "\e[38;2;%u;%u;%um", cell->attr.fg.red, cell->attr.fg.green, cell->attr.fg.blue);
424 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
426 output_printf(o, "\e[%um", cell->attr.fg.ccode - TERM_CCODE_BLACK + 90);
428 output_printf(o, "\e[%um", cell->attr.fg.ccode - TERM_CCODE_BLACK + 30);
430 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
431 output_printf(o, "\e[%um", cell->attr.fg.ccode - TERM_CCODE_LIGHT_BLACK + 90);
435 switch (cell->attr.bg.ccode) {
436 case TERM_CCODE_DEFAULT:
437 output_printf(o, "\e[49m");
440 output_printf(o, "\e[48;5;%um", cell->attr.bg.c256);
443 output_printf(o, "\e[48;2;%u;%u;%um", cell->attr.bg.red, cell->attr.bg.green, cell->attr.bg.blue);
445 case TERM_CCODE_BLACK ... TERM_CCODE_WHITE:
446 output_printf(o, "\e[%um", cell->attr.bg.ccode - TERM_CCODE_BLACK + 40);
448 case TERM_CCODE_LIGHT_BLACK ... TERM_CCODE_LIGHT_WHITE:
449 output_printf(o, "\e[%um", cell->attr.bg.ccode - TERM_CCODE_LIGHT_BLACK + 100);
453 output_printf(o, "\e[%u;%u;%u;%u;%u;%um",
454 cell->attr.bold ? 1 : 22,
455 cell->attr.italic ? 3 : 23,
456 cell->attr.underline ? 4 : 24,
457 cell->attr.inverse ? 7 : 27,
458 cell->attr.blink ? 5 : 25,
459 cell->attr.hidden ? 8 : 28);
461 str = term_char_resolve(cell->ch, &len, &buf);
464 output_printf(o, " ");
466 for (k = 0; k < len; ++k) {
467 ulen = term_utf8_encode(utf8, str[k]);
468 output_write(o, utf8, ulen);
474 output_move_to(o, s->cursor_x + 1, s->cursor_y + 3);
475 output_printf(o, "\e[m");
478 static void output_draw(Output *o, bool menu, term_screen *screen) {
482 * This renders the contenst of the terminal. The layout contains a
483 * header, the main body and a footer. Around all areas we draw a
484 * border. It looks something like this:
486 * +----------------------------------------------------+
488 * +----------------------------------------------------+
497 * +----------------------------------------------------+
499 * +----------------------------------------------------+
501 * The body is the part that grows vertically.
503 * We need at least 6 vertical lines to render the screen. This would
504 * leave 0 lines for the body. Therefore, we require 7 lines so there's
505 * at least one body line. Similarly, we need 2 horizontal cells for the
506 * frame, so we require 3.
507 * If the window is too small, we print an error message instead.
510 if (o->in_width < 1 || o->in_height < 1) {
511 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
512 "\e[H"); /* cursor-position: home */
513 output_printf(o, "error: screen too small, need at least 3x7 cells");
519 output_printf(o, "\e[?25l");
521 /* frame-content is contant; only resizes can change it */
522 if (o->resized || o->in_menu != menu) {
523 output_printf(o, "\e[2J" /* erase-in-display: whole screen */
524 "\e[H"); /* cursor-position: home */
525 output_draw_frame(o);
530 /* move cursor to child's position */
531 output_move_to(o, 1, 3);
536 output_draw_screen(o, screen);
539 if (!(screen->flags & TERM_FLAG_HIDE_CURSOR))
540 output_printf(o, "\e[?25h");
543 * Hack: sd-term was not written to support TTY as output-objects, thus
544 * expects callers to use term_screen_feed_keyboard(). However, we
545 * forward TTY input directly. Hence, we're not notified about keypad
546 * changes. Update the related modes djring redraw to keep them at least
549 if (screen->flags & TERM_FLAG_CURSOR_KEYS)
550 output_printf(o, "\e[?1h");
552 output_printf(o, "\e[?1l");
554 if (screen->flags & TERM_FLAG_KEYPAD_MODE)
555 output_printf(o, "\e=");
557 output_printf(o, "\e>");
566 static void terminal_dirty(Terminal *t) {
572 if (t->is_scheduled) {
578 r = sd_event_now(t->event, CLOCK_MONOTONIC, &usec);
581 usec += 16 * USEC_PER_MSEC;
582 r = sd_event_source_set_time(t->frame_timer, usec);
584 r = sd_event_source_set_enabled(t->frame_timer, SD_EVENT_ONESHOT);
586 t->is_scheduled = true;
590 output_draw(t->output, t->is_menu, t->screen);
593 static int terminal_frame_timer_fn(sd_event_source *source, uint64_t usec, void *userdata) {
594 Terminal *t = userdata;
596 t->is_scheduled = false;
603 static int terminal_winch_fn(sd_event_source *source, const struct signalfd_siginfo *ssi, void *userdata) {
604 Terminal *t = userdata;
607 output_winch(t->output);
610 r = pty_resize(t->pty, t->output->in_width, t->output->in_height);
612 log_error("error: pty_resize() (%d): %s", r, strerror(-r));
615 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
617 log_error("error: term_screen_resize() (%d): %s", r, strerror(-r));
624 static int terminal_push_tmp(Terminal *t, uint32_t ucs4) {
631 len = term_utf8_encode(buf, ucs4);
635 r = ring_push(&t->out_ring, buf, len);
642 static int terminal_write_tmp(Terminal *t) {
649 num = ring_peek(&t->out_ring, vec);
654 for (i = 0; i < num; ++i) {
655 r = pty_write(t->pty, vec[i].iov_base, vec[i].iov_len);
657 log_error("error: cannot write to PTY (%d): %s", r, strerror(-r));
663 ring_flush(&t->out_ring);
667 static void terminal_discard_tmp(Terminal *t) {
670 ring_flush(&t->out_ring);
673 static int terminal_menu(Terminal *t, const term_seq *seq) {
675 case TERM_SEQ_IGNORE:
677 case TERM_SEQ_GRAPHIC:
678 switch (seq->terminator) {
680 sd_event_exit(t->event, 0);
685 case TERM_SEQ_CONTROL:
686 switch (seq->terminator) {
688 terminal_push_tmp(t, 0x03);
689 terminal_write_tmp(t);
702 static int terminal_io_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
703 Terminal *t = userdata;
708 len = read(fd, buf, sizeof(buf));
710 if (errno == EAGAIN || errno == EINTR)
713 log_error("error: cannot read from TTY (%d): %m", -errno);
717 for (i = 0; i < len; ++i) {
722 str = term_utf8_decode(&t->utf8, &n_str, buf[i]);
723 for (j = 0; j < n_str; ++j) {
724 type = term_parser_feed(t->parser, &seq, str[j]);
726 log_error("error: term_parser_feed() (%d): %s", type, strerror(-type));
731 r = terminal_push_tmp(t, str[j]);
736 if (type == TERM_SEQ_NONE) {
737 /* We only intercept one-char sequences, so in
738 * case term_parser_feed() couldn't parse a
739 * sequence, it is waiting for more data. We
740 * know it can never be a one-char sequence
741 * then, so we can safely forward the data.
742 * This avoids withholding ESC or other values
743 * that may be one-shot depending on the
745 r = terminal_write_tmp(t);
748 } else if (t->is_menu) {
749 r = terminal_menu(t, seq);
752 } else if (seq->type == TERM_SEQ_CONTROL && seq->terminator == 0x03) { /* ^C opens the menu */
753 terminal_discard_tmp(t);
757 r = terminal_write_tmp(t);
767 static int terminal_pty_fn(Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size) {
768 Terminal *t = userdata;
773 sd_event_exit(t->event, 0);
776 r = term_screen_feed_text(t->screen, ptr, size);
778 log_error("error: term_screen_feed_text() (%d): %s", r, strerror(-r));
789 static int terminal_write_fn(term_screen *screen, void *userdata, const void *buf, size_t size) {
790 Terminal *t = userdata;
796 r = ring_push(&t->out_ring, buf, size);
803 static int terminal_cmd_fn(term_screen *screen, void *userdata, unsigned int cmd, const term_seq *seq) {
807 static Terminal *terminal_free(Terminal *t) {
811 ring_clear(&t->out_ring);
812 term_screen_unref(t->screen);
813 term_parser_free(t->parser);
814 output_free(t->output);
815 sd_event_source_unref(t->frame_timer);
816 sd_event_unref(t->event);
817 tcsetattr(t->in_fd, TCSANOW, &t->saved_in_attr);
818 tcsetattr(t->out_fd, TCSANOW, &t->saved_out_attr);
824 static int terminal_new(Terminal **out, int in_fd, int out_fd) {
825 struct termios in_attr, out_attr;
829 assert_return(out, -EINVAL);
831 r = tcgetattr(in_fd, &in_attr);
833 log_error("error: tcgetattr() (%d): %m", -errno);
837 r = tcgetattr(out_fd, &out_attr);
839 log_error("error: tcgetattr() (%d): %m", -errno);
843 t = new0(Terminal, 1);
849 memcpy(&t->saved_in_attr, &in_attr, sizeof(in_attr));
850 memcpy(&t->saved_out_attr, &out_attr, sizeof(out_attr));
853 cfmakeraw(&out_attr);
855 r = tcsetattr(t->in_fd, TCSANOW, &in_attr);
857 log_error("error: tcsetattr() (%d): %s", r, strerror(-r));
861 r = tcsetattr(t->out_fd, TCSANOW, &out_attr);
863 log_error("error: tcsetattr() (%d): %s", r, strerror(-r));
867 r = sd_event_default(&t->event);
869 log_error("error: sd_event_default() (%d): %s", r, strerror(-r));
873 r = sigprocmask_many(SIG_BLOCK, SIGINT, SIGQUIT, SIGTERM, SIGWINCH, SIGCHLD, -1);
875 log_error("error: sigprocmask_many() (%d): %s", r, strerror(-r));
879 r = sd_event_add_signal(t->event, NULL, SIGINT, NULL, NULL);
881 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
885 r = sd_event_add_signal(t->event, NULL, SIGQUIT, NULL, NULL);
887 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
891 r = sd_event_add_signal(t->event, NULL, SIGTERM, NULL, NULL);
893 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
897 r = sd_event_add_signal(t->event, NULL, SIGWINCH, terminal_winch_fn, t);
899 log_error("error: sd_event_add_signal() (%d): %s", r, strerror(-r));
903 /* force initial redraw on event-loop enter */
905 r = sd_event_add_time(t->event, &t->frame_timer, CLOCK_MONOTONIC, 0, 0, terminal_frame_timer_fn, t);
907 log_error("error: sd_event_add_time() (%d): %s", r, strerror(-r));
911 r = output_new(&t->output, out_fd);
915 r = term_parser_new(&t->parser, true);
919 r = term_screen_new(&t->screen, terminal_write_fn, t, terminal_cmd_fn, t);
923 r = term_screen_set_answerback(t->screen, "systemd-subterm");
927 r = term_screen_resize(t->screen, t->output->in_width, t->output->in_height);
929 log_error("error: term_screen_resize() (%d): %s", r, strerror(-r));
933 r = sd_event_add_io(t->event, NULL, in_fd, EPOLLIN, terminal_io_fn, t);
945 static int terminal_run(Terminal *t) {
948 assert_return(t, -EINVAL);
950 pid = pty_fork(&t->pty, t->event, terminal_pty_fn, t, t->output->in_width, t->output->in_height);
952 log_error("error: cannot fork PTY (%d): %s", pid, strerror(-pid));
954 } else if (pid == 0) {
957 char **argv = (char*[]){
958 (char*)getenv("SHELL") ? : (char*)_PATH_BSHELL,
962 setenv("TERM", "xterm-256color", 1);
963 setenv("COLORTERM", "systemd-subterm", 1);
965 execve(argv[0], argv, environ);
966 log_error("error: cannot exec %s (%d): %m", argv[0], -errno);
972 return sd_event_loop(t->event);
979 int main(int argc, char *argv[]) {
983 r = terminal_new(&t, 0, 1);
993 log_error("error: terminal failed (%d): %s", r, strerror(-r));