From 51de1d8436100f725f3576aefa24a2bd2057bc28 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 22:36:56 +0200 Subject: Adding upstream version 0.37.0. Signed-off-by: Daniel Baumann --- osdep/terminal-unix.c | 573 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 573 insertions(+) create mode 100644 osdep/terminal-unix.c (limited to 'osdep/terminal-unix.c') diff --git a/osdep/terminal-unix.c b/osdep/terminal-unix.c new file mode 100644 index 0000000..d5b8fe3 --- /dev/null +++ b/osdep/terminal-unix.c @@ -0,0 +1,573 @@ +/* + * Based on GyS-TermIO v2.0 (for GySmail v3) (copyright (C) 1999 A'rpi/ESP-team) + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "osdep/io.h" +#include "osdep/threads.h" +#include "osdep/poll_wrapper.h" + +#include "common/common.h" +#include "misc/bstr.h" +#include "input/input.h" +#include "input/keycodes.h" +#include "misc/ctype.h" +#include "terminal.h" + +// Timeout in ms after which the (normally ambiguous) ESC key is detected. +#define ESC_TIMEOUT 100 + +// Timeout in ms after which the poll for input is aborted. The FG/BG state is +// tested before every wait, and a positive value allows reactivating input +// processing when mpv is brought to the foreground while it was running in the +// background. In such a situation, an infinite timeout (-1) will keep mpv +// waiting for input without realizing the terminal state changed - and thus +// buffer all keypresses until ENTER is pressed. +#define INPUT_TIMEOUT 1000 + +static struct termios tio_orig; + +static int tty_in = -1, tty_out = -1; + +struct key_entry { + const char *seq; + int mpkey; + // If this is not NULL, then if seq is matched as unique prefix, the + // existing sequence is replaced by the following string. Matching + // continues normally, and mpkey is or-ed into the final result. + const char *replace; +}; + +static const struct key_entry keys[] = { + {"\010", MP_KEY_BS}, + {"\011", MP_KEY_TAB}, + {"\012", MP_KEY_ENTER}, + {"\177", MP_KEY_BS}, + + {"\033[1~", MP_KEY_HOME}, + {"\033[2~", MP_KEY_INS}, + {"\033[3~", MP_KEY_DEL}, + {"\033[4~", MP_KEY_END}, + {"\033[5~", MP_KEY_PGUP}, + {"\033[6~", MP_KEY_PGDWN}, + {"\033[7~", MP_KEY_HOME}, + {"\033[8~", MP_KEY_END}, + + {"\033[11~", MP_KEY_F+1}, + {"\033[12~", MP_KEY_F+2}, + {"\033[13~", MP_KEY_F+3}, + {"\033[14~", MP_KEY_F+4}, + {"\033[15~", MP_KEY_F+5}, + {"\033[17~", MP_KEY_F+6}, + {"\033[18~", MP_KEY_F+7}, + {"\033[19~", MP_KEY_F+8}, + {"\033[20~", MP_KEY_F+9}, + {"\033[21~", MP_KEY_F+10}, + {"\033[23~", MP_KEY_F+11}, + {"\033[24~", MP_KEY_F+12}, + + {"\033OA", MP_KEY_UP}, + {"\033OB", MP_KEY_DOWN}, + {"\033OC", MP_KEY_RIGHT}, + {"\033OD", MP_KEY_LEFT}, + {"\033[A", MP_KEY_UP}, + {"\033[B", MP_KEY_DOWN}, + {"\033[C", MP_KEY_RIGHT}, + {"\033[D", MP_KEY_LEFT}, + {"\033[E", MP_KEY_KP5}, + {"\033[F", MP_KEY_END}, + {"\033[H", MP_KEY_HOME}, + + {"\033[[A", MP_KEY_F+1}, + {"\033[[B", MP_KEY_F+2}, + {"\033[[C", MP_KEY_F+3}, + {"\033[[D", MP_KEY_F+4}, + {"\033[[E", MP_KEY_F+5}, + + {"\033OE", MP_KEY_KP5}, // mintty? + {"\033OM", MP_KEY_KPENTER}, + {"\033OP", MP_KEY_F+1}, + {"\033OQ", MP_KEY_F+2}, + {"\033OR", MP_KEY_F+3}, + {"\033OS", MP_KEY_F+4}, + + {"\033Oa", MP_KEY_UP | MP_KEY_MODIFIER_CTRL}, // urxvt + {"\033Ob", MP_KEY_DOWN | MP_KEY_MODIFIER_CTRL}, + {"\033Oc", MP_KEY_RIGHT | MP_KEY_MODIFIER_CTRL}, + {"\033Od", MP_KEY_LEFT | MP_KEY_MODIFIER_CTRL}, + {"\033Oj", '*'}, // also keypad, but we don't have separate codes for them + {"\033Ok", '+'}, + {"\033Om", '-'}, + {"\033On", MP_KEY_KPDEC}, + {"\033Oo", '/'}, + {"\033Op", MP_KEY_KP0}, + {"\033Oq", MP_KEY_KP1}, + {"\033Or", MP_KEY_KP2}, + {"\033Os", MP_KEY_KP3}, + {"\033Ot", MP_KEY_KP4}, + {"\033Ou", MP_KEY_KP5}, + {"\033Ov", MP_KEY_KP6}, + {"\033Ow", MP_KEY_KP7}, + {"\033Ox", MP_KEY_KP8}, + {"\033Oy", MP_KEY_KP9}, + + {"\033[a", MP_KEY_UP | MP_KEY_MODIFIER_SHIFT}, // urxvt + {"\033[b", MP_KEY_DOWN | MP_KEY_MODIFIER_SHIFT}, + {"\033[c", MP_KEY_RIGHT | MP_KEY_MODIFIER_SHIFT}, + {"\033[d", MP_KEY_LEFT | MP_KEY_MODIFIER_SHIFT}, + {"\033[2^", MP_KEY_INS | MP_KEY_MODIFIER_CTRL}, + {"\033[3^", MP_KEY_DEL | MP_KEY_MODIFIER_CTRL}, + {"\033[5^", MP_KEY_PGUP | MP_KEY_MODIFIER_CTRL}, + {"\033[6^", MP_KEY_PGDWN | MP_KEY_MODIFIER_CTRL}, + {"\033[7^", MP_KEY_HOME | MP_KEY_MODIFIER_CTRL}, + {"\033[8^", MP_KEY_END | MP_KEY_MODIFIER_CTRL}, + + {"\033[1;2", MP_KEY_MODIFIER_SHIFT, .replace = "\033["}, // xterm + {"\033[1;3", MP_KEY_MODIFIER_ALT, .replace = "\033["}, + {"\033[1;5", MP_KEY_MODIFIER_CTRL, .replace = "\033["}, + {"\033[1;4", MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_SHIFT, .replace = "\033["}, + {"\033[1;6", MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_SHIFT, .replace = "\033["}, + {"\033[1;7", MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_ALT, .replace = "\033["}, + {"\033[1;8", + MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_SHIFT, + .replace = "\033["}, + + {"\033[29~", MP_KEY_MENU}, + {"\033[Z", MP_KEY_TAB | MP_KEY_MODIFIER_SHIFT}, + + {0} +}; + +#define BUF_LEN 256 + +struct termbuf { + unsigned char b[BUF_LEN]; + int len; + int mods; +}; + +static void skip_buf(struct termbuf *b, unsigned int count) +{ + assert(count <= b->len); + + memmove(&b->b[0], &b->b[count], b->len - count); + b->len -= count; + b->mods = 0; +} + +static struct termbuf buf; + +static void process_input(struct input_ctx *input_ctx, bool timeout) +{ + while (buf.len) { + // Lone ESC is ambiguous, so accept it only after a timeout. + if (timeout && + ((buf.len == 1 && buf.b[0] == '\033') || + (buf.len > 1 && buf.b[0] == '\033' && buf.b[1] == '\033'))) + { + mp_input_put_key(input_ctx, MP_KEY_ESC); + skip_buf(&buf, 1); + } + + int utf8_len = bstr_parse_utf8_code_length(buf.b[0]); + if (utf8_len > 1) { + if (buf.len < utf8_len) + goto read_more; + + mp_input_put_key_utf8(input_ctx, buf.mods, (bstr){buf.b, utf8_len}); + skip_buf(&buf, utf8_len); + continue; + } + + const struct key_entry *match = NULL; // may be a partial match + for (int n = 0; keys[n].seq; n++) { + const struct key_entry *e = &keys[n]; + if (memcmp(e->seq, buf.b, MPMIN(buf.len, strlen(e->seq))) == 0) { + if (match) + goto read_more; /* need more bytes to disambiguate */ + match = e; + } + } + + if (!match) { // normal or unknown key + int mods = 0; + if (buf.b[0] == '\033') { + if (buf.len > 1 && buf.b[1] == '[') { + // unknown CSI sequence. wait till it completes + for (int i = 2; i < buf.len; i++) { + if (buf.b[i] >= 0x40 && buf.b[i] <= 0x7E) { + skip_buf(&buf, i+1); + continue; // complete - throw it away + } + } + goto read_more; // not yet complete + } + // non-CSI esc sequence + skip_buf(&buf, 1); + if (buf.len > 0 && buf.b[0] > 0 && buf.b[0] < 127) { + // meta+normal key + mods |= MP_KEY_MODIFIER_ALT; + } else { + // Throw it away. Typically, this will be a complete, + // unsupported sequence, and dropping this will skip it. + skip_buf(&buf, buf.len); + continue; + } + } + unsigned char c = buf.b[0]; + skip_buf(&buf, 1); + if (c < 32) { + // 1..26 is ^A..^Z, and 27..31 is ^3..^7 + c = c <= 26 ? (c + 'a' - 1) : (c + '3' - 27); + mods |= MP_KEY_MODIFIER_CTRL; + } + mp_input_put_key(input_ctx, c | mods); + continue; + } + + int seq_len = strlen(match->seq); + if (seq_len > buf.len) + goto read_more; /* partial match */ + + if (match->replace) { + int rep = strlen(match->replace); + assert(rep <= seq_len); + memcpy(buf.b, match->replace, rep); + memmove(buf.b + rep, buf.b + seq_len, buf.len - seq_len); + buf.len = rep + buf.len - seq_len; + buf.mods |= match->mpkey; + continue; + } + + mp_input_put_key(input_ctx, buf.mods | match->mpkey); + skip_buf(&buf, seq_len); + } + +read_more: ; /* need more bytes */ +} + +static int getch2_active = 0; +static int getch2_enabled = 0; +static bool read_terminal; + +static void enable_kx(bool enable) +{ + // This check is actually always true, as enable_kx calls are all guarded + // by read_terminal, which is true only if both stdin and stdout are a + // tty. Note that stderr being redirected away has no influence over mpv's + // I/O handling except for disabling the terminal OSD, and thus stderr + // shouldn't be relied on here either. + if (isatty(tty_out)) { + char *cmd = enable ? "\033=" : "\033>"; + (void)write(tty_out, cmd, strlen(cmd)); + } +} + +static void do_activate_getch2(void) +{ + if (getch2_active || !read_terminal) + return; + + enable_kx(true); + + struct termios tio_new; + tcgetattr(tty_in,&tio_new); + + tio_new.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */ + tio_new.c_cc[VMIN] = 1; + tio_new.c_cc[VTIME] = 0; + tcsetattr(tty_in,TCSANOW,&tio_new); + + getch2_active = 1; +} + +static void do_deactivate_getch2(void) +{ + if (!getch2_active) + return; + + enable_kx(false); + tcsetattr(tty_in, TCSANOW, &tio_orig); + + getch2_active = 0; +} + +// sigaction wrapper +static int setsigaction(int signo, void (*handler) (int), + int flags, bool do_mask) +{ + struct sigaction sa; + sa.sa_handler = handler; + + if(do_mask) + sigfillset(&sa.sa_mask); + else + sigemptyset(&sa.sa_mask); + + sa.sa_flags = flags | SA_RESTART; + return sigaction(signo, &sa, NULL); +} + +static void getch2_poll(void) +{ + if (!getch2_enabled) + return; + + // check if stdin is in the foreground process group + int newstatus = (tcgetpgrp(tty_in) == getpgrp()); + + // and activate getch2 if it is, deactivate otherwise + if (newstatus) + do_activate_getch2(); + else + do_deactivate_getch2(); +} + +static mp_thread input_thread; +static struct input_ctx *input_ctx; +static int death_pipe[2] = {-1, -1}; +enum { PIPE_STOP, PIPE_CONT }; +static int stop_cont_pipe[2] = {-1, -1}; + +static void stop_sighandler(int signum) +{ + int saved_errno = errno; + (void)write(stop_cont_pipe[1], &(char){PIPE_STOP}, 1); + errno = saved_errno; + + // note: for this signal, we use SA_RESETHAND but do NOT mask signals + // so this will invoke the default handler + raise(SIGTSTP); +} + +static void continue_sighandler(int signum) +{ + int saved_errno = errno; + // SA_RESETHAND has reset SIGTSTP, so we need to restore it here + setsigaction(SIGTSTP, stop_sighandler, SA_RESETHAND, false); + + (void)write(stop_cont_pipe[1], &(char){PIPE_CONT}, 1); + errno = saved_errno; +} + +static void safe_close(int *p) +{ + if (*p >= 0) + close(*p); + *p = -1; +} + +static void close_sig_pipes(void) +{ + for (int n = 0; n < 2; n++) { + safe_close(&death_pipe[n]); + safe_close(&stop_cont_pipe[n]); + } +} + +static void close_tty(void) +{ + if (tty_in >= 0 && tty_in != STDIN_FILENO) + close(tty_in); + + tty_in = tty_out = -1; +} + +static void quit_request_sighandler(int signum) +{ + int saved_errno = errno; + (void)write(death_pipe[1], &(char){1}, 1); + errno = saved_errno; +} + +static MP_THREAD_VOID terminal_thread(void *ptr) +{ + mp_thread_set_name("terminal/input"); + bool stdin_ok = read_terminal; // if false, we still wait for SIGTERM + while (1) { + getch2_poll(); + struct pollfd fds[3] = { + { .events = POLLIN, .fd = death_pipe[0] }, + { .events = POLLIN, .fd = stop_cont_pipe[0] }, + { .events = POLLIN, .fd = tty_in } + }; + /* + * if the process isn't in foreground process group, then on macos + * polldev() doesn't rest and gets into 100% cpu usage (see issue #11795) + * with read() returning EIO. but we shouldn't quit on EIO either since + * the process might be foregrounded later. + * + * so just avoid poll-ing tty_in when we know the process is not in the + * foreground. there's a small race window, but the timeout will take + * care of it so it's fine. + */ + bool is_fg = tcgetpgrp(tty_in) == getpgrp(); + int r = polldev(fds, stdin_ok && is_fg ? 3 : 2, buf.len ? ESC_TIMEOUT : INPUT_TIMEOUT); + if (fds[0].revents) { + do_deactivate_getch2(); + break; + } + if (fds[1].revents & POLLIN) { + int8_t c = -1; + (void)read(stop_cont_pipe[0], &c, 1); + if (c == PIPE_STOP) + do_deactivate_getch2(); + else if (c == PIPE_CONT) + getch2_poll(); + } + if (fds[2].revents) { + int retval = read(tty_in, &buf.b[buf.len], BUF_LEN - buf.len); + if (!retval || (retval == -1 && errno != EINTR && errno != EAGAIN && errno != EIO)) + break; // EOF/closed + if (retval > 0) { + buf.len += retval; + process_input(input_ctx, false); + } + } + if (r == 0) + process_input(input_ctx, true); + } + char c; + bool quit = read(death_pipe[0], &c, 1) == 1 && c == 1; + // Important if we received SIGTERM, rather than regular quit. + if (quit) { + struct mp_cmd *cmd = mp_input_parse_cmd(input_ctx, bstr0("quit 4"), ""); + if (cmd) + mp_input_queue_cmd(input_ctx, cmd); + } + MP_THREAD_RETURN(); +} + +void terminal_setup_getch(struct input_ctx *ictx) +{ + if (!getch2_enabled || input_ctx) + return; + + if (mp_make_wakeup_pipe(death_pipe) < 0) + return; + if (mp_make_wakeup_pipe(stop_cont_pipe) < 0) { + close_sig_pipes(); + return; + } + + // Disable reading from the terminal even if stdout is not a tty, to make + // mpv ... | less + // do the right thing. + read_terminal = isatty(tty_in) && isatty(STDOUT_FILENO); + + input_ctx = ictx; + + if (mp_thread_create(&input_thread, terminal_thread, NULL)) { + input_ctx = NULL; + close_sig_pipes(); + close_tty(); + return; + } + + setsigaction(SIGINT, quit_request_sighandler, SA_RESETHAND, false); + setsigaction(SIGQUIT, quit_request_sighandler, SA_RESETHAND, false); + setsigaction(SIGTERM, quit_request_sighandler, SA_RESETHAND, false); +} + +void terminal_uninit(void) +{ + if (!getch2_enabled) + return; + + // restore signals + setsigaction(SIGCONT, SIG_DFL, 0, false); + setsigaction(SIGTSTP, SIG_DFL, 0, false); + setsigaction(SIGINT, SIG_DFL, 0, false); + setsigaction(SIGQUIT, SIG_DFL, 0, false); + setsigaction(SIGTERM, SIG_DFL, 0, false); + setsigaction(SIGTTIN, SIG_DFL, 0, false); + setsigaction(SIGTTOU, SIG_DFL, 0, false); + + if (input_ctx) { + (void)write(death_pipe[1], &(char){0}, 1); + mp_thread_join(input_thread); + close_sig_pipes(); + input_ctx = NULL; + } + + do_deactivate_getch2(); + close_tty(); + + getch2_enabled = 0; + read_terminal = false; +} + +bool terminal_in_background(void) +{ + return read_terminal && tcgetpgrp(STDERR_FILENO) != getpgrp(); +} + +void terminal_get_size(int *w, int *h) +{ + struct winsize ws; + if (ioctl(tty_in, TIOCGWINSZ, &ws) < 0 || !ws.ws_row || !ws.ws_col) + return; + + *w = ws.ws_col; + *h = ws.ws_row; +} + +void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height) +{ + struct winsize ws; + if (ioctl(tty_in, TIOCGWINSZ, &ws) < 0 || !ws.ws_row || !ws.ws_col + || !ws.ws_xpixel || !ws.ws_ypixel) + return; + + *rows = ws.ws_row; + *cols = ws.ws_col; + *px_width = ws.ws_xpixel; + *px_height = ws.ws_ypixel; +} + +void terminal_init(void) +{ + assert(!getch2_enabled); + getch2_enabled = 1; + + tty_in = tty_out = open("/dev/tty", O_RDWR | O_CLOEXEC); + if (tty_in < 0) { + tty_in = STDIN_FILENO; + tty_out = STDOUT_FILENO; + } + + tcgetattr(tty_in, &tio_orig); + + // handlers to fix terminal settings + setsigaction(SIGCONT, continue_sighandler, 0, true); + setsigaction(SIGTSTP, stop_sighandler, SA_RESETHAND, false); + setsigaction(SIGTTIN, SIG_IGN, 0, true); + setsigaction(SIGTTOU, SIG_IGN, 0, true); + + getch2_poll(); +} -- cgit v1.2.3