mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-27 16:46:35 +00:00
* feat(terminal): add Ctrl toggle and Ctrl+X shortcut keys to mobile terminal floating keyboard Signed-off-by: dongrencd <dongrencd@users.noreply.github.com> * refactor(terminal): restructure keyboard layout with collapse button - Move | from Row1 position 3 to Row1 end (aligned with collapse button) - Remove ~ from Row2, add collapse button (∨/∧) after PgDn - Row3: conditional render, add ~ and -, remove trailing placeholders - Collapse state persisted via kOptionEnableShowTerminalCtrlKeys - Row3 defaults to collapsed for compact layout Signed-off-by: dongrencd <dongrencd@users.noreply.github.com> * fix(terminal): restore trailing placeholders in Row3 for alignment Row3 needs trailing placeholders to match Row1/Row2 width (348px) so Ctrl aligns with Tab in Row2 and Esc in Row1. Signed-off-by: dongrencd <dongrencd@users.noreply.github.com> * fix(terminal): update mobile keyboard layout per review Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> * fix(terminal): address mobile keyboard review regressions Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> * fix(terminal): preserve ctrl-j newline mapping on mobile Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> * fix(terminal): preserve pasted input with modifiers Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> * fix(terminal): harden mobile modifier and paste input Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> * fix(terminal): harden mobile paste shortcut handling Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> * fix(terminal): preserve unicode graphemes under ctrl * fix(terminal): avoid modifier scan for inactive locks * fix(terminal): keep default hardware paste shortcuts * fix(terminal): guard hardware paste with modifier locks * fix(terminal): update mobile key button color role --------- Signed-off-by: dongrencd <dongrencd@users.noreply.github.com> Signed-off-by: dong.ren.cd <dong.ren.cd@tcl.com> Co-authored-by: dongrencd <dongrencd@users.noreply.github.com> Co-authored-by: dong.ren.cd <dong.ren.cd@tcl.com>
191 lines
6.0 KiB
Dart
191 lines
6.0 KiB
Dart
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
/// Identifies where terminal input originated so paste data can bypass all
|
|
/// keyboard-only transformations.
|
|
enum TerminalInputSource {
|
|
keyboard,
|
|
paste,
|
|
}
|
|
|
|
/// Returns true when a stale mobile one-shot Shift state should be released
|
|
/// by replaying a tracked Shift key-down as a synthesized key-up.
|
|
///
|
|
/// This is only valid on mobile when Flutter's cached Shift state is still on
|
|
/// (`cachedShiftPressed == true`) but the current hardware/raw event reports
|
|
/// Shift as off (`actualShiftPressed == false`).
|
|
///
|
|
/// A tracked Shift key-down is required so the caller can safely synthesize the
|
|
/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the
|
|
/// Shift key event itself must be processed first; otherwise we could release
|
|
/// the tracked key while still handling the original Shift press/release.
|
|
/// Callers should evaluate this only after their cached modifier state has been
|
|
/// updated for the current event.
|
|
///
|
|
/// When this returns true, the caller logs a line like:
|
|
/// `input: releasing stale mobile Shift before replaying tracked raw key-up`
|
|
/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`.
|
|
bool shouldReleaseStaleMobileShift({
|
|
required bool isMobile,
|
|
required bool cachedShiftPressed,
|
|
required bool actualShiftPressed,
|
|
required LogicalKeyboardKey logicalKey,
|
|
required bool hasTrackedShiftKeyDown,
|
|
}) {
|
|
if (!isMobile || !cachedShiftPressed || actualShiftPressed) {
|
|
return false;
|
|
}
|
|
if (!hasTrackedShiftKeyDown) {
|
|
return false;
|
|
}
|
|
if (logicalKey == LogicalKeyboardKey.shiftLeft ||
|
|
logicalKey == LogicalKeyboardKey.shiftRight) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Applies the terminal Ctrl/Alt one-shot modifiers to a single input payload.
|
|
///
|
|
String applyTerminalInputModifiers(
|
|
String data, {
|
|
required bool ctrlLocked,
|
|
required bool altLocked,
|
|
}) {
|
|
var result = data;
|
|
if (ctrlLocked) {
|
|
result = _applyTerminalCtrlModifier(result);
|
|
}
|
|
if (altLocked) {
|
|
result = '\x1B$result';
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Builds the exact payload xterm sends for paste, without applying modifiers.
|
|
String terminalPastePayload(String text, {required bool bracketedPasteMode}) {
|
|
if (!bracketedPasteMode) {
|
|
return text;
|
|
}
|
|
return '\x1B[200~$text\x1B[201~';
|
|
}
|
|
|
|
/// Returns whether one-shot Ctrl/Alt may transform and consume this input.
|
|
///
|
|
/// xterm emits terminal control keys as either one control byte or a longer
|
|
/// escape sequence. Neither form is ordinary text input, so a pending modifier
|
|
/// must survive until the user enters a printable character.
|
|
bool shouldApplyTerminalInputModifiers(String data) {
|
|
if (data.characters.length != 1) return false;
|
|
final codeUnit = data.codeUnitAt(0);
|
|
return codeUnit >= 0x20 && codeUnit != 0x7F;
|
|
}
|
|
|
|
/// Builds the payload sent to the remote terminal for keyboard and paste input.
|
|
///
|
|
/// Keyboard input keeps the mobile Enter workaround and one-shot Ctrl/Alt
|
|
/// mapping. Paste input deliberately bypasses both transformations so even a
|
|
/// one-character clipboard payload is preserved exactly.
|
|
String prepareTerminalInputPayload(
|
|
String data, {
|
|
required TerminalInputSource source,
|
|
required bool isMobileOrWebMobile,
|
|
required bool bracketedPasteMode,
|
|
required bool ctrlLocked,
|
|
required bool altLocked,
|
|
}) {
|
|
if (source == TerminalInputSource.paste) {
|
|
return terminalPastePayload(
|
|
data,
|
|
bracketedPasteMode: bracketedPasteMode,
|
|
);
|
|
}
|
|
|
|
var result = data;
|
|
if (isMobileOrWebMobile && result == '\n') {
|
|
result = '\r';
|
|
}
|
|
if ((ctrlLocked || altLocked) && shouldApplyTerminalInputModifiers(result)) {
|
|
result = applyTerminalInputModifiers(
|
|
result,
|
|
ctrlLocked: ctrlLocked,
|
|
altLocked: altLocked,
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
|
///
|
|
/// xterm already handles hardware Ctrl/Cmd+V correctly in the common case. Only
|
|
/// intercept while a virtual Ctrl/Alt lock is active, because xterm can emit a
|
|
/// one-character paste as normal text when bracketed paste mode is disabled.
|
|
bool shouldHandleTerminalPasteShortcut({
|
|
required LogicalKeyboardKey logicalKey,
|
|
required bool isKeyDown,
|
|
required bool isKeyRepeat,
|
|
required bool controlPressed,
|
|
required bool metaPressed,
|
|
required bool altPressed,
|
|
required bool shiftPressed,
|
|
required bool modifierLockActive,
|
|
}) {
|
|
if (!modifierLockActive) return false;
|
|
if (!isKeyDown && !isKeyRepeat) return false;
|
|
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
|
if (altPressed || shiftPressed) return false;
|
|
return controlPressed != metaPressed;
|
|
}
|
|
|
|
/// Returns true when collapsing Row3 should also clear hidden modifier state.
|
|
bool shouldClearTerminalModifiersWhenRow3Collapses({
|
|
required bool wasExpanded,
|
|
required bool willExpand,
|
|
required bool ctrlLocked,
|
|
required bool altLocked,
|
|
}) {
|
|
return wasExpanded && !willExpand && (ctrlLocked || altLocked);
|
|
}
|
|
|
|
String _applyTerminalCtrlModifier(String data) {
|
|
// Ctrl mappings are defined only for ASCII scalars. A visible character can
|
|
// be multiple scalars (for example, a decomposed accent), so leave those
|
|
// graphemes untouched instead of rewriting only their ASCII base letter.
|
|
final graphemes = data.characters.toList(growable: false);
|
|
if (graphemes.length != 1) {
|
|
return data;
|
|
}
|
|
|
|
final runes = graphemes.single.runes.toList(growable: false);
|
|
if (runes.length != 1) {
|
|
return data;
|
|
}
|
|
|
|
final code = runes.single;
|
|
if (code >= 0x61 && code <= 0x7A) {
|
|
return String.fromCharCode(code - 0x60);
|
|
}
|
|
if (code >= 0x41 && code <= 0x5A) {
|
|
return String.fromCharCode(code - 0x40);
|
|
}
|
|
if (code == 0x20) {
|
|
return String.fromCharCode(0);
|
|
}
|
|
if (code == 0x5B) {
|
|
return String.fromCharCode(27);
|
|
}
|
|
if (code == 0x5C) {
|
|
return String.fromCharCode(28);
|
|
}
|
|
if (code == 0x5D) {
|
|
return String.fromCharCode(29);
|
|
}
|
|
if (code == 0x5E) {
|
|
return String.fromCharCode(30);
|
|
}
|
|
if (code == 0x5F || code == 0x2F) {
|
|
return String.fromCharCode(31);
|
|
}
|
|
return data;
|
|
}
|