feat: replace xterm with flutter_alacritty for true industrial-grade rendering
This commit is contained in:
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user