From 574ce79f2d591a9f21da5ef0ceede01093b5a3a5 Mon Sep 17 00:00:00 2001 From: sladro Date: Mon, 6 Apr 2026 09:18:53 +0800 Subject: [PATCH] Restore terminal soft keyboard focus --- .../lib/features/terminal/terminal_page.dart | 232 +++++++++--------- .../terminal/terminal_page_input_test.dart | 16 +- 2 files changed, 126 insertions(+), 122 deletions(-) diff --git a/apps/mobile_app/lib/features/terminal/terminal_page.dart b/apps/mobile_app/lib/features/terminal/terminal_page.dart index c0372b3..15f9f96 100644 --- a/apps/mobile_app/lib/features/terminal/terminal_page.dart +++ b/apps/mobile_app/lib/features/terminal/terminal_page.dart @@ -94,7 +94,7 @@ class _TerminalPageState extends ConsumerState final TerminalInteractionController controller = TerminalInteractionController(); final TerminalDiagnosticLog _diagnosticLog = TerminalDiagnosticLog(); - final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false); + final FocusNode _terminalFocusNode = FocusNode(); final ScrollController _terminalScrollController = ScrollController(); late final TerminalSessionCoordinator _coordinator; late final Listenable _pageStateListenable; @@ -236,11 +236,7 @@ class _TerminalPageState extends ConsumerState return; } - _sendTerminalInput( - '\r', - diagnosticEvent: 'ui.input.send', - detail: r'\r', - ); + _sendTerminalInput('\r', diagnosticEvent: 'ui.input.send', detail: r'\r'); } void _sendQuickKey(_QuickTerminalKey quickKey) { @@ -599,130 +595,128 @@ class _TerminalPageState extends ConsumerState ''; return Scaffold( - appBar: AppBar( - toolbarHeight: 44, - titleSpacing: 0, - title: Container( - key: const Key('terminal_header_panel'), - padding: const EdgeInsets.symmetric(vertical: 2), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ + appBar: AppBar( + toolbarHeight: 44, + titleSpacing: 0, + title: Container( + key: const Key('terminal_header_panel'), + padding: const EdgeInsets.symmetric(vertical: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.session.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + ), + if (!isTight) Text( - widget.session.name, + workingDirectory, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleSmall, + style: Theme.of(context).textTheme.bodySmall, ), - if (!isTight) - Text( - workingDirectory, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), + ], ), - actions: [ - AnimatedBuilder( - animation: controller, - builder: (context, _) { - final mode = controller.isFollowingLiveOutput - ? 'Live' - : 'Scrollback'; - final modeLabel = isCompact - ? mode - : '$mode | ${controller.liveLines.length} lines'; - final statusLabel = isCompact - ? _compactStatusLabel - : _statusLabel; + ), + actions: [ + AnimatedBuilder( + animation: controller, + builder: (context, _) { + final mode = controller.isFollowingLiveOutput + ? 'Live' + : 'Scrollback'; + final modeLabel = isCompact + ? mode + : '$mode | ${controller.liveLines.length} lines'; + final statusLabel = isCompact + ? _compactStatusLabel + : _statusLabel; - return Row( - key: const Key('terminal_status_summary'), - mainAxisSize: MainAxisSize.min, - children: [ - TextButton( - onPressed: controller.isFollowingLiveOutput - ? controller.enterScrollback - : controller.jumpToLive, - style: TextButton.styleFrom( - foregroundColor: const Color(0xFFD8C4A0), - minimumSize: const Size(0, 32), - padding: const EdgeInsets.symmetric(horizontal: 8), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - ), - child: Text(modeLabel), - ), - SizedBox(width: isCompact ? 2 : 4), - Padding( - padding: EdgeInsets.only(right: isCompact ? 4 : 8), - child: Center( - child: StatusPill( - label: statusLabel, - icon: _statusIcon, - color: _statusColor(context), - ), - ), - ), - IconButton( - key: const Key('terminal_actions_button'), - onPressed: _showActionsSheet, + return Row( + key: const Key('terminal_status_summary'), + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: controller.isFollowingLiveOutput + ? controller.enterScrollback + : controller.jumpToLive, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFD8C4A0), + minimumSize: const Size(0, 32), + padding: const EdgeInsets.symmetric(horizontal: 8), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, - icon: const Icon(Icons.tune), - tooltip: 'Show actions', ), - ], - ); - }, + child: Text(modeLabel), + ), + SizedBox(width: isCompact ? 2 : 4), + Padding( + padding: EdgeInsets.only(right: isCompact ? 4 : 8), + child: Center( + child: StatusPill( + label: statusLabel, + icon: _statusIcon, + color: _statusColor(context), + ), + ), + ), + IconButton( + key: const Key('terminal_actions_button'), + onPressed: _showActionsSheet, + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.tune), + tooltip: 'Show actions', + ), + ], + ); + }, + ), + ], + ), + body: SafeArea( + top: false, + minimum: AppTheme.pagePadding, + child: Column( + children: [ + _buildScrollbackSection(context, isCompact), + Expanded( + child: Container( + key: const Key('terminal_surface_panel'), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.surfaceContainerHighest, + const Color(0xFF090B0E), + ], + ), + borderRadius: BorderRadius.zero, + border: Border.all(color: const Color(0xFF332B22)), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + child: TerminalView( + terminal, + focusNode: _terminalFocusNode, + autofocus: false, + scrollController: _terminalScrollController, + ), + ), + ), ), + const SizedBox(height: 6), + _buildCommandDeck(context, isCompact), ], ), - body: SafeArea( - top: false, - minimum: AppTheme.pagePadding, - child: Column( - children: [ - _buildScrollbackSection(context, isCompact), - Expanded( - child: Container( - key: const Key('terminal_surface_panel'), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - const Color(0xFF090B0E), - ], - ), - borderRadius: BorderRadius.zero, - border: Border.all(color: const Color(0xFF332B22)), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - child: TerminalView( - terminal, - focusNode: _terminalFocusNode, - autofocus: false, - scrollController: _terminalScrollController, - ), - ), - ), - ), - const SizedBox(height: 6), - _buildCommandDeck(context, isCompact), - ], - ), - ), - ); + ), + ); } Widget _buildScrollbackSection(BuildContext context, bool isCompact) { diff --git a/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart b/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart index af57a87..2c3b46e 100644 --- a/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart +++ b/apps/mobile_app/test/features/terminal/terminal_page_input_test.dart @@ -11,6 +11,7 @@ import 'package:term_remote_ctl/features/presets/preset_repository.dart'; import 'package:term_remote_ctl/features/terminal/terminal_page.dart'; import 'package:term_remote_ctl/features/terminal/terminal_socket_session.dart'; import 'package:term_remote_ctl/features/sessions/session.dart'; +import 'package:xterm/xterm.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -24,6 +25,17 @@ void main() { expect(find.byKey(const Key('terminal_send_button')), findsOneWidget); }); + testWidgets('terminal view remains focusable for soft keyboard input', ( + tester, + ) async { + await _pumpTerminalPage(tester); + + final terminalView = tester.widget(find.byType(TerminalView)); + + expect(terminalView.focusNode, isNotNull); + expect(terminalView.focusNode!.canRequestFocus, isTrue); + }); + testWidgets('terminal actions sheet unifies session actions and quick keys', ( tester, ) async { @@ -49,9 +61,7 @@ void main() { ); }); - testWidgets('terminal actions sheet opens preset management', ( - tester, - ) async { + testWidgets('terminal actions sheet opens preset management', (tester) async { await _pumpTerminalPage( tester, presetRepository: _MemoryPresetRepository([