/* * 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; enum entry_type { ENTRY_TYPE_KEY = 0, ENTRY_TYPE_MOUSE_BUTTON, ENTRY_TYPE_MOUSE_MOVE, }; 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; // Extend the match length by a certain length, so the contents // after the match can be processed with custom logic. int skip; enum entry_type type; }; 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}, // Mouse button inputs. 2 bytes of position information requires special processing. {"\033[M ", MP_MBTN_LEFT | MP_KEY_STATE_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_BUTTON}, {"\033[M!", MP_MBTN_MID | MP_KEY_STATE_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_BUTTON}, {"\033[M\"", MP_MBTN_RIGHT | MP_KEY_STATE_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_BUTTON}, {"\033[M#", MP_INPUT_RELEASE_ALL, .skip = 2, .type = ENTRY_TYPE_MOUSE_BUTTON}, {"\033[M`", MP_WHEEL_UP, .skip = 2, .type = ENTRY_TYPE_MOUSE_BUTTON}, {"\033[Ma", MP_WHEEL_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_BUTTON}, // Mouse move inputs. No key events should be generated for them. {"\033[M@", MP_MBTN_LEFT | MP_KEY_STATE_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_MOVE}, {"\033[MA", MP_MBTN_MID | MP_KEY_STATE_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_MOVE}, {"\033[MB", MP_MBTN_RIGHT | MP_KEY_STATE_DOWN, .skip = 2, .type = ENTRY_TYPE_MOUSE_MOVE}, {"\033[MC", MP_INPUT_RELEASE_ALL, .skip = 2, .type = ENTRY_TYPE_MOUSE_MOVE}, {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] == '[') { // Throw away unrecognized mouse CSI sequences. // Cannot be handled by the loop below since the bytes // afterwards can be out of that range. if (buf.len > 2 && buf.b[2] == 'M') { skip_buf(&buf, buf.len); continue; } // 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) + match->skip; 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; } // Parse the initially skipped mouse position information. // The positions are 1-based character cell positions plus 32. // Treat mouse position as the pixel values at the center of the cell. if ((match->type == ENTRY_TYPE_MOUSE_BUTTON || match->type == ENTRY_TYPE_MOUSE_MOVE) && seq_len >= 6) { int num_rows = 80; int num_cols = 25; int total_px_width = 0; int total_px_height = 0; terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height); mp_input_set_mouse_pos(input_ctx, (buf.b[4] - 32.5) * (total_px_width / num_cols), (buf.b[5] - 32.5) * (total_px_height / num_rows)); } if (match->type != ENTRY_TYPE_MOUSE_MOVE) 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_cont_sighandler(int signum) { int saved_errno = errno; char sig = signum == SIGCONT ? PIPE_CONT : PIPE_STOP; (void)write(stop_cont_pipe[1], &sig, 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(); if (isatty(STDERR_FILENO)) { (void)write(STDERR_FILENO, TERM_ESC_RESTORE_CURSOR, sizeof(TERM_ESC_RESTORE_CURSOR) - 1); } // trying to reset SIGTSTP handler to default and raise it will // result in a race and there's no other way to invoke the // default handler. so just invoke SIGSTOP since it's // effectively the same thing. raise(SIGSTOP); } 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; // 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, 0, true); setsigaction(SIGTERM, quit_request_sighandler, 0, true); } 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_set_mouse_input(bool enable) { printf(enable ? TERM_ESC_ENABLE_MOUSE : TERM_ESC_DISABLE_MOUSE); fflush(stdout); } void terminal_init(void) { assert(!getch2_enabled); getch2_enabled = 1; if (mp_make_wakeup_pipe(stop_cont_pipe) < 0) { getch2_enabled = 0; return; } 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, stop_cont_sighandler, 0, true); setsigaction(SIGTSTP, stop_cont_sighandler, 0, true); setsigaction(SIGTTIN, SIG_IGN, 0, true); setsigaction(SIGTTOU, SIG_IGN, 0, true); getch2_poll(); }