mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-20 15:55:08 +08:00
Compare commits
3 Commits
display_na
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
483fe80308 | ||
|
|
34ceeac36e | ||
|
|
20f11018ce |
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
@@ -15,6 +16,7 @@ class TerminalPage extends StatefulWidget {
|
||||
required this.tabController,
|
||||
required this.isSharedPassword,
|
||||
required this.terminalId,
|
||||
required this.tabKey,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
}) : super(key: key);
|
||||
@@ -25,6 +27,8 @@ class TerminalPage extends StatefulWidget {
|
||||
final bool? isSharedPassword;
|
||||
final String? connToken;
|
||||
final int terminalId;
|
||||
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
||||
final String tabKey;
|
||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||
|
||||
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
|
||||
@@ -42,11 +46,16 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
late FFI _ffi;
|
||||
late TerminalModel _terminalModel;
|
||||
double? _cellHeight;
|
||||
final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false);
|
||||
StreamSubscription<DesktopTabState>? _tabStateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Listen for tab selection changes to request focus
|
||||
_tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged);
|
||||
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
@@ -64,6 +73,13 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
_terminalModel.onResizeExternal = (w, h, pw, ph) {
|
||||
_cellHeight = ph * 1.0;
|
||||
|
||||
// Enable focus once terminal has valid dimensions (first valid resize)
|
||||
if (!_terminalFocusNode.canRequestFocus && w > 0 && h > 0) {
|
||||
_terminalFocusNode.canRequestFocus = true;
|
||||
// Auto-focus if this tab is currently selected
|
||||
_requestFocusIfSelected();
|
||||
}
|
||||
|
||||
// Schedule the setState for the next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
@@ -99,14 +115,42 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Cancel tab state subscription to prevent memory leak
|
||||
_tabStateSubscription?.cancel();
|
||||
// Unregister terminal model from FFI
|
||||
_ffi.unregisterTerminalModel(widget.terminalId);
|
||||
_terminalModel.dispose();
|
||||
_terminalFocusNode.dispose();
|
||||
// Release connection reference instead of closing directly
|
||||
TerminalConnectionManager.releaseConnection(widget.id);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabStateChanged(DesktopTabState state) {
|
||||
// Check if this tab is now selected and request focus
|
||||
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||
final selectedTab = state.tabs[state.selected];
|
||||
if (selectedTab.key == widget.tabKey && mounted) {
|
||||
_requestFocusIfSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _requestFocusIfSelected() {
|
||||
if (!mounted || !_terminalFocusNode.canRequestFocus) return;
|
||||
// Use post-frame callback to ensure widget is fully laid out in focus tree
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Re-check conditions after frame: mounted, focusable, still selected, not already focused
|
||||
if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return;
|
||||
final state = widget.tabController.state.value;
|
||||
if (state.selected >= 0 && state.selected < state.tabs.length) {
|
||||
if (state.tabs[state.selected].key == widget.tabKey) {
|
||||
_terminalFocusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This method ensures that the number of visible rows is an integer by computing the
|
||||
// extra space left after dividing the available height by the height of a single
|
||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||
@@ -131,7 +175,9 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
return TerminalView(
|
||||
_terminalModel.terminal,
|
||||
controller: _terminalModel.terminalController,
|
||||
autofocus: true,
|
||||
focusNode: _terminalFocusNode,
|
||||
// Note: autofocus is not used here because focus is managed manually
|
||||
// via _onTabStateChanged() to handle tab switching properly.
|
||||
backgroundOpacity: 0.7,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
|
||||
@@ -92,6 +92,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
key: ValueKey(tabKey),
|
||||
id: peerId,
|
||||
terminalId: terminalId,
|
||||
tabKey: tabKey,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
tabController: tabController,
|
||||
@@ -194,7 +195,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
assert(call.arguments is String,
|
||||
"Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
|
||||
if (currentTab.key.startsWith(call.arguments)) {
|
||||
// Use lastIndexOf to handle peerIds containing underscores
|
||||
final lastUnderscore = currentTab.key.lastIndexOf('_');
|
||||
if (lastUnderscore > 0 &&
|
||||
currentTab.key.substring(0, lastUnderscore) == call.arguments) {
|
||||
windowOnTop(windowId());
|
||||
return true;
|
||||
}
|
||||
@@ -329,7 +333,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
void _addNewTerminal(String peerId, {int? terminalId}) {
|
||||
// Find first tab for this peer to get connection parameters
|
||||
final firstTab = tabController.state.value.tabs.firstWhere(
|
||||
(tab) => tab.key.startsWith('$peerId\_'),
|
||||
(tab) {
|
||||
final last = tab.key.lastIndexOf('_');
|
||||
return last > 0 && tab.key.substring(0, last) == peerId;
|
||||
},
|
||||
);
|
||||
if (firstTab.page is TerminalPage) {
|
||||
final page = firstTab.page as TerminalPage;
|
||||
@@ -350,9 +357,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
|
||||
void _addNewTerminalForCurrentPeer({int? terminalId}) {
|
||||
final currentTab = tabController.state.value.selectedTabInfo;
|
||||
final parts = currentTab.key.split('_');
|
||||
if (parts.isNotEmpty) {
|
||||
final peerId = parts[0];
|
||||
final tabKey = currentTab.key;
|
||||
final lastUnderscore = tabKey.lastIndexOf('_');
|
||||
if (lastUnderscore > 0) {
|
||||
final peerId = tabKey.substring(0, lastUnderscore);
|
||||
_addNewTerminal(peerId, terminalId: terminalId);
|
||||
}
|
||||
}
|
||||
@@ -369,9 +377,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
labelGetter: DesktopTab.tablabelGetter,
|
||||
tabMenuBuilder: (key) {
|
||||
// Extract peerId from tab key (format: "peerId_terminalId")
|
||||
final parts = key.split('_');
|
||||
if (parts.isEmpty) return Container();
|
||||
final peerId = parts[0];
|
||||
// Use lastIndexOf to handle peerIds containing underscores
|
||||
final lastUnderscore = key.lastIndexOf('_');
|
||||
if (lastUnderscore <= 0) return Container();
|
||||
final peerId = key.substring(0, lastUnderscore);
|
||||
return _tabMenuBuilder(peerId, () {});
|
||||
},
|
||||
));
|
||||
|
||||
@@ -24,6 +24,13 @@ class TerminalModel with ChangeNotifier {
|
||||
bool _disposed = false;
|
||||
|
||||
final _inputBuffer = <String>[];
|
||||
// Buffer for output data received before terminal view has valid dimensions.
|
||||
// This prevents NaN errors when writing to terminal before layout is complete.
|
||||
final _pendingOutputChunks = <String>[];
|
||||
int _pendingOutputSize = 0;
|
||||
static const int _kMaxOutputBufferChars = 8 * 1024;
|
||||
// View ready state: true when terminal has valid dimensions, safe to write
|
||||
bool _terminalViewReady = false;
|
||||
|
||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
||||
|
||||
@@ -74,6 +81,12 @@ class TerminalModel with ChangeNotifier {
|
||||
// This piece of code must be placed before the conditional check in order to initialize properly.
|
||||
onResizeExternal?.call(w, h, pw, ph);
|
||||
|
||||
// Mark terminal view as ready and flush any buffered output on first valid resize.
|
||||
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
|
||||
if (!_terminalViewReady) {
|
||||
_markViewReady();
|
||||
}
|
||||
|
||||
if (_terminalOpened) {
|
||||
// Notify remote terminal of resize
|
||||
try {
|
||||
@@ -141,7 +154,7 @@ class TerminalModel with ChangeNotifier {
|
||||
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
|
||||
// Optionally show error to user
|
||||
if (e is TimeoutException) {
|
||||
terminal.write('Failed to open terminal: Connection timeout\r\n');
|
||||
_writeToTerminal('Failed to open terminal: Connection timeout\r\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,7 +296,7 @@ class TerminalModel with ChangeNotifier {
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
terminal.write('Failed to open terminal: $message\r\n');
|
||||
_writeToTerminal('Failed to open terminal: $message\r\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,29 +340,83 @@ class TerminalModel with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
terminal.write(text);
|
||||
_writeToTerminal(text);
|
||||
} catch (e) {
|
||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write text to terminal, buffering if the view is not yet ready.
|
||||
/// All terminal output should go through this method to avoid NaN errors
|
||||
/// from writing before the terminal view has valid layout dimensions.
|
||||
void _writeToTerminal(String text) {
|
||||
if (!_terminalViewReady) {
|
||||
// If a single chunk exceeds the cap, keep only its tail.
|
||||
// Note: truncation may split a multi-byte ANSI escape sequence,
|
||||
// which can cause a brief visual glitch on flush. This is acceptable
|
||||
// because it only affects the pre-layout buffering window and the
|
||||
// terminal will self-correct on subsequent output.
|
||||
if (text.length >= _kMaxOutputBufferChars) {
|
||||
final truncated =
|
||||
text.substring(text.length - _kMaxOutputBufferChars);
|
||||
_pendingOutputChunks
|
||||
..clear()
|
||||
..add(truncated);
|
||||
_pendingOutputSize = truncated.length;
|
||||
} else {
|
||||
_pendingOutputChunks.add(text);
|
||||
_pendingOutputSize += text.length;
|
||||
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
||||
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
||||
_pendingOutputChunks.length > 1) {
|
||||
final removed = _pendingOutputChunks.removeAt(0);
|
||||
_pendingOutputSize -= removed.length;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
terminal.write(text);
|
||||
}
|
||||
|
||||
void _flushOutputBuffer() {
|
||||
if (_pendingOutputChunks.isEmpty) return;
|
||||
debugPrint(
|
||||
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
||||
for (final chunk in _pendingOutputChunks) {
|
||||
terminal.write(chunk);
|
||||
}
|
||||
_pendingOutputChunks.clear();
|
||||
_pendingOutputSize = 0;
|
||||
}
|
||||
|
||||
/// Mark terminal view as ready and flush buffered output.
|
||||
void _markViewReady() {
|
||||
if (_terminalViewReady) return;
|
||||
_terminalViewReady = true;
|
||||
_flushOutputBuffer();
|
||||
}
|
||||
|
||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||
final int exitCode = evt['exit_code'] ?? 0;
|
||||
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||
_terminalOpened = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||
final String message = evt['message'] ?? 'Unknown error';
|
||||
terminal.write('\r\nTerminal error: $message\r\n');
|
||||
_writeToTerminal('\r\nTerminal error: $message\r\n');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// Clear buffers to free memory
|
||||
_inputBuffer.clear();
|
||||
_pendingOutputChunks.clear();
|
||||
_pendingOutputSize = 0;
|
||||
// Terminal cleanup is handled server-side when service closes
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -107,9 +107,9 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||
let center_x = rect.left + (rect.right - rect.left) / 2;
|
||||
let center_y = rect.top + (rect.bottom - rect.top) / 2;
|
||||
center_x >= display.x
|
||||
&& center_x <= display.x + display.width
|
||||
&& center_x < display.x + display.width
|
||||
&& center_y >= display.y
|
||||
&& center_y <= display.y + display.height
|
||||
&& center_y < display.y + display.height
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user