feat: replace xterm with flutter_alacritty for true industrial-grade rendering

This commit is contained in:
2026-08-01 19:53:30 +08:00
parent 7e4236e2f3
commit 562662c1bd
12 changed files with 744 additions and 375 deletions

View File

@@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:xterm/xterm.dart';
import 'package:flutter_alacritty/flutter_alacritty.dart';
import 'connection_bar.dart';
import 'rtty_client.dart';
import 'rtty_pty_backend.dart';
import 'terminal_config.dart';
import 'theme.dart';
/// 主终端页面。
///
/// 组织连接控制条 + xterm 终端渲染区 + 底部状态栏。
/// 断开时展示终端提示页,连接后进入实时渲染
/// 使用 `flutter_alacritty` 的 Rust 引擎TerminalEngine + TerminalView渲染
/// 通过 [RttyPtyBackend] 桥接远程 Rtty Rust 服务端的 WebSocket 数据流
/// 断开时展示终端提示页,连接后进入实时工业级 ANSI 渲染。
class TerminalScreen extends StatefulWidget {
const TerminalScreen({super.key});
@@ -17,8 +19,12 @@ class TerminalScreen extends StatefulWidget {
}
class _TerminalScreenState extends State<TerminalScreen> {
late final Terminal _terminal;
late final RttyClient _client;
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');
@@ -32,32 +38,17 @@ class _TerminalScreenState extends State<TerminalScreen> {
@override
void initState() {
super.initState();
_terminal = Terminal(onOutput: (_) {});
_client = RttyClient(_terminal);
_client.onStateChanged = (s) => setState(() => _state = s);
_client.onReady = (id, cols, rows) {
setState(() {
_sessionId = id;
_cols = cols;
_rows = rows;
});
};
_client.onSessionClosed = () {
if (mounted) {
setState(() {
_state = ConnState.disconnected;
_sessionId = null;
});
}
};
_config = RttyTerminalConfig.build();
}
@override
void dispose() {
_client.disconnect();
_tearDownSession();
_host.dispose();
_port.dispose();
_session.dispose();
_controller.dispose();
_focus.dispose();
super.dispose();
}
@@ -67,17 +58,65 @@ class _TerminalScreenState extends State<TerminalScreen> {
final session = _session.text.trim();
if (host.isEmpty) return;
_terminal.write('\x1b[2J\x1b[H');
await _client.connect(host: host, port: port, session: session);
_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);
}
Future<void> _disconnect() => _client.disconnect();
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'
: '';
final sizeText = _state == ConnState.connected ? '$_cols × $_rows' : '';
return Scaffold(
backgroundColor: RttyTheme.background,
@@ -101,92 +140,36 @@ class _TerminalScreenState extends State<TerminalScreen> {
}
Widget _buildBody() {
if (_state == ConnState.connected) {
return _TerminalViewport(
terminal: _terminal,
cols: _cols,
rows: _rows,
onResize: _handleResize,
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);
}
void _handleResize(int cols, int rows) {
setState(() {
_cols = cols;
_rows = rows;
});
_client.sendResize(cols, rows);
}
}
/// 终端渲染视口。监听尺寸变化并同步给服务端。
class _TerminalViewport extends StatefulWidget {
const _TerminalViewport({
required this.terminal,
required this.cols,
required this.rows,
required this.onResize,
});
final Terminal terminal;
final int cols;
final int rows;
final void Function(int cols, int rows) onResize;
@override
State<_TerminalViewport> createState() => _TerminalViewportState();
}
class _TerminalViewportState extends State<_TerminalViewport> {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// 估算每格宽高,把像素尺寸转成网格尺寸并触发 resize。
final style = RttyTheme.terminalStyle();
final fontSize = style.fontSize;
final colWidth = fontSize * 0.62;
final rowHeight = fontSize * style.height;
final cols = (constraints.maxWidth / colWidth).floor().clamp(20, 500);
final rows =
(constraints.maxHeight / rowHeight).floor().clamp(5, 300);
// 网格变化时通知服务端。
if (cols != widget.cols || rows != widget.rows) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.terminal.resize(cols, rows);
widget.onResize(cols, rows);
});
}
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(
widget.terminal,
theme: RttyTheme.terminal(),
textStyle: style,
autofocus: true,
mouseCursor: SystemMouseCursors.text,
),
),
),
);
},
);
}
}
/// 未连接时的提示面板。
@@ -325,7 +308,7 @@ class _StatusBar extends StatelessWidget {
_Hint('Ctrl+Shift+C / V 复制粘贴', RttyTheme.textFaint),
Spacer(),
Text(
'FLUTTER × XTERM TERMINAL',
'FLUTTER × ALACRITTY ENGINE',
style: TextStyle(color: RttyTheme.textFaint, fontSize: 10),
),
],