feat(ui): complete M2 Milestone - Desktop POSIX PTY FFI implementation and attach terminal tab integration
This commit is contained in:
+30
-1
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:mam_core/mam_core.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
import 'terminal_pane.dart';
|
||||
|
||||
class _KV {
|
||||
final String label;
|
||||
@@ -27,7 +28,35 @@ class DetailPane extends StatelessWidget {
|
||||
),
|
||||
border: Border(left: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
child: s == null ? const _EmptyDetail() : _DetailContent(session: s),
|
||||
child: s == null
|
||||
? const _EmptyDetail()
|
||||
: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'INFO'),
|
||||
Tab(text: 'TERMINAL'),
|
||||
],
|
||||
labelColor: AppColors.accent2,
|
||||
unselectedLabelColor: AppColors.textSecondary,
|
||||
indicatorColor: AppColors.accent2,
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
_DetailContent(session: s),
|
||||
TerminalPane(
|
||||
sessionName: s.name,
|
||||
serverName: s.server,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import 'package:mam_pty/mam_pty.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class TerminalPane extends StatefulWidget {
|
||||
final String sessionName;
|
||||
final String serverName;
|
||||
final VoidCallback? onClosed;
|
||||
|
||||
const TerminalPane({
|
||||
super.key,
|
||||
required this.sessionName,
|
||||
required this.serverName,
|
||||
this.onClosed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TerminalPane> createState() => _TerminalPaneState();
|
||||
}
|
||||
|
||||
class _TerminalPaneState extends State<TerminalPane> {
|
||||
late final Terminal _terminal;
|
||||
PtySession? _pty;
|
||||
StreamSubscription<List<int>>? _stdoutSub;
|
||||
bool _hasError = false;
|
||||
String _errorMessage = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_terminal = Terminal(
|
||||
maxLines: 1000,
|
||||
);
|
||||
_startPty();
|
||||
}
|
||||
|
||||
Future<void> _startPty() async {
|
||||
try {
|
||||
final pty = await PtySession.start(
|
||||
'tmux',
|
||||
['-L', widget.serverName, 'attach', '-t', widget.sessionName],
|
||||
);
|
||||
if (!mounted) {
|
||||
pty.close();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_pty = pty;
|
||||
});
|
||||
|
||||
// Handle PTY stdout stream data redirection to xterm widget
|
||||
_stdoutSub = pty.stdout.listen(
|
||||
(data) {
|
||||
_terminal.write(utf8.decode(data, allowMalformed: true));
|
||||
},
|
||||
onError: (err) {
|
||||
_showError(err.toString());
|
||||
},
|
||||
onDone: () {
|
||||
widget.onClosed?.call();
|
||||
},
|
||||
);
|
||||
|
||||
// Handle user keyboard inputs redirection from xterm to PTY using onOutput (xterm v3)
|
||||
_terminal.onOutput = (data) {
|
||||
_pty?.writeString(data);
|
||||
};
|
||||
|
||||
// Handle user-driven terminal layout resizes (4 args in xterm v3)
|
||||
_terminal.onResize = (cols, rows, pxW, pxH) {
|
||||
_pty?.resize(cols, rows);
|
||||
};
|
||||
} catch (e) {
|
||||
_showError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_errorMessage = message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stdoutSub?.cancel();
|
||||
_pty?.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_hasError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.danger, size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'PTY Session failed:\n$_errorMessage',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: AppColors.danger),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_hasError = false;
|
||||
_errorMessage = '';
|
||||
});
|
||||
_startPty();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TerminalView(
|
||||
_terminal,
|
||||
autofocus: true,
|
||||
backgroundOpacity: 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user