Restore terminal soft keyboard focus
This commit is contained in:
parent
62908440e1
commit
574ce79f2d
@ -94,7 +94,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
||||
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<TerminalPage>
|
||||
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<TerminalPage>
|
||||
'';
|
||||
|
||||
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) {
|
||||
|
||||
@ -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<TerminalView>(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([
|
||||
|
||||
Loading…
Reference in New Issue
Block a user