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((ref) { final service = SshService(); ref.onDispose(() => service.dispose()); return service; }); // State for connection status final connectionStateProvider = StateProvider((ref) => false); // State for connection errors final connectionErrorProvider = StateProvider((ref) => null); class TerminalNotifier extends StateNotifier> { final SshService _sshService; final StateController _connectionStateController; final StateController _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 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 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>((ref) { final sshService = ref.watch(sshServiceProvider); final connectionState = ref.watch(connectionStateProvider.notifier); final errorController = ref.watch(connectionErrorProvider.notifier); return TerminalNotifier(sshService, connectionState, errorController); });