TermRemoteCtl/apps/mobile_app/lib/features/terminal/terminal_input_controller.dart

228 lines
5.5 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
enum TerminalInputMode { buffered, direct }
typedef TerminalBufferedSubmit = FutureOr<void> Function(String command);
typedef TerminalDirectInputSink = void Function(String input);
class TerminalInputController extends ChangeNotifier {
TerminalInputController({
required TerminalBufferedSubmit onBufferedSubmit,
required TerminalDirectInputSink onDirectInput,
}) : _onBufferedSubmit = onBufferedSubmit,
_onDirectInput = onDirectInput {
focusNode.addListener(notifyListeners);
textController.addListener(_handleTextChanged);
_applyEditingValue(_bufferedEditingValue);
}
static const String _directInputSentinel = ' ';
static const int _directInputSelectionOffset = 2;
final FocusNode focusNode = FocusNode();
final TextEditingController textController = TextEditingController();
final TerminalBufferedSubmit _onBufferedSubmit;
final TerminalDirectInputSink _onDirectInput;
TerminalInputMode _mode = TerminalInputMode.buffered;
String _bufferedDraft = '';
String? _composingText;
bool _isApplyingValue = false;
bool _isDisposed = false;
TerminalInputMode get mode => _mode;
bool get isDirectInputEnabled => _mode == TerminalInputMode.direct;
String? get composingText => _composingText;
String get hintText => isDirectInputEnabled
? 'Direct mode: keys send immediately'
: 'Buffered mode: type a command and send';
Future<void> submit() async {
if (isDirectInputEnabled) {
_composingText = null;
_onDirectInput('\r');
_restoreDirectEditingValue();
notifyListeners();
return;
}
final command = _bufferedDraft;
if (command.trim().isEmpty) {
return;
}
await _onBufferedSubmit(command);
_bufferedDraft = '';
_applyEditingValue(_bufferedEditingValue);
notifyListeners();
}
void toggleMode() {
setMode(
isDirectInputEnabled
? TerminalInputMode.buffered
: TerminalInputMode.direct,
);
}
void setMode(TerminalInputMode mode) {
if (_mode == mode) {
if (mode == TerminalInputMode.direct) {
_restoreDirectEditingValue();
}
return;
}
_mode = mode;
_composingText = null;
_applyEditingValue(
mode == TerminalInputMode.direct
? _directEditingValue
: _bufferedEditingValue,
);
notifyListeners();
}
void requestFocus() {
focusNode.requestFocus();
}
void syncDirectSelection() {
if (!isDirectInputEnabled) {
return;
}
_restoreDirectEditingValue();
}
@override
void dispose() {
_isDisposed = true;
focusNode.removeListener(notifyListeners);
textController.removeListener(_handleTextChanged);
focusNode.dispose();
textController.dispose();
super.dispose();
}
void _handleTextChanged() {
if (_isApplyingValue) {
return;
}
final value = textController.value;
if (!isDirectInputEnabled) {
_bufferedDraft = value.text;
final nextComposing = value.composing.isCollapsed
? null
: value.composing.textInside(value.text);
if (_composingText != nextComposing) {
_composingText = nextComposing;
notifyListeners();
}
return;
}
if (!value.composing.isCollapsed) {
final nextComposing = value.composing.textInside(value.text);
if (_composingText != nextComposing) {
_composingText = nextComposing;
notifyListeners();
}
return;
}
var didChange = false;
if (_composingText != null) {
_composingText = null;
didChange = true;
}
final backspaceCount = _detectBackspaceCount(value.text);
if (backspaceCount > 0) {
for (var index = 0; index < backspaceCount; index += 1) {
_onDirectInput('\x7f');
}
_restoreDirectEditingValue();
if (didChange) {
notifyListeners();
}
return;
}
final insertedText = _extractInsertedText(value.text);
if (insertedText.isNotEmpty) {
_onDirectInput(insertedText);
_restoreDirectEditingValue();
if (didChange) {
notifyListeners();
}
return;
}
if (value.text != _directInputSentinel ||
value.selection.baseOffset != _directInputSelectionOffset ||
value.selection.extentOffset != _directInputSelectionOffset) {
_restoreDirectEditingValue();
if (didChange) {
notifyListeners();
}
return;
}
if (didChange) {
notifyListeners();
}
}
int _detectBackspaceCount(String text) {
if (text.length >= _directInputSentinel.length) {
return 0;
}
return _directInputSentinel.length - text.length;
}
String _extractInsertedText(String text) {
if (text.isEmpty || text == _directInputSentinel) {
return '';
}
if (text.startsWith(_directInputSentinel)) {
return text.substring(_directInputSentinel.length);
}
return text;
}
void _restoreDirectEditingValue() {
_applyEditingValue(_directEditingValue);
}
void _applyEditingValue(TextEditingValue value) {
if (_isDisposed) {
return;
}
_isApplyingValue = true;
textController.value = value;
_isApplyingValue = false;
}
TextEditingValue get _bufferedEditingValue => TextEditingValue(
text: _bufferedDraft,
selection: TextSelection.collapsed(offset: _bufferedDraft.length),
);
TextEditingValue get _directEditingValue => const TextEditingValue(
text: _directInputSentinel,
selection: TextSelection.collapsed(offset: _directInputSelectionOffset),
);
}