- Flutter app with chat and terminal screens - WebSocket integration for real-time chat - xterm integration for screen sessions - Markdown rendering with code blocks - JWT authentication Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
116 lines
2.9 KiB
Dart
116 lines
2.9 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
import '../config/api_config.dart';
|
|
|
|
enum TerminalConnectionState {
|
|
disconnected,
|
|
connecting,
|
|
connected,
|
|
error,
|
|
}
|
|
|
|
class TerminalService {
|
|
WebSocketChannel? _channel;
|
|
String? _token;
|
|
String? _sessionName;
|
|
|
|
final _outputController = StreamController<String>.broadcast();
|
|
final _stateController = StreamController<TerminalConnectionState>.broadcast();
|
|
|
|
Stream<String> get output => _outputController.stream;
|
|
Stream<TerminalConnectionState> get connectionState => _stateController.stream;
|
|
|
|
TerminalConnectionState _currentState = TerminalConnectionState.disconnected;
|
|
TerminalConnectionState get currentState => _currentState;
|
|
|
|
void setToken(String? token) {
|
|
_token = token;
|
|
}
|
|
|
|
Future<void> connect(String sessionName) async {
|
|
if (_token == null) {
|
|
_updateState(TerminalConnectionState.error);
|
|
return;
|
|
}
|
|
|
|
_sessionName = sessionName;
|
|
_updateState(TerminalConnectionState.connecting);
|
|
|
|
try {
|
|
final uri = Uri.parse('${ApiConfig.wsUrl}${ApiConfig.wsTerminal(sessionName)}');
|
|
_channel = WebSocketChannel.connect(uri);
|
|
|
|
await _channel!.ready;
|
|
|
|
// Send auth token
|
|
_channel!.sink.add(jsonEncode({'token': _token}));
|
|
|
|
_channel!.stream.listen(
|
|
(data) {
|
|
try {
|
|
final message = jsonDecode(data);
|
|
if (message['type'] == 'connected') {
|
|
_updateState(TerminalConnectionState.connected);
|
|
} else if (message['type'] == 'output') {
|
|
_outputController.add(message['data'] ?? '');
|
|
}
|
|
} catch (e) {
|
|
// Raw data
|
|
_outputController.add(data.toString());
|
|
}
|
|
},
|
|
onError: (error) {
|
|
_updateState(TerminalConnectionState.error);
|
|
},
|
|
onDone: () {
|
|
_updateState(TerminalConnectionState.disconnected);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
_updateState(TerminalConnectionState.error);
|
|
}
|
|
}
|
|
|
|
void sendInput(String input) {
|
|
if (_channel == null || _currentState != TerminalConnectionState.connected) {
|
|
return;
|
|
}
|
|
|
|
_channel!.sink.add(jsonEncode({
|
|
'type': 'input',
|
|
'data': input,
|
|
}));
|
|
}
|
|
|
|
void resize(int rows, int cols) {
|
|
if (_channel == null || _currentState != TerminalConnectionState.connected) {
|
|
return;
|
|
}
|
|
|
|
_channel!.sink.add(jsonEncode({
|
|
'type': 'resize',
|
|
'rows': rows,
|
|
'cols': cols,
|
|
}));
|
|
}
|
|
|
|
void _updateState(TerminalConnectionState state) {
|
|
_currentState = state;
|
|
_stateController.add(state);
|
|
}
|
|
|
|
void disconnect() {
|
|
_channel?.sink.close();
|
|
_channel = null;
|
|
_sessionName = null;
|
|
_updateState(TerminalConnectionState.disconnected);
|
|
}
|
|
|
|
void dispose() {
|
|
disconnect();
|
|
_outputController.close();
|
|
_stateController.close();
|
|
}
|
|
}
|