#include "terminal_renderer.h" #include #include namespace crossdesk::terminal { void Input::Tap(ControllerSession &s, int key, int modifier) { if (modifier) s.Key(modifier, true); s.Key(key, true); s.Key(key, false); if (modifier) s.Key(modifier, false); } bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) { if (key == 29) { if (command_mode) { command_mode = false; s.SetControl(control); if (control) Tap(s, 0xDD, 17); // VK_OEM_6: right bracket, not ASCII ']'. } else { command_mode = true; // Release held buttons and suspend remote input while choosing a command. s.SetControl(false); mouse_buttons_ = 0; } return true; } if (control && !command_mode) return false; if (key == '?') { command_mode = true; s.SetControl(false); mouse_buttons_ = 0; return true; } if (command_mode && (key == 27 || key == 3)) { command_mode = false; s.SetControl(control); return true; } if (key == 'q' || key == 3) { exit = true; command_mode = false; control = false; } else if (key == 'c') { control = !control; } else if (key == '+' || key == '=' || key == '-' || key == 'f' || key == 'd' || key == 'i' || key == 'j' || key == 'k' || key == 'l') { Navigate(char(key), grid); } else if (!command_mode) { return false; } else { error = "Unknown local command; ? for help"; } s.SetControl(control && !command_mode); if (!control || command_mode) mouse_buttons_ = 0; return true; } void Input::Navigate(char key, const Grid *grid) { if (key == '+' || key == '=') zoom = std::min(32.0, zoom * 2); if (key == '-') zoom = std::max(1.0, zoom / 2); if (key == 'f') { native = false; zoom = 1; center_x = center_y = .5; } if (key == 'd') { native = true; zoom = 1; } // Use the displayed crop as the pan step, including native pixel mode. const double dx = grid && grid->source_width ? double(grid->source.width) / grid->source_width : 1 / zoom; const double dy = grid && grid->source_height ? double(grid->source.height) / grid->source_height : 1 / zoom; if (key == 'i' || key == 'j' || key == 'k' || key == 'l') { center_x = std::clamp(center_x, dx / 2, 1 - dx / 2); center_y = std::clamp(center_y, dy / 2, 1 - dy / 2); } if (key == 'j') center_x -= .2 * dx; if (key == 'l') center_x += .2 * dx; if (key == 'i') center_y -= .2 * dy; if (key == 'k') center_y += .2 * dy; center_x = std::clamp(center_x, 0.0, 1.0); center_y = std::clamp(center_y, 0.0, 1.0); ++view_revision; } void Input::Mouse(int b, int x, int y, bool release, ControllerSession &s, const Grid *grid) { ++mouse_reports; // Extra mouse buttons must not alias the three supported remote buttons. if (!grid || command_mode || b < 0 || b >= 128) return; auto pos = MapPointer(*grid, x - 1, y - 1); if (!control) { if (pos && !release && ((b & 64) || b == 0)) { center_x = pos->first; center_y = pos->second; Navigate((b & 64) ? ((b & 1) ? '-' : '+') : 'd', grid); } return; } if (!pos && !release) return; if (pos) { pointer_x_ = pos->first; pointer_y_ = pos->second; } const auto send = [&](int flag, int wheel = 0) { if (s.Pointer(pointer_x_, pointer_y_, flag, wheel)) { ++mouse_forwarded; return true; } ++mouse_send_failures; return false; }; // Linux/X11 only positions the cursor on move events. A click or wheel // report can arrive without prior motion, including over an SSH terminal. // Always position before a button/scroll event; still release on failure. if (pos && !send(0) && !release) return; if (b & 64) { if (!release) send((b & 2) ? 8 : 7, (b & 1) ? -120 : 120); return; } if (b & 32) return; const int base = b & 3; if (base == 3) { // Legacy reports identify a release, but not which button was released. if (release) { for (unsigned i = 0; i < 3; ++i) if ((mouse_buttons_ & (1u << i)) && send(2 + i * 2)) mouse_buttons_ &= ~(1u << i); } return; } const int index = base == 0 ? 0 : base == 1 ? 2 : 1; if (send(1 + index * 2 + int(release))) { if (release) mouse_buttons_ &= ~(1u << index); else mouse_buttons_ |= 1u << index; } } void Input::Feed(const std::string &bytes, ControllerSession &s, const Grid *grid) { if (exit) return; if (pending_.size() + bytes.size() > 32768) { pending_.clear(); pasting_ = false; s.SetControl(false); mouse_buttons_ = 0; control = false; return; } if (!bytes.empty()) { last_input_ = std::chrono::steady_clock::now(); error.clear(); } if (bytes.empty() && pending_ == "\033" && std::chrono::steady_clock::now() - last_input_ > std::chrono::milliseconds(80)) { if (!LocalKey(27, s, grid)) Tap(s, 27); pending_.clear(); return; } pending_ += bytes; while (!pending_.empty()) { if (pasting_) { auto end = pending_.find("\033[201~"); if (end == std::string::npos) return; // bounded by 32 KiB; one UTF-8 transaction // Drop local-mode paste without leaving the mode or running commands. if (control && !command_mode && !s.Text(pending_.substr(0, end))) error = "Remote UTF-8 text input unavailable"; pending_.erase(0, end + 6); pasting_ = false; continue; } unsigned char c = pending_[0]; if (c == 29) { LocalKey(c, s, grid); pending_.erase(0, 1); continue; } if (c == 27) { if (pending_.size() < 2) return; if (pending_.compare(0, 6, "\033[200~") == 0) { pasting_ = true; pending_.erase(0, 6); continue; } if (pending_[1] == '_') { auto end = pending_.find("\033\\", 2); if (end == std::string::npos) return; if (auto ack = ParseKittyAck(pending_.substr(0, end + 2))) kitty_ack = ack; pending_.erase(0, end + 2); continue; } if (pending_[1] == '[' || pending_[1] == 'O') { if (pending_.compare(0, 3, "\033[M") == 0) { // Normal (non-SGR) reports have three raw bytes after CSI M. // Keep the whole packet buffered across reads so its coordinates // cannot become keystrokes or local commands. if (pending_.size() < 6) return; const int b = static_cast(pending_[3]) - 32; const int x = static_cast(pending_[4]) - 32; const int y = static_cast(pending_[5]) - 32; pending_.erase(0, 6); if (b >= 0 && x > 0 && y > 0) { ++mouse_legacy_reports; const bool release = (b & 3) == 3 && !(b & (32 | 64)); Mouse(b, x, y, release, s, grid); } continue; } size_t end = 2; while (end < pending_.size() && !(pending_[end] >= '@' && pending_[end] <= '~')) ++end; if (end == pending_.size()) return; std::string seq = pending_.substr(0, end + 1); pending_.erase(0, end + 1); if (seq.back() == 't') { terminal_metrics.Report(seq); continue; } if (seq == "\033[O") { control = false; s.SetControl(false); mouse_buttons_ = 0; continue; } int key = 0; if (seq == "\033[A" || seq == "\033OA") key = 38; else if (seq == "\033[B" || seq == "\033OB") key = 40; else if (seq == "\033[C" || seq == "\033OC") key = 39; else if (seq == "\033[D" || seq == "\033OD") key = 37; else if (seq == "\033[H") key = 36; else if (seq == "\033[F") key = 35; else if (seq == "\033[3~") key = 46; else if (seq == "\033[5~") key = 33; else if (seq == "\033[6~") key = 34; else if (seq == "\033OP") key = 112; else if (seq == "\033OQ") key = 113; else if (seq == "\033OR") key = 114; else if (seq == "\033OS") key = 115; if (key) { if ((!control || command_mode) && key >= 37 && key <= 40) { LocalKey(key == 37 ? 'j' : key == 38 ? 'i' : key == 39 ? 'l' : 'k', s, grid); continue; } if (command_mode) LocalKey(0, s, grid); else Tap(s, key); continue; } int b = 0, x = 0, y = 0; char action = seq.back(); bool mouse = seq.rfind("\033[<", 0) == 0 && (action == 'M' || action == 'm'); const char *cur = seq.data() + std::min(size_t(3), seq.size()); const char *limit = seq.data() + seq.size() - 1; for (int *value : {&b, &x, &y}) { if (!mouse) break; auto result = std::from_chars(cur, limit, *value); mouse = result.ec == std::errc{} && *value >= 0 && *value <= 10000; cur = result.ptr; if (value != &y) { mouse = mouse && cur < limit && *cur == ';'; if (mouse) ++cur; } else mouse = mouse && cur == limit; } if (mouse) Mouse(b, x, y, action == 'm', s, grid); continue; } // Alt+ASCII letter: a tap with explicit modifier release. if (command_mode) LocalKey(0, s, grid); else if (pending_[1] >= 'a' && pending_[1] <= 'z') Tap(s, pending_[1] - 'a' + 'A', 18); pending_.erase(0, 2); continue; } if (c < 32 || c == 127) { pending_.erase(0, 1); if (LocalKey(c, s, grid)) { if (exit) return; } else if (c == 13 || c == 10) Tap(s, 13); else if (c == 9) Tap(s, 9); else if (c == 127 || c == 8) Tap(s, 8); else if (c >= 1 && c <= 26) Tap(s, 'A' + c - 1, 17); continue; } size_t n = c < 128 ? 1 : (c & 0xe0) == 0xc0 ? 2 : (c & 0xf0) == 0xe0 ? 3 : (c & 0xf8) == 0xf0 ? 4 : 1; if (pending_.size() < n) return; auto text = pending_.substr(0, n); pending_.erase(0, n); if (LocalKey(c, s, grid)) { if (exit) return; } else if (control && !s.Text(text)) error = "Remote UTF-8 text input unavailable"; } } } // namespace crossdesk::terminal