feat(ui): complete M2 Milestone - Desktop POSIX PTY FFI implementation and attach terminal tab integration

This commit is contained in:
2026-07-16 22:50:59 +09:00
parent 7e4cab6c09
commit b7901bcce5
11 changed files with 605 additions and 371 deletions
@@ -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,
),
],
),
),
],
),
),
);
}
}
@@ -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,
),
);
}
}
@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@@ -89,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter:
dependency: "direct main"
description: flutter
@@ -178,6 +194,13 @@ packages:
relative: true
source: path
version: "1.0.0"
mam_pty:
dependency: "direct main"
description:
path: "../../packages/mam_pty"
relative: true
source: path
version: "1.0.0"
markdown:
dependency: transitive
description:
@@ -218,14 +241,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform_info:
dependency: transitive
description:
name: platform_info
sha256: "012e73712166cf0b56d3eb95c0d33491f56b428c169eca385f036448474147e4"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
quiver:
dependency: transitive
description:
@@ -242,6 +257,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sky_engine:
dependency: transitive
description: flutter
@@ -335,14 +366,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
url: "https://pub.dev"
source: hosted
version: "2.4.0"
xterm:
dependency: "direct main"
description:
name: xterm
sha256: "6a02b15d03152b8186e12790902ff28c8a932fc441e89fa7255a7491661a8e69"
sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "4.0.0"
yaml:
dependency: transitive
description:
@@ -351,6 +390,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.3"
zmodem:
dependency: transitive
description:
name: zmodem
sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf"
url: "https://pub.dev"
source: hosted
version: "0.0.6"
sdks:
dart: ">=3.12.1 <4.0.0"
flutter: ">=3.27.0"
@@ -33,9 +33,11 @@ dependencies:
cupertino_icons: ^1.0.8
mam_core:
path: ../../packages/mam_core
mam_pty:
path: ../../packages/mam_pty
flutter_riverpod: ^2.4.9
data_table_2: ^2.5.8
xterm: ^3.2.0
xterm: ^4.0.0
flutter_markdown: ^0.6.18
meta: ^1.9.0
@@ -218,14 +218,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform_info:
dependency: transitive
description:
name: platform_info
sha256: "012e73712166cf0b56d3eb95c0d33491f56b428c169eca385f036448474147e4"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
quiver:
dependency: transitive
description:
@@ -339,10 +331,10 @@ packages:
dependency: "direct main"
description:
name: xterm
sha256: "6a02b15d03152b8186e12790902ff28c8a932fc441e89fa7255a7491661a8e69"
sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "4.0.0"
yaml:
dependency: transitive
description:
@@ -351,6 +343,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.3"
zmodem:
dependency: transitive
description:
name: zmodem
sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf"
url: "https://pub.dev"
source: hosted
version: "0.0.6"
sdks:
dart: ">=3.12.1 <4.0.0"
flutter: ">=3.27.0"
@@ -35,7 +35,7 @@ dependencies:
path: ../../packages/mam_core
flutter_riverpod: ^2.4.9
data_table_2: ^2.5.8
xterm: ^3.2.0
xterm: ^4.0.0
flutter_markdown: ^0.6.18
meta: ^1.9.0