From 8a612292a81e5b130be8d70e98548a72929ba702 Mon Sep 17 00:00:00 2001 From: Wangshuyi Date: Wed, 30 Sep 2026 22:04:50 +0800 Subject: [PATCH 1/7] Support custom mobile terminal key combinations --- FEATURES.md | 9 + .../MobileTerminalShortcutsDialog.css | 41 ++++- .../MobileTerminalShortcutsDialog.tsx | 167 +++++++++++++++--- web/src/mobileTerminalKeyCombination.ts | 118 +++++++++++++ web/src/mobileTerminalShortcuts.test.ts | 90 ++++++++++ web/src/mobileTerminalShortcuts.ts | 53 ++++-- 6 files changed, 440 insertions(+), 38 deletions(-) create mode 100644 web/src/mobileTerminalKeyCombination.ts diff --git a/FEATURES.md b/FEATURES.md index e72acf33..4e4cd5bd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -280,6 +280,15 @@ separate for desktop/mobile. Jump from a diff to its file preview. - Customize the floating `2×8` grid and up to four side buttons under **Configuration > Behavior > Mobile terminal shortcuts**. The Tabs sheet and pane controls work when the tab strip is hidden. + Choose a preset or **Custom combination** to set an ASCII key, navigation key, + or F1-F12 with Ctrl, Alt, and Shift, plus a label. Edit or clear individual slots; + defaults stay available. Buttons send only that combination, with no extra + Enter. Shift uses US symbols; Ctrl letters ignore case and can share bytes with + other keys, while Alt sends an Escape prefix. Modified Enter requires application + support. Custom PageUp/PageDown sends application input; the presets scroll + history. Unsupported combinations block saving. Buttons bypass browser keyboard + shortcuts, but application keybindings still apply. Cmd/Meta, text macros, + commands, and multi-step sequences are not supported. - Drag the `⋯` controls button to move the floating controls; release snaps them to the nearer side edge at that height, mirrored on the left. The position is kept per browser and stays clear of the header and tab strip. diff --git a/web/src/components/MobileTerminalShortcutsDialog.css b/web/src/components/MobileTerminalShortcutsDialog.css index 5fb55c8b..1904f4be 100644 --- a/web/src/components/MobileTerminalShortcutsDialog.css +++ b/web/src/components/MobileTerminalShortcutsDialog.css @@ -220,6 +220,7 @@ } .mobile-shortcut-slot-editor-fields input, +.mobile-shortcut-slot-editor-fields select, .mobile-shortcut-key-trigger { width: 100%; min-width: 0; @@ -232,10 +233,48 @@ font: 12px / 1.2 inherit; } -.mobile-shortcut-slot-editor-fields input { +.mobile-shortcut-slot-editor-fields input, +.mobile-shortcut-slot-editor-fields select { padding: 0 8px; } +.mobile-shortcut-custom-fields { + grid-column: 1 / -1; + display: grid; + gap: 8px; +} + +.mobile-shortcut-modifiers { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.mobile-shortcut-modifiers label { + display: flex; + align-items: center; + gap: 5px; + text-transform: none; +} + +.mobile-shortcut-modifiers input { + width: 16px; + height: 16px; + accent-color: var(--accent); +} + +.mobile-shortcut-help, +.mobile-shortcut-error { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.mobile-shortcut-error { + color: var(--danger-text); +} + .mobile-shortcut-slot-editor-empty { min-height: 74px; display: flex; diff --git a/web/src/components/MobileTerminalShortcutsDialog.tsx b/web/src/components/MobileTerminalShortcutsDialog.tsx index aa4d20c4..49d4ccfa 100644 --- a/web/src/components/MobileTerminalShortcutsDialog.tsx +++ b/web/src/components/MobileTerminalShortcutsDialog.tsx @@ -1,5 +1,9 @@ import { useEffect, useRef, useState } from "react"; import { Check, ChevronsUpDown, Plus, RotateCcw, Trash2 } from "lucide-react"; +import { + MOBILE_TERMINAL_CUSTOM_SPECIAL_KEYS, + mobileTerminalKeyCombinationBytes, +} from "../mobileTerminalKeyCombination"; import { MAX_MOBILE_TERMINAL_SHORTCUTS_PER_ROW, MAX_MOBILE_TERMINAL_SIDE_SHORTCUTS, @@ -28,6 +32,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import "./MobileTerminalShortcutsDialog.css"; const OPTION_GROUPS = ["Control", "Basic", "Navigation", "Modified"] as const; +const CUSTOM_KEY_GROUPS = { + Letters: Array.from("abcdefghijklmnopqrstuvwxyz"), + Numbers: Array.from("0123456789"), + Symbols: Array.from("`-=[]\\;',./~!@#$%^&*()_+{}|:\"<>?"), + Special: MOBILE_TERMINAL_CUSTOM_SPECIAL_KEYS, +}; let nextShortcutId = 1; type SelectedSlot = @@ -91,20 +101,27 @@ function ShortcutKeySelect({ valueRef.current = value; const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); - const [activeValue, setActiveValue] = useState(() => String(value)); + const [activeValue, setActiveValue] = useState(() => + typeof value === "string" ? value : "custom", + ); const currentOption = mobileTerminalShortcutOption(value); const setSelectorOpen = (next: boolean) => { setOpen(next); setSearch(""); - if (next) setActiveValue(valueRef.current); + if (next) + setActiveValue( + typeof valueRef.current === "string" ? valueRef.current : "custom", + ); }; useEffect(() => { if (openRequest === 0) return; setOpen(true); setSearch(""); - setActiveValue(valueRef.current); + setActiveValue( + typeof valueRef.current === "string" ? valueRef.current : "custom", + ); }, [openRequest]); return ( @@ -117,7 +134,7 @@ function ShortcutKeySelect({ aria-expanded={open} aria-label={ariaLabel} > - {currentOption?.label ?? value} + {currentOption?.label}