diff --git a/lib/builtin/ipc_status/src/extension.dart b/lib/builtin/ipc_status/src/extension.dart index f28d4d23..9ddf1bab 100644 --- a/lib/builtin/ipc_status/src/extension.dart +++ b/lib/builtin/ipc_status/src/extension.dart @@ -1,33 +1,20 @@ import 'package:clide/builtin/ipc_status/src/status_item.dart'; import 'package:clide/extension/extension.dart'; -import 'package:clide/kernel/kernel.dart'; class IpcStatusExtension extends ClideExtension { @override String get id => 'builtin.ipc-status'; @override - String get title => 'Daemon connection'; + String get title => 'Tool status'; @override - String get version => '0.1.0'; - - DaemonClient? _ipc; + String get version => '0.2.0'; @override - Future activate(ClideExtensionContext ctx) async { - _ipc = ctx.ipc; - } - - @override - List get contributions { - final ipc = _ipc; - if (ipc == null) return const []; - return [ - StatusItemContribution( - id: 'ipc-status.indicator', - priority: 100, // right-side - listenable: ipc, - build: (_) => IpcStatusItem(ipc: ipc), - ), - ]; - } + List get contributions => [ + StatusItemContribution( + id: 'ipc-status.indicator', + priority: 100, + build: (_) => const ToolStatusItem(), + ), + ]; } diff --git a/lib/builtin/ipc_status/src/status_item.dart b/lib/builtin/ipc_status/src/status_item.dart index 11ef0f23..e781232e 100644 --- a/lib/builtin/ipc_status/src/status_item.dart +++ b/lib/builtin/ipc_status/src/status_item.dart @@ -1,53 +1,84 @@ +import 'dart:io'; + import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; import 'package:flutter/widgets.dart'; -class IpcStatusItem extends StatelessWidget { - const IpcStatusItem({super.key, required this.ipc}); +class ToolStatusItem extends StatefulWidget { + const ToolStatusItem({super.key}); - final DaemonClient ipc; + @override + State createState() => _ToolStatusItemState(); +} - static const _ns = 'builtin.ipc-status'; +class _ToolStatusItemState extends State { + bool _ptycOk = false; + bool _pqlOk = false; + bool _checked = false; + + @override + void initState() { + super.initState(); + _check(); + } + + Future _check() async { + final ptycResult = await _which('ptyc'); + final pqlResult = await _which('pql'); + if (!mounted) return; + setState(() { + _ptycOk = ptycResult; + _pqlOk = pqlResult; + _checked = true; + }); + } + + Future _which(String name) async { + try { + final r = await Process.run('which', [name]); + return r.exitCode == 0; + } catch (_) { + return false; + } + } @override Widget build(BuildContext context) { - final kernel = ClideKernel.of(context); final tokens = ClideTheme.of(context).surface; - return ListenableBuilder( - listenable: Listenable.merge([ipc, kernel.i18n]), - builder: (ctx, _) { - final connected = ipc.isConnected; - final color = connected ? tokens.statusSuccess : tokens.statusError; - final i = kernel.i18n; - final label = connected - ? i.string('connected', namespace: _ns, placeholder: 'connected') - : i.string('disconnected', - namespace: _ns, placeholder: 'disconnected'); - final hint = connected - ? i.string('connected.hint', - namespace: _ns, - placeholder: 'clide daemon is reachable over the local socket') - : i.string('disconnected.hint', - namespace: _ns, - placeholder: - 'clide daemon is not running — start it with `clide --daemon`'); - return Semantics( - label: label, - hint: hint, - liveRegion: true, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ClideIcon(const PlugIcon(), size: 12, color: color), - const SizedBox(width: 6), - ClideText(label, fontSize: clideFontCaption, color: color), - ], - ), - ), - ); - }, + if (!_checked) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _Indicator(label: 'ptyc', ok: _ptycOk, tokens: tokens), + const SizedBox(width: 10), + _Indicator(label: 'pql', ok: _pqlOk, tokens: tokens), + ], + ), + ); + } +} + +class _Indicator extends StatelessWidget { + const _Indicator({required this.label, required this.ok, required this.tokens}); + final String label; + final bool ok; + final SurfaceTokens tokens; + + @override + Widget build(BuildContext context) { + final color = ok ? tokens.statusSuccess : tokens.statusWarning; + return Semantics( + label: '$label ${ok ? "available" : "not found"}', + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClideIcon(PhosphorIcons.circlesFour, size: 10, color: color), + const SizedBox(width: 4), + ClideText(label, fontSize: clideFontCaption, color: color, fontFamily: clideMonoFamily), + ], + ), ); } } diff --git a/test/builtin/ipc_status/widget_test.dart b/test/builtin/ipc_status/widget_test.dart index 949ab331..0e339502 100644 --- a/test/builtin/ipc_status/widget_test.dart +++ b/test/builtin/ipc_status/widget_test.dart @@ -1,9 +1,7 @@ -import 'dart:ui'; - import 'package:clide/builtin/ipc_status/ipc_status.dart'; +import 'package:clide/builtin/ipc_status/src/status_item.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../helpers/kernel_fixture.dart'; @@ -14,18 +12,7 @@ void main() { late KernelFixture f; setUp(() async { - f = await KernelFixture.create( - i18nCatalogs: { - 'builtin.ipc-status': { - const Locale('en', 'US'): const { - 'connected': {'translation': 'connected'}, - 'connected.hint': {'translation': 'daemon reachable'}, - 'disconnected': {'translation': 'disconnected'}, - 'disconnected.hint': {'translation': 'daemon down'}, - }, - }, - }, - ); + f = await KernelFixture.create(); }); tearDown(() async => f.dispose()); @@ -41,21 +28,10 @@ void main() { expect(items.first.priority, 100); }); - testWidgets('renders "disconnected" label until connected', (tester) async { - await tester.pumpWidget( - harness(f, IpcStatusItem(ipc: f.services.ipc)), - ); - expect(find.text('disconnected'), findsOneWidget); - }); - - testWidgets('flips to "connected" when the client reports connected', - (tester) async { - await tester.pumpWidget( - harness(f, IpcStatusItem(ipc: f.services.ipc)), - ); - f.ipc.setConnected(true); + testWidgets('renders without crashing', (tester) async { + await tester.pumpWidget(harness(f, const ToolStatusItem())); await tester.pumpAndSettle(); - expect(find.text('connected'), findsOneWidget); + expect(tester.takeException(), isNull); }); }); }