feat: refine terminal input controls
This commit is contained in:
parent
e071f73490
commit
2dd5113044
@ -41,30 +41,53 @@ class TerminalPage extends ConsumerStatefulWidget {
|
|||||||
class _TerminalPageState extends ConsumerState<TerminalPage>
|
class _TerminalPageState extends ConsumerState<TerminalPage>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
static const Duration _historySeedDelay = Duration(milliseconds: 120);
|
static const Duration _historySeedDelay = Duration(milliseconds: 120);
|
||||||
static const List<_QuickTerminalKey> _symbolTerminalKeys = [
|
static const List<_QuickTerminalKey> _editingControlKeys = [
|
||||||
_QuickTerminalKey(keyId: 'symbol_at', label: '@', input: '@'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_slash', label: '/', input: '/'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_dash', label: '-', input: '-'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_underscore', label: '_', input: '_'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_dot', label: '.', input: '.'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_colon', label: ':', input: ':'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_tilde', label: '~', input: '~'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_backslash', label: r'\', input: r'\'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_pipe', label: '|', input: '|'),
|
|
||||||
_QuickTerminalKey(keyId: 'symbol_dollar', label: r'$', input: r'$'),
|
|
||||||
];
|
|
||||||
static const List<_QuickTerminalKey> _controlTerminalKeys = [
|
|
||||||
_QuickTerminalKey(keyId: 'esc', label: 'Esc', input: '\u001b'),
|
_QuickTerminalKey(keyId: 'esc', label: 'Esc', input: '\u001b'),
|
||||||
_QuickTerminalKey(keyId: 'tab', label: 'Tab', input: '\t'),
|
_QuickTerminalKey(keyId: 'tab', label: 'Tab', input: '\t'),
|
||||||
|
_QuickTerminalKey(keyId: 'enter', label: 'Enter', input: '\r'),
|
||||||
_QuickTerminalKey(keyId: 'ctrl_c', label: 'Ctrl+C', input: '\u0003'),
|
_QuickTerminalKey(keyId: 'ctrl_c', label: 'Ctrl+C', input: '\u0003'),
|
||||||
_QuickTerminalKey(keyId: 'ctrl_d', label: 'Ctrl+D', input: '\u0004'),
|
_QuickTerminalKey(keyId: 'ctrl_d', label: 'Ctrl+D', input: '\u0004'),
|
||||||
_QuickTerminalKey(keyId: 'ctrl_l', label: 'Ctrl+L', input: '\u000c'),
|
_QuickTerminalKey(keyId: 'ctrl_l', label: 'Ctrl+L', input: '\u000c'),
|
||||||
|
_QuickTerminalKey(keyId: 'ctrl_u', label: 'Ctrl+U', input: '\u0015'),
|
||||||
|
_QuickTerminalKey(keyId: 'ctrl_z', label: 'Ctrl+Z', input: '\u001a'),
|
||||||
_QuickTerminalKey(
|
_QuickTerminalKey(
|
||||||
keyId: 'backspace',
|
keyId: 'backspace',
|
||||||
label: 'Backspace',
|
label: 'Backspace',
|
||||||
input: '\x7f',
|
input: '\x7f',
|
||||||
repeatable: true,
|
repeatable: true,
|
||||||
),
|
),
|
||||||
|
_QuickTerminalKey(
|
||||||
|
keyId: 'delete',
|
||||||
|
label: 'Del',
|
||||||
|
input: '\u001b[3~',
|
||||||
|
repeatable: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
static const List<_QuickTerminalKey> _navigationKeys = [
|
||||||
|
_QuickTerminalKey(
|
||||||
|
keyId: 'home',
|
||||||
|
label: 'Home',
|
||||||
|
input: '\u001b[H',
|
||||||
|
repeatable: true,
|
||||||
|
),
|
||||||
|
_QuickTerminalKey(
|
||||||
|
keyId: 'end',
|
||||||
|
label: 'End',
|
||||||
|
input: '\u001b[F',
|
||||||
|
repeatable: true,
|
||||||
|
),
|
||||||
|
_QuickTerminalKey(
|
||||||
|
keyId: 'page_up',
|
||||||
|
label: 'PgUp',
|
||||||
|
input: '\u001b[5~',
|
||||||
|
repeatable: true,
|
||||||
|
),
|
||||||
|
_QuickTerminalKey(
|
||||||
|
keyId: 'page_down',
|
||||||
|
label: 'PgDn',
|
||||||
|
input: '\u001b[6~',
|
||||||
|
repeatable: true,
|
||||||
|
),
|
||||||
_QuickTerminalKey(
|
_QuickTerminalKey(
|
||||||
keyId: 'up',
|
keyId: 'up',
|
||||||
label: 'Up',
|
label: 'Up',
|
||||||
@ -90,6 +113,18 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
repeatable: true,
|
repeatable: true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
static const List<_QuickTerminalKey> _symbolTerminalKeys = [
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_at', label: '@', input: '@'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_slash', label: '/', input: '/'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_dash', label: '-', input: '-'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_underscore', label: '_', input: '_'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_dot', label: '.', input: '.'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_colon', label: ':', input: ':'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_tilde', label: '~', input: '~'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_backslash', label: r'\', input: r'\'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_pipe', label: '|', input: '|'),
|
||||||
|
_QuickTerminalKey(keyId: 'symbol_dollar', label: r'$', input: r'$'),
|
||||||
|
];
|
||||||
|
|
||||||
final Terminal terminal = Terminal(maxLines: 1000);
|
final Terminal terminal = Terminal(maxLines: 1000);
|
||||||
final TerminalInteractionController controller =
|
final TerminalInteractionController controller =
|
||||||
@ -107,6 +142,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
bool _awaitingAttachReplayFrame = true;
|
bool _awaitingAttachReplayFrame = true;
|
||||||
bool _awaitingReconnectRestore = false;
|
bool _awaitingReconnectRestore = false;
|
||||||
bool _shouldReconnectOnResume = false;
|
bool _shouldReconnectOnResume = false;
|
||||||
|
_TerminalInputMode _inputMode = _TerminalInputMode.read;
|
||||||
TerminalConnectionState? _lastConnectionState;
|
TerminalConnectionState? _lastConnectionState;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -138,6 +174,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
_diagnosticLog.add('ui.terminal.key', normalizedInput);
|
_diagnosticLog.add('ui.terminal.key', normalizedInput);
|
||||||
_coordinator.sendInput(normalizedInput);
|
_coordinator.sendInput(normalizedInput);
|
||||||
};
|
};
|
||||||
|
_applyInputMode();
|
||||||
unawaited(_coordinator.start());
|
unawaited(_coordinator.start());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,14 +273,6 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendEnter() {
|
|
||||||
if (!_canSendInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendTerminalInput('\r', diagnosticEvent: 'ui.input.send', detail: r'\r');
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendQuickKey(_QuickTerminalKey quickKey) {
|
void _sendQuickKey(_QuickTerminalKey quickKey) {
|
||||||
_sendTerminalInput(
|
_sendTerminalInput(
|
||||||
quickKey.input,
|
quickKey.input,
|
||||||
@ -263,11 +292,44 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
|
|
||||||
_QuickTerminalKey _quickKey(String keyId) {
|
_QuickTerminalKey _quickKey(String keyId) {
|
||||||
return <_QuickTerminalKey>[
|
return <_QuickTerminalKey>[
|
||||||
|
..._editingControlKeys,
|
||||||
|
..._navigationKeys,
|
||||||
..._symbolTerminalKeys,
|
..._symbolTerminalKeys,
|
||||||
..._controlTerminalKeys,
|
|
||||||
].singleWhere((key) => key.keyId == keyId);
|
].singleWhere((key) => key.keyId == keyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setInputMode(_TerminalInputMode mode) {
|
||||||
|
if (_inputMode == mode) {
|
||||||
|
if (mode == _TerminalInputMode.edit) {
|
||||||
|
_terminalFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_inputMode = mode;
|
||||||
|
});
|
||||||
|
_applyInputMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyInputMode() {
|
||||||
|
_terminalFocusNode.canRequestFocus = _inputMode == _TerminalInputMode.edit;
|
||||||
|
if (_inputMode == _TerminalInputMode.edit) {
|
||||||
|
_terminalFocusNode.requestFocus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_terminalFocusNode.unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTerminalSurfaceTap() {
|
||||||
|
if (_inputMode != _TerminalInputMode.edit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_terminalFocusNode.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTerminalFrame(String frame) {
|
void _handleTerminalFrame(String frame) {
|
||||||
if (_awaitingAttachReplayFrame) {
|
if (_awaitingAttachReplayFrame) {
|
||||||
_awaitingAttachReplayFrame = false;
|
_awaitingAttachReplayFrame = false;
|
||||||
@ -585,13 +647,6 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
|
||||||
'Quick keys',
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildQuickKeysSection(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
Text(
|
||||||
'Presets',
|
'Presets',
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
@ -748,13 +803,17 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
child: TerminalView(
|
child: GestureDetector(
|
||||||
terminal,
|
behavior: HitTestBehavior.translucent,
|
||||||
focusNode: _terminalFocusNode,
|
onTap: _handleTerminalSurfaceTap,
|
||||||
autofocus: false,
|
child: TerminalView(
|
||||||
keyboardType: TextInputType.multiline,
|
terminal,
|
||||||
deleteDetection: true,
|
focusNode: _terminalFocusNode,
|
||||||
scrollController: _terminalScrollController,
|
autofocus: false,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
deleteDetection: true,
|
||||||
|
scrollController: _terminalScrollController,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1000,19 +1059,43 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildCommandDeckAction(
|
Container(
|
||||||
FilledButton.icon(
|
key: const Key('terminal_mode_button'),
|
||||||
key: const Key('terminal_send_button'),
|
width: double.infinity,
|
||||||
onPressed: _canSendInput ? _sendEnter : null,
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
style: FilledButton.styleFrom(
|
child: Wrap(
|
||||||
minimumSize: const Size(0, 42),
|
spacing: 8,
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
runSpacing: 8,
|
||||||
visualDensity: VisualDensity.compact,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
),
|
children: [
|
||||||
icon: const Icon(Icons.keyboard_return),
|
Text(
|
||||||
label: Text(isCompact ? 'Enter' : 'Send Enter'),
|
'Input mode',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
_buildModeButton(
|
||||||
|
key: const Key('terminal_mode_read_button'),
|
||||||
|
label: 'Read',
|
||||||
|
icon: Icons.menu_book_outlined,
|
||||||
|
selected: _inputMode == _TerminalInputMode.read,
|
||||||
|
onPressed: () => _setInputMode(_TerminalInputMode.read),
|
||||||
|
),
|
||||||
|
_buildModeButton(
|
||||||
|
key: const Key('terminal_mode_edit_button'),
|
||||||
|
label: 'Edit',
|
||||||
|
icon: Icons.keyboard_outlined,
|
||||||
|
selected: _inputMode == _TerminalInputMode.edit,
|
||||||
|
onPressed: () => _setInputMode(_TerminalInputMode.edit),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_inputMode == _TerminalInputMode.read
|
||||||
|
? 'Read mode prevents the terminal from taking focus.'
|
||||||
|
: 'Edit mode keeps the terminal ready for typing.',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_buildQuickKeysSection(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -1023,7 +1106,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
Widget _buildQuickKeysSection() {
|
Widget _buildQuickKeysSection() {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_buildKeyTrayRow(_controlTerminalKeys),
|
_buildKeyTrayRow(_editingControlKeys),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildKeyTrayRow(_navigationKeys),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildKeyTrayRow(_symbolTerminalKeys),
|
_buildKeyTrayRow(_symbolTerminalKeys),
|
||||||
],
|
],
|
||||||
@ -1054,6 +1139,32 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
return ExcludeFocus(child: child);
|
return ExcludeFocus(child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildModeButton({
|
||||||
|
required Key key,
|
||||||
|
required String label,
|
||||||
|
required IconData icon,
|
||||||
|
required bool selected,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
key: key,
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
backgroundColor: selected
|
||||||
|
? colorScheme.primary.withValues(alpha: 0.18)
|
||||||
|
: Colors.transparent,
|
||||||
|
side: BorderSide(
|
||||||
|
color: selected ? colorScheme.primary : const Color(0xFF4A3E31),
|
||||||
|
),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
icon: Icon(icon, size: 16),
|
||||||
|
label: Text(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String get _statusLabel => switch (_connectionState) {
|
String get _statusLabel => switch (_connectionState) {
|
||||||
TerminalConnectionState.connecting => 'Connecting',
|
TerminalConnectionState.connecting => 'Connecting',
|
||||||
TerminalConnectionState.connected => 'Connected',
|
TerminalConnectionState.connected => 'Connected',
|
||||||
@ -1068,7 +1179,8 @@ class _TerminalPageState extends ConsumerState<TerminalPage>
|
|||||||
TerminalConnectionState.disconnected => 'Off',
|
TerminalConnectionState.disconnected => 'Off',
|
||||||
};
|
};
|
||||||
|
|
||||||
bool get _canSendInput => controller.canSendInput;
|
bool get _canSendInput =>
|
||||||
|
controller.canSendInput && _inputMode == _TerminalInputMode.edit;
|
||||||
|
|
||||||
IconData get _statusIcon => switch (_connectionState) {
|
IconData get _statusIcon => switch (_connectionState) {
|
||||||
TerminalConnectionState.connecting => Icons.sync,
|
TerminalConnectionState.connecting => Icons.sync,
|
||||||
@ -1104,3 +1216,5 @@ class _QuickTerminalKey {
|
|||||||
final String input;
|
final String input;
|
||||||
final bool repeatable;
|
final bool repeatable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _TerminalInputMode { read, edit }
|
||||||
|
|||||||
@ -348,7 +348,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'terminal page keeps tools hidden until the user opens the tools sheet',
|
'terminal page shows mode controls and inline quick keys in the command deck',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await _pumpApp(
|
await _pumpApp(
|
||||||
tester,
|
tester,
|
||||||
@ -359,31 +359,19 @@ void main() {
|
|||||||
await _openProjectTerminal(tester);
|
await _openProjectTerminal(tester);
|
||||||
|
|
||||||
expect(find.byKey(const Key('terminal_action_bar')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_action_bar')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_send_button')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_send_button')), findsNothing);
|
||||||
expect(find.byKey(const Key('terminal_command_deck')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_command_deck')), findsOneWidget);
|
||||||
expect(find.byType(TextField), findsNothing);
|
expect(find.byType(TextField), findsNothing);
|
||||||
expect(find.byKey(const Key('terminal_actions_sheet')), findsNothing);
|
expect(find.byKey(const Key('terminal_mode_read_button')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_mode_edit_button')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_ctrl_c')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_up')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_enter')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_actions_button')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_actions_button')), findsOneWidget);
|
||||||
expect(
|
|
||||||
find.byKey(const Key('terminal_toggle_actions_button')),
|
|
||||||
findsNothing,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('terminal_actions_button')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byKey(const Key('terminal_actions_sheet')), findsOneWidget);
|
|
||||||
expect(find.text('Reconnect'), findsOneWidget);
|
|
||||||
expect(find.text('Latest'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.byKey(const Key('terminal_quick_key_ctrl_c')),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(find.text('ssh prod'), findsNothing);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('terminal tools expose quick terminal keys', (tester) async {
|
testWidgets('terminal command deck exposes expanded quick terminal keys', (tester) async {
|
||||||
final transportFactory = _QueuedTerminalSocketTransportFactory();
|
final transportFactory = _QueuedTerminalSocketTransportFactory();
|
||||||
|
|
||||||
await _pumpApp(
|
await _pumpApp(
|
||||||
@ -397,19 +385,24 @@ void main() {
|
|||||||
|
|
||||||
await _openProjectTerminal(tester);
|
await _openProjectTerminal(tester);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('terminal_actions_button')));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(find.byKey(const Key('terminal_quick_key_esc')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_esc')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_tab')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_tab')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_ctrl_c')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_ctrl_c')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_ctrl_d')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_ctrl_d')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_ctrl_l')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_ctrl_l')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_ctrl_z')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_delete')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_home')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_end')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_page_up')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('terminal_quick_key_page_down')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_up')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_up')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_down')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_down')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_left')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_left')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('terminal_quick_key_right')), findsOneWidget);
|
expect(find.byKey(const Key('terminal_quick_key_right')), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('terminal_mode_edit_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('terminal_quick_key_esc')));
|
await tester.tap(find.byKey(const Key('terminal_quick_key_esc')));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
@ -419,7 +412,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('terminal page no longer exposes input mode switching', (
|
testWidgets('terminal page exposes reading and editing mode switching', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await _pumpApp(
|
await _pumpApp(
|
||||||
@ -431,9 +424,45 @@ void main() {
|
|||||||
await _openProjectTerminal(tester);
|
await _openProjectTerminal(tester);
|
||||||
|
|
||||||
expect(find.byType(TextField), findsNothing);
|
expect(find.byType(TextField), findsNothing);
|
||||||
await tester.tap(find.byKey(const Key('terminal_actions_button')));
|
expect(find.byKey(const Key('terminal_mode_button')), findsOneWidget);
|
||||||
|
expect(find.text('Read'), findsOneWidget);
|
||||||
|
expect(find.text('Edit'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('terminal quick keys stay disabled until edit mode is selected', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final transportFactory = _QueuedTerminalSocketTransportFactory();
|
||||||
|
|
||||||
|
await _pumpApp(
|
||||||
|
tester,
|
||||||
|
projectRepository: _FakeProjectRepository(),
|
||||||
|
sessionRepository: _FakeSessionRepository(),
|
||||||
|
socketFactory: TerminalSocketSessionFactory(
|
||||||
|
transportFactory: transportFactory.create,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _openProjectTerminal(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('terminal_quick_key_ctrl_l')));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
transportFactory.createdTransports.single.sentMessages,
|
||||||
|
isNot(contains(contains('"input":"\\f"'))),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('terminal_mode_edit_button')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(const Key('terminal_mode_button')), findsNothing);
|
|
||||||
|
await tester.tap(find.byKey(const Key('terminal_quick_key_ctrl_l')));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
transportFactory.createdTransports.single.sentMessages.last,
|
||||||
|
contains('"input":"\\f"'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
@ -472,21 +501,20 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _openProjectTerminal(tester);
|
await _openProjectTerminal(tester);
|
||||||
await tester.tap(find.byKey(const Key('terminal_send_button')));
|
await tester.tap(find.byKey(const Key('terminal_mode_edit_button')));
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.byKey(const Key('terminal_actions_button')));
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('terminal_quick_key_ctrl_l')));
|
await tester.tap(find.byKey(const Key('terminal_quick_key_ctrl_l')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
transportFactory.createdTransports.single.emit('command-output');
|
transportFactory.createdTransports.single.emit('command-output');
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('terminal_actions_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('terminal_diagnostics_button')));
|
await tester.tap(find.byKey(const Key('terminal_diagnostics_button')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.textContaining('ui.input.quick | Ctrl+L'), findsOneWidget);
|
expect(find.textContaining('ui.input.quick | Ctrl+L'), findsOneWidget);
|
||||||
expect(find.textContaining('socket.input.tx | '), findsWidgets);
|
expect(find.textContaining('socket.input.tx |'), findsOneWidget);
|
||||||
expect(find.textContaining(r'socket.input.tx | \r'), findsOneWidget);
|
|
||||||
expect(
|
expect(
|
||||||
find.textContaining('socket.frame.rx | command-output'),
|
find.textContaining('socket.frame.rx | command-output'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
@ -494,7 +522,7 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('terminal send button writes carriage return', (tester) async {
|
testWidgets('terminal enter quick key writes carriage return', (tester) async {
|
||||||
final transportFactory = _QueuedTerminalSocketTransportFactory();
|
final transportFactory = _QueuedTerminalSocketTransportFactory();
|
||||||
|
|
||||||
await _pumpApp(
|
await _pumpApp(
|
||||||
@ -507,7 +535,9 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _openProjectTerminal(tester);
|
await _openProjectTerminal(tester);
|
||||||
await tester.tap(find.byKey(const Key('terminal_send_button')));
|
await tester.tap(find.byKey(const Key('terminal_mode_edit_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('terminal_quick_key_enter')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@ -695,6 +725,8 @@ void main() {
|
|||||||
transportFactory: transportFactory.create,
|
transportFactory: transportFactory.create,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final terminal = tester
|
final terminal = tester
|
||||||
.widget<TerminalView>(find.byType(TerminalView))
|
.widget<TerminalView>(find.byType(TerminalView))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user