add Toolchain, GitClient, native directory picker
Toolchain centralizes binary resolution — replaces five ad-hoc mechanisms (expandedPath, _resolveGit, _resolve, _resolvePtyc, _existsOnPath). Resolves via Future.delayed after runApp to avoid blocking the merged UI/platform thread on macOS. GitClient wraps all git operations with a typed API. Every subprocess call goes through _run() using toolchain.git + toolchain.gitEnv. Replaces free functions in operations.dart. Native directory picker: NSOpenPanel on macOS (method channel in AppDelegate), GtkFileChooserDialog on Linux. Falls back to text-input dialog on web or MissingPluginException. Shows "No git repo found" dialog when the selected directory is not a git repository. PqlClient and pane commands updated to use Toolchain. ToolCheck replaced by Toolchain.missing/allOk. All IPC handlers now catch GitException to prevent unhandled exceptions on the merged thread. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8edcc78bfe
commit
73e80a55a6
@@ -0,0 +1,146 @@
|
||||
/// Centralized binary resolution for external tools.
|
||||
///
|
||||
/// Resolution runs in a background isolate via [resolvePaths] to avoid
|
||||
/// blocking the merged UI/platform thread on macOS. The result is
|
||||
/// applied on the main thread via [applyResolved].
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../src/pty/env.dart' show expandedPath;
|
||||
|
||||
/// Serializable result of tool resolution (crosses isolate boundary).
|
||||
class ResolvedPaths {
|
||||
const ResolvedPaths({
|
||||
this.git,
|
||||
this.pql,
|
||||
this.tmux,
|
||||
this.ptyc,
|
||||
this.shell,
|
||||
this.gitEnv,
|
||||
});
|
||||
|
||||
final String? git;
|
||||
final String? pql;
|
||||
final String? tmux;
|
||||
final String? ptyc;
|
||||
final String? shell;
|
||||
final Map<String, String>? gitEnv;
|
||||
}
|
||||
|
||||
class Toolchain extends ChangeNotifier {
|
||||
String? _git;
|
||||
String? _pql;
|
||||
String? _tmux;
|
||||
String? _ptyc;
|
||||
String? _shell;
|
||||
Map<String, String>? _gitEnv;
|
||||
bool _resolved = false;
|
||||
|
||||
String get git => _git ?? 'git';
|
||||
String get pql => _pql ?? 'pql';
|
||||
String get tmux => _tmux ?? 'tmux';
|
||||
String get ptyc => _ptyc ?? 'ptyc';
|
||||
String get shell => _shell ?? '/bin/bash';
|
||||
|
||||
/// Extra environment variables for git (e.g. GIT_EXEC_PATH for dugite).
|
||||
Map<String, String>? get gitEnv => _gitEnv;
|
||||
|
||||
bool get resolved => _resolved;
|
||||
bool get allOk => _resolved && missing.isEmpty;
|
||||
|
||||
List<String> get missing => [
|
||||
if (_git == null) 'git',
|
||||
if (_pql == null) 'pql',
|
||||
if (_tmux == null) 'tmux',
|
||||
if (_ptyc == null) 'ptyc',
|
||||
];
|
||||
|
||||
/// Returns a Future that completes when resolution finishes.
|
||||
Future<void> waitForResolution() {
|
||||
if (_resolved) return Future.value();
|
||||
final c = Completer<void>();
|
||||
void listener() {
|
||||
if (_resolved) {
|
||||
removeListener(listener);
|
||||
if (!c.isCompleted) c.complete();
|
||||
}
|
||||
}
|
||||
addListener(listener);
|
||||
return c.future;
|
||||
}
|
||||
|
||||
/// Apply paths resolved in a background isolate.
|
||||
void applyResolved(ResolvedPaths p) {
|
||||
_git = p.git;
|
||||
_pql = p.pql;
|
||||
_tmux = p.tmux;
|
||||
_ptyc = p.ptyc;
|
||||
_shell = p.shell;
|
||||
_gitEnv = p.gitEnv;
|
||||
_resolved = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Pure function — runs in a background isolate. All file I/O happens
|
||||
/// here, off the main thread.
|
||||
static ResolvedPaths resolvePaths({required String workspaceRoot}) {
|
||||
final dugite = '$workspaceRoot/native/dugite/bin';
|
||||
|
||||
String? git;
|
||||
Map<String, String>? gitEnv;
|
||||
final dugiteGit = _firstExisting(['$dugite/git']);
|
||||
if (dugiteGit != null) {
|
||||
git = dugiteGit;
|
||||
final dugiteRoot = File(dugiteGit).parent.parent.path;
|
||||
gitEnv = {
|
||||
'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core',
|
||||
'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates',
|
||||
};
|
||||
} else {
|
||||
git = _findOnPath('git');
|
||||
}
|
||||
|
||||
final pql = _findOnPath('pql');
|
||||
final tmux = _findOnPath('tmux');
|
||||
final shell = _findOnPath(
|
||||
Platform.environment['SHELL']?.split('/').last ?? 'bash');
|
||||
|
||||
final ptyc = _firstExisting([
|
||||
'$workspaceRoot/ptyc/bin/ptyc',
|
||||
'$workspaceRoot/native/linux-x64/ptyc',
|
||||
'$workspaceRoot/native/macos-arm64/ptyc',
|
||||
'$workspaceRoot/native/macos-x64/ptyc',
|
||||
if (Platform.environment['HOME'] case final home?)
|
||||
'$home/.local/bin/ptyc',
|
||||
]) ?? _findOnPath('ptyc');
|
||||
|
||||
return ResolvedPaths(
|
||||
git: git,
|
||||
pql: pql,
|
||||
tmux: tmux,
|
||||
ptyc: ptyc,
|
||||
shell: shell,
|
||||
gitEnv: gitEnv,
|
||||
);
|
||||
}
|
||||
|
||||
static String? _findOnPath(String name) {
|
||||
for (final dir in expandedPath.split(':')) {
|
||||
if (dir.isEmpty) continue;
|
||||
final f = File('$dir/$name');
|
||||
if (f.existsSync()) return f.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _firstExisting(List<String> candidates) {
|
||||
for (final c in candidates) {
|
||||
if (File(c).existsSync()) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user