From 648abba6aeb92e012255e858c23aedc44fb87886 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 10:55:49 +0200 Subject: [PATCH] shared ToolCheck service, welcome status, logo doubled ToolCheck kernel service checks ptyc/pql/tmux/git availability once at boot, shared by status bar and welcome screen. Welcome status line shows "application ok" in green or lists missing tools in amber. Logo doubled to 144px. DialogHost moved inside Expanded to fix blank workspace. Welcome maxWidth 850px, path overflow fixed with Flexible. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/app.dart | 16 ++-- lib/builtin/ipc_status/src/status_item.dart | 95 +++++++-------------- lib/builtin/welcome/src/welcome_view.dart | 22 ++--- lib/kernel/kernel.dart | 1 + lib/kernel/src/facade.dart | 5 ++ lib/kernel/src/tool_check.dart | 41 +++++++++ lib/main.dart | 3 + 7 files changed, 98 insertions(+), 85 deletions(-) create mode 100644 lib/kernel/src/tool_check.dart diff --git a/lib/app.dart b/lib/app.dart index 0b620ff2..f64ac4af 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -87,12 +87,12 @@ class _RootShellState extends State<_RootShell> { color: tokens.globalBackground, child: ClideResizeBorder( windowControls: widget.services.window, - child: DialogHost( - router: widget.services.dialog, - child: Column( - children: [ - _HatBar(kernel: widget.services), - Expanded( + child: Column( + children: [ + _HatBar(kernel: widget.services), + Expanded( + child: DialogHost( + router: widget.services.dialog, child: Stack( children: [ const Positioned.fill(child: RootLayout()), @@ -101,8 +101,8 @@ class _RootShellState extends State<_RootShell> { ], ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/builtin/ipc_status/src/status_item.dart b/lib/builtin/ipc_status/src/status_item.dart index 9be49ec0..ec8ea72c 100644 --- a/lib/builtin/ipc_status/src/status_item.dart +++ b/lib/builtin/ipc_status/src/status_item.dart @@ -1,82 +1,45 @@ -import 'dart:io'; - import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; import 'package:flutter/widgets.dart'; -class ToolStatusItem extends StatefulWidget { +class ToolStatusItem extends StatelessWidget { const ToolStatusItem({super.key}); - @override - State createState() => _ToolStatusItemState(); -} - -class _ToolStatusItemState extends State { - bool _ptycOk = false; - bool _pqlOk = false; - bool _checked = false; - - @override - void initState() { - super.initState(); - _check(); - } - - Future _check() async { - final cwd = Directory.current.path; - final ptycResult = _exists('$cwd/native/linux-x64/ptyc') || _exists('$cwd/ptyc/bin/ptyc') || await _which('ptyc'); - final pqlResult = await _which('pql'); - if (!mounted) return; - setState(() { - _ptycOk = ptycResult; - _pqlOk = pqlResult; - _checked = true; - }); - } - - bool _exists(String path) => File(path).existsSync(); - - 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; - if (!_checked) return const SizedBox.shrink(); - - if (_ptycOk && _pqlOk) { - return _chip('ok', tokens.statusSuccess, tokens); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!_ptycOk) _chip('ptyc not found', tokens.statusWarning, tokens), - if (!_ptycOk && !_pqlOk) const SizedBox(width: 10), - if (!_pqlOk) _chip('pql not found', tokens.statusWarning, tokens), - ], + return ListenableBuilder( + listenable: kernel.toolCheck, + builder: (ctx, _) { + final tc = kernel.toolCheck; + if (!tc.checked) return const SizedBox.shrink(); + if (tc.allOk) { + return _chip('ok', tokens.statusSuccess, tokens); + } + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < tc.errors.length; i++) ...[ + if (i > 0) const SizedBox(width: 10), + _chip(tc.errors[i], tokens.statusWarning, tokens), + ], + ], + ); + }, ); } Widget _chip(String label, Color color, SurfaceTokens tokens) { - return Semantics( - label: label, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), - const SizedBox(width: 6), - ClideText(label, fontSize: clideFontCaption, color: color), - ], - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), + const SizedBox(width: 6), + ClideText(label, fontSize: clideFontCaption, color: color), + ], ), ); } diff --git a/lib/builtin/welcome/src/welcome_view.dart b/lib/builtin/welcome/src/welcome_view.dart index 018b4d20..a417946a 100644 --- a/lib/builtin/welcome/src/welcome_view.dart +++ b/lib/builtin/welcome/src/welcome_view.dart @@ -57,8 +57,8 @@ class _Header extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( - width: 72, - height: 72, + width: 144, + height: 144, child: ScalableImageWidget.fromSISource( si: ScalableImageSource.fromSvg(rootBundle, 'assets/logo/logo.svg'), ), @@ -272,21 +272,21 @@ class _StatusLine extends StatelessWidget { @override Widget build(BuildContext context) { + final themeName = kernel.theme.currentName; return ListenableBuilder( - listenable: kernel.ipc, + listenable: kernel.toolCheck, builder: (ctx, _) { - final connected = kernel.ipc.isConnected; - final themeName = kernel.theme.currentName; + final tc = kernel.toolCheck; return Row( children: [ ClideText('clide 2.0.0-dev', muted: true, fontSize: 12, fontFamily: clideMonoFamily), ClideText(' · ', muted: true, fontSize: 12), - ClideText( - connected ? 'daemon connected' : 'daemon disconnected', - fontSize: 12, - fontFamily: clideMonoFamily, - color: connected ? tokens.statusSuccess : tokens.statusError, - ), + if (!tc.checked) + ClideText('checking…', muted: true, fontSize: 12, fontFamily: clideMonoFamily) + else if (tc.allOk) + ClideText('application ok', fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusSuccess) + else + ClideText(tc.errors.join(' · '), fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusWarning), ClideText(' · ', muted: true, fontSize: 12), ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily), ClideText(themeName, fontSize: 12, fontFamily: clideMonoFamily, color: tokens.globalFocus), diff --git a/lib/kernel/kernel.dart b/lib/kernel/kernel.dart index f210aecb..bdd0b571 100644 --- a/lib/kernel/kernel.dart +++ b/lib/kernel/kernel.dart @@ -46,4 +46,5 @@ export 'src/theme/palette.dart'; export 'src/theme/resolver.dart'; export 'src/theme/semantic.dart'; export 'src/theme/tokens.dart'; +export 'src/tool_check.dart'; export 'src/window_controls.dart'; diff --git a/lib/kernel/src/facade.dart b/lib/kernel/src/facade.dart index 4866e1b5..e4c6a940 100644 --- a/lib/kernel/src/facade.dart +++ b/lib/kernel/src/facade.dart @@ -25,6 +25,7 @@ import 'package:clide/kernel/src/secrets.dart'; import 'package:clide/kernel/src/settings.dart'; import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/kernel/src/theme/loader.dart'; +import 'package:clide/kernel/src/tool_check.dart'; import 'package:clide/kernel/src/tray.dart'; import 'package:clide/kernel/src/window_controls.dart'; import 'package:flutter/widgets.dart'; @@ -57,6 +58,7 @@ class KernelServices { required this.project, required this.extensions, required this.window, + required this.toolCheck, }); final Logger log; @@ -82,6 +84,7 @@ class KernelServices { final ProjectManager project; final ExtensionManager extensions; final WindowControls window; + final ToolCheck toolCheck; static Future boot({ required Directory appDir, @@ -128,6 +131,7 @@ class KernelServices { final net = NetworkStatus(); final focus = FocusTracker(); final window = WindowControls(); + final toolCheck = ToolCheck(); final project = ProjectManager( log: log, events: events, @@ -192,6 +196,7 @@ class KernelServices { project: project, extensions: extensions, window: window, + toolCheck: toolCheck, ); } diff --git a/lib/kernel/src/tool_check.dart b/lib/kernel/src/tool_check.dart new file mode 100644 index 00000000..90166f91 --- /dev/null +++ b/lib/kernel/src/tool_check.dart @@ -0,0 +1,41 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +class ToolCheck extends ChangeNotifier { + bool ptycOk = false; + bool pqlOk = false; + bool tmuxOk = false; + bool gitOk = false; + bool checked = false; + + bool get allOk => ptycOk && pqlOk && tmuxOk && gitOk; + + List get errors => [ + if (!ptycOk) 'ptyc not found', + if (!pqlOk) 'pql not found', + if (!tmuxOk) 'tmux not found', + if (!gitOk) 'git not found', + ]; + + Future check() async { + final cwd = Directory.current.path; + ptycOk = File('$cwd/native/linux-x64/ptyc').existsSync() || + File('$cwd/ptyc/bin/ptyc').existsSync() || + await _which('ptyc'); + pqlOk = await _which('pql'); + tmuxOk = await _which('tmux'); + gitOk = await _which('git'); + checked = true; + notifyListeners(); + } + + static Future _which(String name) async { + try { + final r = await Process.run('which', [name]); + return r.exitCode == 0; + } catch (_) { + return false; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 9e247061..00bedcf9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:clide/app.dart'; import 'package:clide/builtin/canvas/canvas.dart'; import 'package:clide/builtin/claude/claude.dart'; @@ -107,6 +109,7 @@ Future main() async { await services.extensions.activateAll(); if (!kIsWeb) { + unawaited(services.toolCheck.check()); await services.project.loadRecents(); var opened = await services.project.openLast(); if (!opened) {