Files
accessible-ssh/lib/state/terminal_provider.dart

142 lines
5.6 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:accessible_terminal/models/terminal_line.dart';
import 'package:accessible_terminal/services/ssh_service.dart';
// Provider for the SSH Service instance
final sshServiceProvider = Provider<SshService>((ref) {
final service = SshService();
ref.onDispose(() => service.dispose());
return service;
});
// State for connection status
final connectionStateProvider = StateProvider<bool>((ref) => false);
// State for connection errors
final connectionErrorProvider = StateProvider<String?>((ref) => null);
class TerminalNotifier extends StateNotifier<List<TerminalLine>> {
final SshService _sshService;
final StateController<bool> _connectionStateController;
final StateController<String?> _errorController;
String _currentLineBuffer = '';
bool _isUserInitiatedDisconnect = false;
TerminalNotifier(this._sshService, this._connectionStateController, this._errorController) : super([]) {
_sshService.outputStream.listen((data) {
_processOutput(data);
});
_sshService.disconnectStream.listen((isClean) {
if (!_isUserInitiatedDisconnect && !isClean && _connectionStateController.state) {
_errorController.state = 'Connection lost unexpectedly (Timeout or Server closed).';
}
_connectionStateController.state = false;
_processOutput('\n[Connection Closed]\n');
});
}
Future<void> connect(String host, int port, String username, {String? password, String? privateKey}) async {
_isUserInitiatedDisconnect = false;
try {
state = [TerminalLine(text: 'Connecting to $host...'), ...state];
await _sshService.connect(
host: host,
port: port,
username: username,
password: password,
privateKey: privateKey,
);
_connectionStateController.state = true;
} catch (e) {
state = [TerminalLine(text: 'Error: $e'), ...state];
_connectionStateController.state = false;
rethrow;
}
}
Future<void> disconnect() async {
_isUserInitiatedDisconnect = true;
await _sshService.disconnect();
_connectionStateController.state = false;
}
void sendInput(String input) {
if (_connectionStateController.state) {
_sshService.write(input);
}
}
void _processOutput(String data) {
// Normalize line endings to \n
final normalizedData = data.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
final parts = normalizedData.split('\n');
for (int i = 0; i < parts.length; i++) {
final part = parts[i];
if (i == 0) {
// Append to the current pending line
_currentLineBuffer += part;
} else {
// The previous part ended with a newline, so push the buffer and start new
if (_currentLineBuffer.isNotEmpty) {
state = [TerminalLine(text: _currentLineBuffer), ...state];
} else if (state.isNotEmpty) {
// Handle empty lines if needed, or just spacers
state = [TerminalLine(text: ''), ...state];
}
_currentLineBuffer = part;
}
}
// Note: We don't push the last chunk immediately if it doesn't end with \n
// It remains in _currentLineBuffer for the next packet.
// However, to see typing in real-time, we might want to update the latest line on screen?
// For a log-based view (reverse list), the "active" line is usually at index 0.
// If we have a buffer, we might want to temporarily show it.
// For simplicity in this step, we only commit lines when a newline occurs,
// OR we can perform a more complex "update top line" logic.
// Let's stick to: if we have a buffer, it's NOT in the list yet.
// BUT this makes the prompt invisible until you hit enter.
// FIX: Always expose the buffer as the first element if it exists?
// Better approach for "live" feel in a chat/log interface:
// If we have content in _currentLineBuffer, we should probably show it.
// Since we can't easily "edit" the immutable list state efficiently without copying,
// we might just accept that partial lines aren't committed or we commit every chunk.
// Let's commit every chunk for now to ensure feedback, even if it splits weirdly?
// No, that breaks the "line" concept.
// Compromise: We won't show the partial line in the *history* list,
// but the UI could consume `_currentLineBuffer` if we exposed it.
// OR, we just push it to state and replace the first element if it was partial?
// Too complex for now. Let's just push lines as they complete.
// Actually, for SSH, echoing characters back is handled by the remote.
// If I type 'a', remote sends 'a'. I want to see 'a'.
// If I don't flush _currentLineBuffer, I won't see it.
// So I MUST flush _currentLineBuffer to the state if it's not empty,
// effectively "updating" the top line.
if (_currentLineBuffer.isNotEmpty) {
// Filter out purely empty or whitespace-only lines to reduce accessibility noise
if (_currentLineBuffer.trim().isNotEmpty) {
state = [TerminalLine(text: _currentLineBuffer), ...state];
}
_currentLineBuffer = '';
}
}
void clear() {
state = [];
_currentLineBuffer = '';
}
}
final terminalOutputProvider = StateNotifierProvider<TerminalNotifier, List<TerminalLine>>((ref) {
final sshService = ref.watch(sshServiceProvider);
final connectionState = ref.watch(connectionStateProvider.notifier);
final errorController = ref.watch(connectionErrorProvider.notifier);
return TerminalNotifier(sshService, connectionState, errorController);
});