import 'package:flutter/material.dart'; import 'package:flutter_alacritty/flutter_alacritty.dart'; import 'connection_bar.dart'; import 'rtty_pty_backend.dart'; import 'terminal_config.dart'; import 'theme.dart'; /// 主终端页面。 /// /// 使用 `flutter_alacritty` 的 Rust 引擎(TerminalEngine + TerminalView)渲染, /// 通过 [RttyPtyBackend] 桥接远程 Rtty Rust 服务端的 WebSocket 数据流。 /// 断开时展示终端提示页,连接后进入实时工业级 ANSI 渲染。 class TerminalScreen extends StatefulWidget { const TerminalScreen({super.key}); @override State createState() => _TerminalScreenState(); } class _TerminalScreenState extends State { late final TerminalConfig _config; TerminalEngine? _engine; TerminalController _controller = TerminalController(); final FocusNode _focus = FocusNode(); RttyPtyBackend? _backend; final _host = TextEditingController(text: '127.0.0.1'); final _port = TextEditingController(text: '8080'); final _session = TextEditingController(); ConnState _state = ConnState.disconnected; String? _sessionId; int _cols = 0; int _rows = 0; @override void initState() { super.initState(); _config = RttyTerminalConfig.build(); } @override void dispose() { _tearDownSession(); _host.dispose(); _port.dispose(); _session.dispose(); _controller.dispose(); _focus.dispose(); super.dispose(); } Future _connect() async { final host = _host.text.trim(); final port = int.tryParse(_port.text.trim()) ?? 8080; final session = _session.text.trim(); if (host.isEmpty) return; _tearDownSession(); setState(() { _engine = TerminalEngine(config: _config); _controller = TerminalController()..attach(_engine!); _backend = RttyPtyBackend(); }); _backend!.onStateChanged = (s) { if (!mounted) return; setState(() => _state = s); if (s == ConnState.disconnected || s == ConnState.error) { _sessionId = null; } }; _backend!.onReady = (id, cols, rows) { if (!mounted) return; setState(() { _sessionId = id; _cols = cols; _rows = rows; }); }; await _backend!.connect(host: host, port: port, session: session); } void _disconnect() => _tearDownSession(); void _tearDownSession() { final b = _backend; if (b != null) { b.onStateChanged = null; b.onReady = null; b.kill(); b.dispose(); } _backend = null; _engine?.dispose(); _engine = null; _sessionId = null; _state = ConnState.disconnected; } void _handlePtyResize(int cols, int rows) { final b = _backend; if (b == null) return; b.resize(rows, cols); if (mounted) { setState(() { _cols = cols; _rows = rows; }); } } @override Widget build(BuildContext context) { final sizeText = _state == ConnState.connected ? '$_cols × $_rows' : '—'; return Scaffold( backgroundColor: RttyTheme.background, body: Column( children: [ ConnectionBar( state: _state, hostController: _host, portController: _port, sessionController: _session, sessionId: _sessionId, terminalSize: sizeText, onConnect: _connect, onDisconnect: _disconnect, ), Expanded(child: _buildBody()), const _StatusBar(), ], ), ); } Widget _buildBody() { final engine = _engine; if (_state == ConnState.connected && engine != null) { return Container( color: RttyTheme.background, padding: const EdgeInsets.all(16), child: ClipRRect( borderRadius: BorderRadius.circular(4), child: Container( decoration: BoxDecoration( color: RttyTheme.background, border: Border.all(color: RttyTheme.border), borderRadius: BorderRadius.circular(4), ), padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: TerminalView( engine, controller: _controller, focusNode: _focus, autofocus: true, onPtyResize: _handlePtyResize, theme: _config.theme, textStyle: _config.style, mouseCursor: SystemMouseCursors.text, ), ), ), ); } return _IdlePane(state: _state); } } /// 未连接时的提示面板。 class _IdlePane extends StatelessWidget { const _IdlePane({required this.state}); final ConnState state; @override Widget build(BuildContext context) { final (title, subtitle) = switch (state) { ConnState.disconnected => ( 'AWAITING CONNECTION', '输入服务端地址后点击 CONNECT 建立终端会话', ), ConnState.connecting => ( 'CONNECTING', '正在协商 WebSocket 终端会话…', ), ConnState.error => ( 'CONNECTION LOST', '无法建立连接,请检查服务端是否已启动', ), ConnState.connected => ('', ''), }; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ _ScanlineGlyph(state: state), const SizedBox(height: 28), Text( title, style: const TextStyle( color: RttyTheme.textDim, fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: 3, ), ), const SizedBox(height: 10), Text( subtitle, style: const TextStyle(color: RttyTheme.textFaint, fontSize: 12), ), ], ), ); } } /// 终端提示区的装饰性扫描线字形。 class _ScanlineGlyph extends StatelessWidget { const _ScanlineGlyph({required this.state}); final ConnState state; @override Widget build(BuildContext context) { final color = switch (state) { ConnState.connected => RttyTheme.primary, ConnState.connecting => RttyTheme.accent, ConnState.error => RttyTheme.danger, _ => RttyTheme.textFaint, }; return Container( width: 96, height: 96, decoration: BoxDecoration( border: Border.all(color: color.withValues(alpha: 0.5), width: 1.5), borderRadius: BorderRadius.circular(12), ), child: CustomPaint(painter: _ScanlinePainter(color)), ); } } class _ScanlinePainter extends CustomPainter { _ScanlinePainter(this.color); final Color color; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color.withValues(alpha: 0.18) ..strokeWidth = 1; const spacing = 6.0; for (double y = 0; y < size.height; y += spacing) { canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } final cursorPaint = Paint() ..color = color.withValues(alpha: 0.8) ..strokeWidth = 2; canvas.drawRect( Rect.fromLTWH(size.width * 0.3, size.height * 0.45, 24, 14), cursorPaint..style = PaintingStyle.stroke, ); } @override bool shouldRepaint(covariant _ScanlinePainter old) => old.color != color; } /// 底部状态栏:会话 ID、连接状态、按键提示。 class _StatusBar extends StatelessWidget { const _StatusBar(); @override Widget build(BuildContext context) { return Container( height: 26, decoration: BoxDecoration( color: RttyTheme.surface, border: Border(top: BorderSide(color: RttyTheme.border)), ), padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: const [ _StatusDot(), SizedBox(width: 8), Text( 'RTTY DESKTOP', style: TextStyle( color: RttyTheme.textFaint, fontSize: 10, letterSpacing: 1.6, fontWeight: FontWeight.w600, ), ), SizedBox(width: 20), _Hint('拖选复制', RttyTheme.textFaint), SizedBox(width: 14), _Hint('Ctrl+Shift+C / V 复制粘贴', RttyTheme.textFaint), Spacer(), Text( 'FLUTTER × ALACRITTY ENGINE', style: TextStyle(color: RttyTheme.textFaint, fontSize: 10), ), ], ), ); } } class _StatusDot extends StatelessWidget { const _StatusDot(); @override Widget build(BuildContext context) { return Container( width: 6, height: 6, decoration: const BoxDecoration( color: RttyTheme.primary, shape: BoxShape.circle, ), ); } } class _Hint extends StatelessWidget { const _Hint(this.text, this.color); final String text; final Color color; @override Widget build(BuildContext context) { return Text( text, style: TextStyle(color: color, fontSize: 10, fontFamily: 'Consolas'), ); } }