mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-07 13:01:11 +03: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>
602 lines
20 KiB
Dart
602 lines
20 KiB
Dart
import 'dart:async';
|
||
import 'dart:math';
|
||
import 'package:flutter/gestures.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_hbb/common.dart';
|
||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||
import 'package:flutter_hbb/models/input_modifier_utils.dart';
|
||
import 'package:flutter_hbb/models/model.dart';
|
||
import 'package:flutter_hbb/models/platform_model.dart';
|
||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:xterm/xterm.dart';
|
||
import '../../desktop/pages/terminal_connection_manager.dart';
|
||
import '../../consts.dart';
|
||
|
||
class TerminalPage extends StatefulWidget {
|
||
const TerminalPage({
|
||
Key? key,
|
||
required this.id,
|
||
required this.password,
|
||
required this.isSharedPassword,
|
||
this.forceRelay,
|
||
this.connToken,
|
||
}) : super(key: key);
|
||
final String id;
|
||
final String? password;
|
||
final bool? forceRelay;
|
||
final bool? isSharedPassword;
|
||
final String? connToken;
|
||
final terminalId = 0;
|
||
|
||
@override
|
||
State<TerminalPage> createState() => _TerminalPageState();
|
||
}
|
||
|
||
class _TerminalPageState extends State<TerminalPage>
|
||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||
late FFI _ffi;
|
||
late TerminalModel _terminalModel;
|
||
double? _cellHeight;
|
||
double _sysKeyboardHeight = 0;
|
||
Timer? _keyboardDebounce;
|
||
final GlobalKey _keyboardKey = GlobalKey();
|
||
double _keyboardHeight = 0;
|
||
late bool _showTerminalExtraKeys;
|
||
// Ctrl lock state for virtual keyboard: active key presses are mapped to control codes
|
||
bool _ctrlLocked = false;
|
||
bool _altLocked = false;
|
||
// Row3 expand/collapse state for compact keyboard layout
|
||
bool _row3Expanded = false;
|
||
// For iOS edge swipe gesture
|
||
double _swipeStartX = 0;
|
||
double _swipeCurrentX = 0;
|
||
|
||
// For web only.
|
||
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
|
||
final String _robotoMonoFontFamily = isWeb
|
||
? (GoogleFonts.robotoMono().fontFamily ?? 'monospace')
|
||
: 'monospace';
|
||
|
||
SessionID get sessionId => _ffi.sessionId;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
|
||
debugPrint(
|
||
'[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}');
|
||
|
||
// Use shared FFI instance from connection manager
|
||
_ffi = TerminalConnectionManager.getConnection(
|
||
peerId: widget.id,
|
||
password: widget.password,
|
||
isSharedPassword: widget.isSharedPassword,
|
||
forceRelay: widget.forceRelay,
|
||
connToken: widget.connToken,
|
||
);
|
||
|
||
// Create terminal model with specific terminal ID
|
||
_terminalModel = TerminalModel(_ffi, widget.terminalId);
|
||
debugPrint(
|
||
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
|
||
|
||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||
_cellHeight = ph * 1.0;
|
||
};
|
||
|
||
// Register this terminal model with FFI for event routing
|
||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||
|
||
// Auto-close connection when shell exits
|
||
_terminalModel.onClosed = () {
|
||
if (mounted) {
|
||
closeConnection(id: widget.id);
|
||
}
|
||
};
|
||
|
||
// Web desktop users have full hardware keyboard access, so the on-screen
|
||
// terminal extra keys bar is unnecessary and disabled.
|
||
_showTerminalExtraKeys = !isWebDesktop &&
|
||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||
_terminalModel.isCtrlLocked = () => _ctrlLocked;
|
||
_terminalModel.clearCtrlLock = () {
|
||
if (_ctrlLocked) setState(() => _ctrlLocked = false);
|
||
};
|
||
_terminalModel.isAltLocked = () => _altLocked;
|
||
_terminalModel.clearAltLock = () {
|
||
if (_altLocked) setState(() => _altLocked = false);
|
||
};
|
||
// Load Row3 expand/collapse state from persistent storage. The raw option
|
||
// read keeps Row3 collapsed when no value has been saved yet.
|
||
_row3Expanded =
|
||
bind.mainGetLocalOption(key: kOptionShowTerminalCtrlKeys) == 'Y';
|
||
// Initialize terminal connection
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_ffi.dialogManager
|
||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||
|
||
if (_showTerminalExtraKeys) {
|
||
_updateKeyboardHeight();
|
||
}
|
||
});
|
||
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
// Unregister terminal model from FFI
|
||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||
_terminalModel.dispose();
|
||
_keyboardDebounce?.cancel();
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
super.dispose();
|
||
TerminalConnectionManager.releaseConnection(widget.id);
|
||
}
|
||
|
||
@override
|
||
void didChangeMetrics() {
|
||
super.didChangeMetrics();
|
||
|
||
_keyboardDebounce?.cancel();
|
||
_keyboardDebounce = Timer(const Duration(milliseconds: 20), () {
|
||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||
setState(() {
|
||
_sysKeyboardHeight = bottomInset;
|
||
});
|
||
});
|
||
}
|
||
|
||
void _updateKeyboardHeight() {
|
||
if (_keyboardKey.currentContext != null) {
|
||
final renderBox = _keyboardKey.currentContext!.findRenderObject() as RenderBox;
|
||
_keyboardHeight = renderBox.size.height;
|
||
}
|
||
}
|
||
|
||
EdgeInsets _calculatePadding(double heightPx) {
|
||
if (_cellHeight == null) {
|
||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||
}
|
||
final realHeight = heightPx - _sysKeyboardHeight - _keyboardHeight;
|
||
final rows = (realHeight / _cellHeight!).floor();
|
||
final extraSpace = realHeight - rows * _cellHeight!;
|
||
final topBottom = max(0.0, extraSpace / 2.0);
|
||
return EdgeInsets.only(left: 5.0, right: 5.0, top: topBottom, bottom: topBottom + _sysKeyboardHeight + _keyboardHeight);
|
||
}
|
||
|
||
/// Pastes clipboard text through TerminalModel so keyboard-only modifiers and
|
||
/// mobile Enter normalization never alter clipboard data.
|
||
Future<void> _pasteClipboardText() async {
|
||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||
final text = data?.text;
|
||
if (text == null || !mounted) return;
|
||
|
||
await _terminalModel.pasteText(text);
|
||
if (mounted) {
|
||
_terminalModel.terminalController.clearSelection();
|
||
}
|
||
}
|
||
|
||
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
||
final hardwareKeyboard = HardwareKeyboard.instance;
|
||
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
||
logicalKey: event.logicalKey,
|
||
isKeyDown: event is KeyDownEvent,
|
||
isKeyRepeat: event is KeyRepeatEvent,
|
||
controlPressed: hardwareKeyboard.isControlPressed,
|
||
metaPressed: hardwareKeyboard.isMetaPressed,
|
||
altPressed: hardwareKeyboard.isAltPressed,
|
||
shiftPressed: hardwareKeyboard.isShiftPressed,
|
||
modifierLockActive: _ctrlLocked || _altLocked,
|
||
);
|
||
if (!shouldPaste) return KeyEventResult.ignored;
|
||
|
||
// Only locked virtual modifiers need interception. Without a lock, keep
|
||
// xterm's default hardware paste behavior, including bracketed paste mode.
|
||
unawaited(_pasteClipboardText());
|
||
return KeyEventResult.handled;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
super.build(context);
|
||
return WillPopScope(
|
||
onWillPop: () async {
|
||
clientClose(sessionId, _ffi);
|
||
return false; // Prevent default back behavior
|
||
},
|
||
child: buildBody(),
|
||
);
|
||
}
|
||
|
||
Widget buildBody() {
|
||
final scaffold = Scaffold(
|
||
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
|
||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||
body: Stack(
|
||
children: [
|
||
Positioned.fill(
|
||
child: SafeArea(
|
||
top: true,
|
||
child: LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final heightPx = constraints.maxHeight;
|
||
return TerminalView(
|
||
_terminalModel.terminal,
|
||
controller: _terminalModel.terminalController,
|
||
autofocus: true,
|
||
textStyle: _getTerminalStyle(),
|
||
backgroundOpacity: 0.7,
|
||
// The following comment is from xterm.dart source code:
|
||
// Workaround to detect delete key for platforms and IMEs that do not
|
||
// emit a hardware delete event. Preferred on mobile platforms. [false] by
|
||
// default.
|
||
//
|
||
// Android works fine without this workaround.
|
||
deleteDetection: isIOS,
|
||
onKeyEvent: _handleTerminalKeyEvent,
|
||
padding: _calculatePadding(heightPx),
|
||
onSecondaryTapDown: (details, offset) async {
|
||
final selection = _terminalModel.terminalController.selection;
|
||
if (selection != null) {
|
||
final text = _terminalModel.terminal.buffer.getText(selection);
|
||
_terminalModel.terminalController.clearSelection();
|
||
await Clipboard.setData(ClipboardData(text: text));
|
||
} else {
|
||
await _pasteClipboardText();
|
||
}
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
|
||
// iOS-style circular close button in top-right corner
|
||
if (isIOS) _buildCloseButton(),
|
||
],
|
||
),
|
||
);
|
||
|
||
// Add iOS edge swipe gesture to exit (similar to Android back button)
|
||
if (isIOS) {
|
||
return LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final screenWidth = constraints.maxWidth;
|
||
// Base thresholds on screen width but clamp to reasonable logical pixel ranges
|
||
// Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels
|
||
final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0);
|
||
// Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels
|
||
final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0);
|
||
|
||
return RawGestureDetector(
|
||
behavior: HitTestBehavior.translucent,
|
||
gestures: <Type, GestureRecognizerFactory>{
|
||
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
|
||
() => HorizontalDragGestureRecognizer(
|
||
debugOwner: this,
|
||
// Only respond to touch input, exclude mouse/trackpad
|
||
supportedDevices: kTouchBasedDeviceKinds,
|
||
),
|
||
(HorizontalDragGestureRecognizer instance) {
|
||
instance
|
||
// Capture initial touch-down position (before touch slop)
|
||
..onDown = (details) {
|
||
_swipeStartX = details.localPosition.dx;
|
||
_swipeCurrentX = details.localPosition.dx;
|
||
}
|
||
..onUpdate = (details) {
|
||
_swipeCurrentX = details.localPosition.dx;
|
||
}
|
||
..onEnd = (details) {
|
||
// Check if swipe started from left edge and moved right
|
||
if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) {
|
||
clientClose(sessionId, _ffi);
|
||
}
|
||
_swipeStartX = 0;
|
||
_swipeCurrentX = 0;
|
||
}
|
||
..onCancel = () {
|
||
_swipeStartX = 0;
|
||
_swipeCurrentX = 0;
|
||
};
|
||
},
|
||
),
|
||
},
|
||
child: scaffold,
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
return scaffold;
|
||
}
|
||
|
||
Widget _buildCloseButton() {
|
||
return Positioned(
|
||
top: 0,
|
||
right: 0,
|
||
child: SafeArea(
|
||
minimum: const EdgeInsets.only(
|
||
top: 16, // iOS standard margin
|
||
right: 16, // iOS standard margin
|
||
),
|
||
child: Semantics(
|
||
button: true,
|
||
label: translate('Close'),
|
||
child: Container(
|
||
width: 44, // iOS standard tap target size
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: Colors.black.withOpacity(0.5), // Half transparency
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
shape: const CircleBorder(),
|
||
clipBehavior: Clip.antiAlias,
|
||
child: InkWell(
|
||
customBorder: const CircleBorder(),
|
||
onTap: () {
|
||
clientClose(sessionId, _ffi);
|
||
},
|
||
child: Tooltip(
|
||
message: translate('Close'),
|
||
child: const Icon(
|
||
Icons.chevron_left, // iOS-style back arrow
|
||
color: Colors.white,
|
||
size: 28,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildFloatingKeyboard() {
|
||
return AnimatedPositioned(
|
||
duration: const Duration(milliseconds: 200),
|
||
left: 0,
|
||
right: 0,
|
||
bottom: _sysKeyboardHeight,
|
||
child: Container(
|
||
key: _keyboardKey,
|
||
color: Theme.of(context).scaffoldBackgroundColor,
|
||
padding: EdgeInsets.zero,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
// Row 1 follows the latest reviewed PR layout.
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: _buildKeyboardKeyButtons(terminalKeyboardRow1Keys),
|
||
),
|
||
// Row 2 ends with the full-width Row3 collapse/expand toggle.
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
..._buildKeyboardKeyButtons(terminalKeyboardRow2Keys),
|
||
const SizedBox(width: terminalKeyboardKeySpacing),
|
||
_buildCollapseButton(),
|
||
],
|
||
),
|
||
// Row 3 restores paging keys and trailing alignment placeholders.
|
||
if (_row3Expanded)
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
..._buildKeyboardKeyButtons(terminalKeyboardRow3Keys),
|
||
for (var i = 0;
|
||
i < terminalKeyboardRow3TrailingPlaceholderCount;
|
||
i++) ...[
|
||
const SizedBox(width: terminalKeyboardKeySpacing),
|
||
const SizedBox(width: terminalKeyboardKeyWidth),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Ctrl toggle button with highlighted locked state
|
||
Widget _buildCtrlKeyButton() {
|
||
return _buildModifierToggleButton(
|
||
text: 'Ctrl',
|
||
semanticsLabel: 'Ctrl',
|
||
isLocked: _ctrlLocked,
|
||
onPressed: () => setState(() => _ctrlLocked = !_ctrlLocked),
|
||
);
|
||
}
|
||
|
||
// Alt toggle button with highlighted locked state
|
||
Widget _buildAltKeyButton() {
|
||
return _buildModifierToggleButton(
|
||
text: 'Alt',
|
||
semanticsLabel: 'Alt',
|
||
isLocked: _altLocked,
|
||
onPressed: () => setState(() => _altLocked = !_altLocked),
|
||
);
|
||
}
|
||
|
||
// Collapse/expand toggle button for Row3
|
||
void _toggleRow3Expanded() {
|
||
final willExpand = !_row3Expanded;
|
||
final shouldClearModifiers = shouldClearTerminalModifiersWhenRow3Collapses(
|
||
wasExpanded: _row3Expanded,
|
||
willExpand: willExpand,
|
||
ctrlLocked: _ctrlLocked,
|
||
altLocked: _altLocked,
|
||
);
|
||
setState(() {
|
||
_row3Expanded = willExpand;
|
||
if (shouldClearModifiers) {
|
||
_ctrlLocked = false;
|
||
_altLocked = false;
|
||
}
|
||
});
|
||
mainSetLocalBoolOption(kOptionShowTerminalCtrlKeys, willExpand);
|
||
|
||
// The floating keyboard height changes after Row3 is inserted/removed.
|
||
// Re-measure on the next frame so terminal padding uses the new height.
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted || !_showTerminalExtraKeys) return;
|
||
setState(() {
|
||
_updateKeyboardHeight();
|
||
});
|
||
});
|
||
}
|
||
|
||
Widget _buildCollapseButton() {
|
||
return Semantics(
|
||
label: translate('Show terminal extra keys'),
|
||
toggled: _row3Expanded,
|
||
child: ElevatedButton(
|
||
onPressed: _toggleRow3Expanded,
|
||
child: Text(_row3Expanded ? '∧' : '∨'),
|
||
style: ElevatedButton.styleFrom(
|
||
minimumSize: const Size(terminalKeyboardKeyWidth, 32),
|
||
padding: EdgeInsets.zero,
|
||
textStyle: const TextStyle(fontSize: 12),
|
||
backgroundColor:
|
||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Builds a fixed-width key sequence with the reviewed 2dp spacing.
|
||
List<Widget> _buildKeyboardKeyButtons(List<String> labels) {
|
||
return [
|
||
for (var i = 0; i < labels.length; i++) ...[
|
||
_buildKeyButton(labels[i]),
|
||
if (i < labels.length - 1)
|
||
const SizedBox(width: terminalKeyboardKeySpacing),
|
||
],
|
||
];
|
||
}
|
||
|
||
/// Build a modifier toggle button (Ctrl/Alt) with one-shot behavior.
|
||
/// When [isLocked] is true, the button highlights in blue and the next
|
||
/// single-character input is mapped to its modified equivalent.
|
||
Widget _buildModifierToggleButton({
|
||
required String text,
|
||
required String semanticsLabel,
|
||
required bool isLocked,
|
||
required VoidCallback onPressed,
|
||
}) {
|
||
return Semantics(
|
||
// Ctrl and Alt are technical key names and intentionally stay unchanged.
|
||
label: semanticsLabel,
|
||
toggled: isLocked,
|
||
child: ElevatedButton(
|
||
onPressed: onPressed,
|
||
child: Text(text),
|
||
style: ElevatedButton.styleFrom(
|
||
minimumSize: const Size(terminalKeyboardKeyWidth, 32),
|
||
padding: EdgeInsets.zero,
|
||
textStyle: const TextStyle(fontSize: 12),
|
||
backgroundColor: isLocked
|
||
? Colors.blue
|
||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
foregroundColor: isLocked
|
||
? Colors.white
|
||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildKeyButton(String label) {
|
||
if (label == 'Ctrl') return _buildCtrlKeyButton();
|
||
if (label == 'Alt') return _buildAltKeyButton();
|
||
|
||
return ElevatedButton(
|
||
onPressed: () {
|
||
_sendKeyToTerminal(label);
|
||
},
|
||
child: Text(label),
|
||
style: ElevatedButton.styleFrom(
|
||
minimumSize: const Size(terminalKeyboardKeyWidth, 32),
|
||
padding: EdgeInsets.zero,
|
||
textStyle: const TextStyle(fontSize: 12),
|
||
backgroundColor:
|
||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
);
|
||
}
|
||
|
||
void _sendKeyToTerminal(String key) {
|
||
String send;
|
||
|
||
switch (key) {
|
||
case 'Esc':
|
||
send = '\x1B';
|
||
break;
|
||
case 'Tab':
|
||
send = '\t';
|
||
break;
|
||
case 'Ctrl+C':
|
||
send = '\x03';
|
||
break;
|
||
|
||
case '↑':
|
||
send = '\x1B[A';
|
||
break;
|
||
case '↓':
|
||
send = '\x1B[B';
|
||
break;
|
||
case '→':
|
||
send = '\x1B[C';
|
||
break;
|
||
case '←':
|
||
send = '\x1B[D';
|
||
break;
|
||
|
||
case 'Home':
|
||
send = '\x1B[H';
|
||
break;
|
||
case 'End':
|
||
send = '\x1B[F';
|
||
break;
|
||
case 'PgUp':
|
||
send = '\x1B[5~';
|
||
break;
|
||
case 'PgDn':
|
||
send = '\x1B[6~';
|
||
break;
|
||
|
||
default:
|
||
send = key;
|
||
break;
|
||
}
|
||
|
||
_terminalModel.sendVirtualKey(send);
|
||
}
|
||
|
||
// https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472
|
||
// https://github.com/TerminalStudio/xterm.dart/issues/198#issuecomment-2526548458
|
||
TerminalStyle _getTerminalStyle() {
|
||
return isWeb
|
||
? TerminalStyle(
|
||
fontFamily: _robotoMonoFontFamily,
|
||
fontSize: 14,
|
||
)
|
||
: const TerminalStyle();
|
||
}
|
||
|
||
@override
|
||
bool get wantKeepAlive => true;
|
||
}
|