chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist 0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is the binding floor. Pin the exact build toolchain in .fvmrc (Flutter 3.44.1). Moving to the Dart 3.9 language level switches `dart format` to the new "tall" style and enables two new lints. This commit is the resulting mechanical churn, isolated from any behaviour change: - whole-tree `dart format` reformat (tall style) - `dart fix` for unnecessary_underscores + use_null_aware_elements No runtime behaviour change; `make test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,13 +54,7 @@ class CliInstallStatus {
|
||||
|
||||
/// Result of [CliInstaller.install].
|
||||
class CliInstallResult {
|
||||
const CliInstallResult({
|
||||
required this.ok,
|
||||
required this.message,
|
||||
this.installedPath,
|
||||
this.onPath = true,
|
||||
this.fromDevTree = false,
|
||||
});
|
||||
const CliInstallResult({required this.ok, required this.message, this.installedPath, this.onPath = true, this.fromDevTree = false});
|
||||
|
||||
final bool ok;
|
||||
final String message;
|
||||
@@ -80,14 +74,10 @@ class CliInstallResult {
|
||||
/// environment, candidate client locations, the target dir) is injectable so
|
||||
/// the logic is unit-testable without a real install.
|
||||
class CliInstaller {
|
||||
CliInstaller({
|
||||
required this.resolvedExecutable,
|
||||
Map<String, String>? env,
|
||||
List<String>? bundledClientCandidates,
|
||||
String? installDir,
|
||||
}) : env = env ?? Platform.environment,
|
||||
bundledClientCandidates = bundledClientCandidates ?? _defaultBundledCandidates(resolvedExecutable, env ?? Platform.environment),
|
||||
installDir = installDir ?? _defaultInstallDir(env ?? Platform.environment);
|
||||
CliInstaller({required this.resolvedExecutable, Map<String, String>? env, List<String>? bundledClientCandidates, String? installDir})
|
||||
: env = env ?? Platform.environment,
|
||||
bundledClientCandidates = bundledClientCandidates ?? _defaultBundledCandidates(resolvedExecutable, env ?? Platform.environment),
|
||||
installDir = installDir ?? _defaultInstallDir(env ?? Platform.environment);
|
||||
|
||||
/// Path to the running Flutter GUI executable
|
||||
/// (`Platform.resolvedExecutable`).
|
||||
@@ -132,7 +122,8 @@ class CliInstaller {
|
||||
if (src == null) {
|
||||
return const CliInstallResult(
|
||||
ok: false,
|
||||
message: 'No bundled clide client found to install. Build with '
|
||||
message:
|
||||
'No bundled clide client found to install. Build with '
|
||||
'`make build` so the C client ships inside the app bundle.',
|
||||
);
|
||||
}
|
||||
@@ -203,11 +194,7 @@ class CliInstaller {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _expandedPath() => expandedPath(
|
||||
env['PATH'] ?? '',
|
||||
macOS: Platform.isMacOS,
|
||||
home: env['HOME'] ?? '',
|
||||
);
|
||||
String _expandedPath() => expandedPath(env['PATH'] ?? '', macOS: Platform.isMacOS, home: env['HOME'] ?? '');
|
||||
|
||||
static String _defaultInstallDir(Map<String, String> env) => '${env['HOME'] ?? ''}/.local/bin';
|
||||
|
||||
@@ -217,10 +204,7 @@ class CliInstaller {
|
||||
/// `Contents/MacOS/` on macOS).
|
||||
static List<String> _defaultBundledCandidates(String resolvedExecutable, Map<String, String> env) {
|
||||
final exeDir = File(resolvedExecutable).parent.path;
|
||||
return [
|
||||
if ((env['CLIDE_CLI_BIN'] ?? '').isNotEmpty) env['CLIDE_CLI_BIN']!,
|
||||
'$exeDir/clide-cli',
|
||||
];
|
||||
return [if ((env['CLIDE_CLI_BIN'] ?? '').isNotEmpty) env['CLIDE_CLI_BIN']!, '$exeDir/clide-cli'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,12 +223,7 @@ bool isDevTreeClient(String path) => _devTreeClient.hasMatch(path);
|
||||
/// platform-parameterized function so both branches are testable off-platform.
|
||||
String expandedPath(String base, {required bool macOS, String home = ''}) {
|
||||
if (!macOS) return base;
|
||||
final extras = <String>[
|
||||
if (home.isNotEmpty) '$home/.local/bin',
|
||||
'/opt/homebrew/bin',
|
||||
'/opt/homebrew/sbin',
|
||||
'/usr/local/bin',
|
||||
];
|
||||
final extras = <String>[if (home.isNotEmpty) '$home/.local/bin', '/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin'];
|
||||
final existing = base.split(':').toSet();
|
||||
final missing = extras.where((p) => !existing.contains(p));
|
||||
if (missing.isEmpty) return base;
|
||||
|
||||
@@ -14,10 +14,7 @@ class ClideClipboard {
|
||||
final int historyLimit;
|
||||
final Map<Type, List<Object>> _history = {};
|
||||
|
||||
Future<void> write<T extends Object>(
|
||||
T value, {
|
||||
String Function(T)? toPlain,
|
||||
}) async {
|
||||
Future<void> write<T extends Object>(T value, {String Function(T)? toPlain}) async {
|
||||
final bucket = _history.putIfAbsent(T, () => <Object>[]);
|
||||
bucket.insert(0, value);
|
||||
if (bucket.length > historyLimit) bucket.removeLast();
|
||||
|
||||
@@ -5,9 +5,7 @@ import 'package:flutter/services.dart';
|
||||
/// (modifiers sorted, lowercased) so equality works for lookup keys.
|
||||
@immutable
|
||||
class Keybinding {
|
||||
Keybinding({required Set<String> modifiers, required String key})
|
||||
: modifiers = _canonModifiers(modifiers),
|
||||
key = key.toLowerCase();
|
||||
Keybinding({required Set<String> modifiers, required String key}) : modifiers = _canonModifiers(modifiers), key = key.toLowerCase();
|
||||
|
||||
final List<String> modifiers;
|
||||
final String key;
|
||||
|
||||
@@ -17,19 +17,12 @@ class CommandRegistry extends ChangeNotifier {
|
||||
Iterable<CommandContribution> get all => _byCommand.values;
|
||||
CommandContribution? get(String command) => _byCommand[command];
|
||||
|
||||
Future<IpcResponse> execute(
|
||||
String command, {
|
||||
List<String> args = const [],
|
||||
}) async {
|
||||
Future<IpcResponse> execute(String command, {List<String> args = const []}) async {
|
||||
final c = _byCommand[command];
|
||||
if (c == null) {
|
||||
return IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(
|
||||
code: IpcExitCode.notFound,
|
||||
kind: IpcErrorKind.notFound,
|
||||
message: 'no such command: $command',
|
||||
),
|
||||
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: 'no such command: $command'),
|
||||
);
|
||||
}
|
||||
return c.run(args);
|
||||
|
||||
@@ -2,10 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef DialogBuilder<T> = Widget Function(
|
||||
BuildContext context,
|
||||
void Function([T? result]) dismiss,
|
||||
);
|
||||
typedef DialogBuilder<T> = Widget Function(BuildContext context, void Function([T? result]) dismiss);
|
||||
|
||||
/// Single-at-a-time modal router.
|
||||
///
|
||||
@@ -70,12 +67,7 @@ class _Queued {
|
||||
/// Hosts the current dialog from [DialogRouter]. Place high in the tree
|
||||
/// (inside the WidgetsApp) so dialogs overlay every other surface.
|
||||
class DialogHost extends StatelessWidget {
|
||||
const DialogHost({
|
||||
super.key,
|
||||
required this.router,
|
||||
required this.child,
|
||||
this.backdropColor = const Color(0xC0000000),
|
||||
});
|
||||
const DialogHost({super.key, required this.router, required this.child, this.backdropColor = const Color(0xC0000000)});
|
||||
|
||||
final DialogRouter router;
|
||||
final Widget child;
|
||||
@@ -98,10 +90,7 @@ class DialogHost extends StatelessWidget {
|
||||
child: ColoredBox(
|
||||
color: backdropColor,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: b(ctx, router.dismiss),
|
||||
),
|
||||
child: GestureDetector(onTap: () {}, child: b(ctx, router.dismiss)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
class Message {
|
||||
Message({
|
||||
required this.publisher,
|
||||
required this.channel,
|
||||
required this.data,
|
||||
}) : timestamp = DateTime.now();
|
||||
Message({required this.publisher, required this.channel, required this.data}) : timestamp = DateTime.now();
|
||||
|
||||
final String publisher;
|
||||
final String channel;
|
||||
|
||||
@@ -13,13 +13,7 @@ class ClideEventEnvelope {
|
||||
final ClideEvent event;
|
||||
final DateTime timestamp;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'v': 1,
|
||||
'subsystem': event.subsystem,
|
||||
'kind': event.kind,
|
||||
'ts': timestamp.toIso8601String(),
|
||||
'data': event.payload(),
|
||||
};
|
||||
Map<String, Object?> toJson() => {'v': 1, 'subsystem': event.subsystem, 'kind': event.kind, 'ts': timestamp.toIso8601String(), 'data': event.payload()};
|
||||
}
|
||||
|
||||
class DaemonConnectionChanged extends ClideEvent {
|
||||
@@ -89,12 +83,7 @@ class ExtensionDeactivated extends ClideEvent {
|
||||
/// narrow by subsystem+kind, or register a converter that emits a typed
|
||||
/// `ClideEvent` subclass into the bus.
|
||||
class DaemonEvent extends ClideEvent {
|
||||
const DaemonEvent({
|
||||
required this.subsystem,
|
||||
required this.kind,
|
||||
required this.data,
|
||||
required this.ts,
|
||||
});
|
||||
const DaemonEvent({required this.subsystem, required this.kind, required this.data, required this.ts});
|
||||
|
||||
@override
|
||||
final String subsystem;
|
||||
@@ -145,26 +134,22 @@ class TeamMemberJoined extends ClideEvent {
|
||||
String get kind => 'member-joined';
|
||||
@override
|
||||
Map<String, Object?> payload() => {
|
||||
'team': team,
|
||||
'agentId': agentId,
|
||||
'name': name,
|
||||
'agentType': agentType,
|
||||
'paneId': paneId,
|
||||
if (model != null) 'model': model,
|
||||
if (color != null) 'color': color,
|
||||
if (cwd != null) 'cwd': cwd,
|
||||
if (transcriptPath != null) 'transcriptPath': transcriptPath,
|
||||
};
|
||||
'team': team,
|
||||
'agentId': agentId,
|
||||
'name': name,
|
||||
'agentType': agentType,
|
||||
'paneId': paneId,
|
||||
if (model != null) 'model': model,
|
||||
if (color != null) 'color': color,
|
||||
if (cwd != null) 'cwd': cwd,
|
||||
if (transcriptPath != null) 'transcriptPath': transcriptPath,
|
||||
};
|
||||
}
|
||||
|
||||
/// A Claude Code tmux teammate's pane went away (it exited or the team
|
||||
/// dissolved) — T-139.
|
||||
class TeamMemberLeft extends ClideEvent {
|
||||
const TeamMemberLeft({
|
||||
required this.team,
|
||||
required this.agentId,
|
||||
required this.paneId,
|
||||
});
|
||||
const TeamMemberLeft({required this.team, required this.agentId, required this.paneId});
|
||||
|
||||
final String team;
|
||||
final String agentId;
|
||||
|
||||
@@ -147,13 +147,7 @@ class KernelServices {
|
||||
final settings = SettingsStore(appDir: appDir);
|
||||
await settings.load();
|
||||
|
||||
final i18n = I18n(
|
||||
loader: i18nLoader,
|
||||
log: log,
|
||||
defaultLocale: defaultLocale,
|
||||
initialLocale: initialLocale,
|
||||
availableLocales: availableLocales,
|
||||
);
|
||||
final i18n = I18n(loader: i18nLoader, log: log, defaultLocale: defaultLocale, initialLocale: initialLocale, availableLocales: availableLocales);
|
||||
for (final ns in preloadNamespaces) {
|
||||
await i18n.ensureNamespaceLoaded(ns);
|
||||
}
|
||||
@@ -193,7 +187,8 @@ class KernelServices {
|
||||
onProjectOpen: onProjectOpen,
|
||||
onValidateProject: onValidateProject,
|
||||
);
|
||||
final ipc = isolateClient ??
|
||||
final ipc =
|
||||
isolateClient ??
|
||||
(daemonClientFactory != null
|
||||
? daemonClientFactory(log, events, arrangement, panels)
|
||||
: DaemonClient(
|
||||
@@ -309,11 +304,7 @@ class KernelServices {
|
||||
}
|
||||
|
||||
class ClideKernel extends InheritedWidget {
|
||||
const ClideKernel({
|
||||
super.key,
|
||||
required this.services,
|
||||
required super.child,
|
||||
});
|
||||
const ClideKernel({super.key, required this.services, required super.child});
|
||||
|
||||
final KernelServices services;
|
||||
|
||||
|
||||
@@ -16,10 +16,7 @@ class FilesDropped extends ClideEvent {
|
||||
@override
|
||||
String get kind => 'dropped';
|
||||
@override
|
||||
Map<String, Object?> payload() => {
|
||||
'paths': paths,
|
||||
'slot': slot.value,
|
||||
};
|
||||
Map<String, Object?> payload() => {'paths': paths, 'slot': slot.value};
|
||||
}
|
||||
|
||||
/// Tier-0 stub for file pickers and drop targets.
|
||||
@@ -33,17 +30,11 @@ class FileServices {
|
||||
FileServices(this._events);
|
||||
final DaemonBus _events;
|
||||
|
||||
Future<List<String>> pickOpen({
|
||||
List<String> extensions = const [],
|
||||
bool multiple = false,
|
||||
}) async {
|
||||
Future<List<String>> pickOpen({List<String> extensions = const [], bool multiple = false}) async {
|
||||
throw UnimplementedError('pickOpen — wired in a later tier');
|
||||
}
|
||||
|
||||
Future<String?> pickSave({
|
||||
String? defaultName,
|
||||
List<String> extensions = const [],
|
||||
}) async {
|
||||
Future<String?> pickSave({String? defaultName, List<String> extensions = const []}) async {
|
||||
throw UnimplementedError('pickSave — wired in a later tier');
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ class FocusTracker extends ChangeNotifier {
|
||||
/// of the three-column layout (sidebar → workspace → context); the
|
||||
/// statusbar / toolbar aren't included because they don't host
|
||||
/// keyboard-active content.
|
||||
static const List<SlotId> traversalOrder = [
|
||||
Slots.sidebar,
|
||||
Slots.workspace,
|
||||
Slots.contextPanel,
|
||||
];
|
||||
static const List<SlotId> traversalOrder = [Slots.sidebar, Slots.workspace, Slots.contextPanel];
|
||||
|
||||
SlotId? get activeSlot => _slot;
|
||||
String? get activeContributionId => _contributionId;
|
||||
|
||||
@@ -14,22 +14,14 @@ import 'package:flutter/foundation.dart';
|
||||
/// canonicalized by omitting it (not empty string) so equality works.
|
||||
@immutable
|
||||
class FallbackChain {
|
||||
const FallbackChain({
|
||||
required this.current,
|
||||
required this.defaultLocale,
|
||||
});
|
||||
const FallbackChain({required this.current, required this.defaultLocale});
|
||||
|
||||
final Locale current;
|
||||
final Locale defaultLocale;
|
||||
|
||||
List<Locale> resolve() {
|
||||
final out = <Locale>[];
|
||||
for (final l in [
|
||||
current,
|
||||
Locale(current.languageCode),
|
||||
defaultLocale,
|
||||
Locale(defaultLocale.languageCode),
|
||||
]) {
|
||||
for (final l in [current, Locale(current.languageCode), defaultLocale, Locale(defaultLocale.languageCode)]) {
|
||||
final canon = _canon(l);
|
||||
if (!out.any((e) => _canon(e) == canon)) {
|
||||
out.add(l);
|
||||
|
||||
@@ -26,9 +26,9 @@ class I18n extends ChangeNotifier {
|
||||
required Locale defaultLocale,
|
||||
Locale? initialLocale,
|
||||
List<Locale> availableLocales = const [Locale('en', 'US')],
|
||||
}) : _defaultLocale = defaultLocale,
|
||||
_current = initialLocale ?? defaultLocale,
|
||||
_available = List<Locale>.unmodifiable(availableLocales);
|
||||
}) : _defaultLocale = defaultLocale,
|
||||
_current = initialLocale ?? defaultLocale,
|
||||
_available = List<Locale>.unmodifiable(availableLocales);
|
||||
|
||||
final CatalogLoader loader;
|
||||
final Logger log;
|
||||
@@ -50,11 +50,7 @@ class I18n extends ChangeNotifier {
|
||||
|
||||
/// Register a catalog that was loaded outside of [loader] — e.g. by the
|
||||
/// ExtensionManager when a third-party extension activates.
|
||||
void registerCatalog(
|
||||
String namespace,
|
||||
Locale locale,
|
||||
Map<String, Object?> catalog,
|
||||
) {
|
||||
void registerCatalog(String namespace, Locale locale, Map<String, Object?> catalog) {
|
||||
_cache.putIfAbsent(namespace, () => <Locale, Map<String, Object?>>{})[locale] = catalog;
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -85,14 +81,8 @@ class I18n extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> _ensureLoaded(String namespace) async {
|
||||
final byLocale = _cache.putIfAbsent(
|
||||
namespace,
|
||||
() => <Locale, Map<String, Object?>>{},
|
||||
);
|
||||
final chain = FallbackChain(
|
||||
current: _current,
|
||||
defaultLocale: _defaultLocale,
|
||||
).resolve();
|
||||
final byLocale = _cache.putIfAbsent(namespace, () => <Locale, Map<String, Object?>>{});
|
||||
final chain = FallbackChain(current: _current, defaultLocale: _defaultLocale).resolve();
|
||||
for (final l in chain) {
|
||||
if (byLocale.containsKey(l)) continue;
|
||||
byLocale[l] = await loader.load(namespace, l);
|
||||
@@ -102,24 +92,14 @@ class I18n extends ChangeNotifier {
|
||||
/// Look up a key, walking the locale fallback chain. Returns the
|
||||
/// placeholder if nothing hits; returns the key itself when placeholder
|
||||
/// is null (developer fallback — keys are more useful than blanks).
|
||||
String string(
|
||||
String key, {
|
||||
required String namespace,
|
||||
String? placeholder,
|
||||
}) {
|
||||
String string(String key, {required String namespace, String? placeholder}) {
|
||||
final byLocale = _cache[namespace];
|
||||
if (byLocale == null) {
|
||||
_warnOnce(
|
||||
'$namespace::MISSING_NAMESPACE::$key',
|
||||
'i18n: namespace not registered: $namespace (key: $key)',
|
||||
);
|
||||
_warnOnce('$namespace::MISSING_NAMESPACE::$key', 'i18n: namespace not registered: $namespace (key: $key)');
|
||||
return placeholder ?? key;
|
||||
}
|
||||
|
||||
final chain = FallbackChain(
|
||||
current: _current,
|
||||
defaultLocale: _defaultLocale,
|
||||
).resolve();
|
||||
final chain = FallbackChain(current: _current, defaultLocale: _defaultLocale).resolve();
|
||||
|
||||
for (final locale in chain) {
|
||||
final catalog = byLocale[locale];
|
||||
@@ -128,22 +108,14 @@ class I18n extends ChangeNotifier {
|
||||
if (hit != null) return hit;
|
||||
}
|
||||
|
||||
_warnOnce(
|
||||
'$namespace::${_current.languageCode}::$key',
|
||||
'i18n: missing key "$key" in namespace "$namespace" (locale ${_current.toString()})',
|
||||
);
|
||||
_warnOnce('$namespace::${_current.languageCode}::$key', 'i18n: missing key "$key" in namespace "$namespace" (locale ${_current.toString()})');
|
||||
return placeholder ?? key;
|
||||
}
|
||||
|
||||
/// [string] + naive `replaceAll` interpolation per replacer.
|
||||
/// Matches fframe: replacers whose [I18nReplacer.from] isn't present
|
||||
/// are silent no-ops.
|
||||
String interpolated(
|
||||
String key, {
|
||||
required String namespace,
|
||||
String? placeholder,
|
||||
List<I18nReplacer> replacers = const [],
|
||||
}) {
|
||||
String interpolated(String key, {required String namespace, String? placeholder, List<I18nReplacer> replacers = const []}) {
|
||||
var out = string(key, namespace: namespace, placeholder: placeholder);
|
||||
for (final r in replacers) {
|
||||
out = out.replaceAll(r.from, r.replace);
|
||||
|
||||
@@ -10,13 +10,7 @@ import 'package:clide/kernel/src/log.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class DaemonClient extends ChangeNotifier {
|
||||
DaemonClient({
|
||||
required String socketPath,
|
||||
required Logger log,
|
||||
required DaemonBus events,
|
||||
}) : _socketPath = socketPath,
|
||||
_log = log,
|
||||
_events = events;
|
||||
DaemonClient({required String socketPath, required Logger log, required DaemonBus events}) : _socketPath = socketPath, _log = log, _events = events;
|
||||
|
||||
String _socketPath;
|
||||
String get socketPath => _socketPath;
|
||||
@@ -85,10 +79,7 @@ class DaemonClient extends ChangeNotifier {
|
||||
await _connect();
|
||||
}
|
||||
|
||||
Future<IpcResponse> request(
|
||||
String cmd, {
|
||||
Map<String, Object?> args = const {},
|
||||
}) async {
|
||||
Future<IpcResponse> request(String cmd, {Map<String, Object?> args = const {}}) async {
|
||||
if (!_connected || _socket == null) {
|
||||
// A connection attempt is in flight (startup or reconnect) — wait
|
||||
// for it rather than failing instantly, so queries issued during
|
||||
@@ -100,11 +91,7 @@ class DaemonClient extends ChangeNotifier {
|
||||
if (!_connected || _socket == null) {
|
||||
return IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: 'daemon not connected',
|
||||
),
|
||||
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'daemon not connected'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -150,15 +137,19 @@ class DaemonClient extends ChangeNotifier {
|
||||
_backoff = const Duration(milliseconds: 200);
|
||||
_setConnected(true);
|
||||
_log.info('ipc', 'connected to $_socketPath');
|
||||
socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).listen(
|
||||
_handleLine,
|
||||
onDone: _handleDisconnect,
|
||||
onError: (Object e) {
|
||||
_log.warn('ipc', 'socket error', error: e);
|
||||
_handleDisconnect();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
socket
|
||||
.cast<List<int>>()
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.listen(
|
||||
_handleLine,
|
||||
onDone: _handleDisconnect,
|
||||
onError: (Object e) {
|
||||
_log.warn('ipc', 'socket error', error: e);
|
||||
_handleDisconnect();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.debug('ipc', 'connect failed ($e); retry in ${_backoff.inMilliseconds}ms');
|
||||
_scheduleReconnect();
|
||||
@@ -174,12 +165,7 @@ class DaemonClient extends ChangeNotifier {
|
||||
final c = _pending.remove(r.id);
|
||||
if (c != null && !c.isCompleted) c.complete(r);
|
||||
case IpcEvent e:
|
||||
_events.emit(DaemonEvent(
|
||||
subsystem: e.subsystem,
|
||||
kind: e.kind,
|
||||
data: e.data,
|
||||
ts: e.timestamp,
|
||||
));
|
||||
_events.emit(DaemonEvent(subsystem: e.subsystem, kind: e.kind, data: e.data, ts: e.timestamp));
|
||||
case IpcRequest _:
|
||||
_log.warn('ipc', 'daemon sent a request — unexpected');
|
||||
}
|
||||
@@ -196,11 +182,7 @@ class DaemonClient extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _failPending(String reason) {
|
||||
final err = IpcError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: reason,
|
||||
);
|
||||
final err = IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: reason);
|
||||
for (final entry in _pending.entries) {
|
||||
if (!entry.value.isCompleted) {
|
||||
entry.value.complete(IpcResponse.err(id: entry.key, error: err));
|
||||
@@ -213,9 +195,7 @@ class DaemonClient extends ChangeNotifier {
|
||||
if (_disposed) return;
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer(_backoff, _connect);
|
||||
_backoff = Duration(
|
||||
milliseconds: math.min(_backoff.inMilliseconds * 2, 5000),
|
||||
);
|
||||
_backoff = Duration(milliseconds: math.min(_backoff.inMilliseconds * 2, 5000));
|
||||
}
|
||||
|
||||
void _setConnected(bool v) {
|
||||
|
||||
@@ -23,11 +23,11 @@ enum KeyModifier {
|
||||
|
||||
/// Display string used in palette + tooltip hints.
|
||||
String get display => switch (this) {
|
||||
KeyModifier.ctrl => 'Ctrl',
|
||||
KeyModifier.alt => 'Alt',
|
||||
KeyModifier.shift => 'Shift',
|
||||
KeyModifier.meta => 'Cmd',
|
||||
};
|
||||
KeyModifier.ctrl => 'Ctrl',
|
||||
KeyModifier.alt => 'Alt',
|
||||
KeyModifier.shift => 'Shift',
|
||||
KeyModifier.meta => 'Cmd',
|
||||
};
|
||||
}
|
||||
|
||||
/// A modifier-set + a single key, identified by layout-independent
|
||||
|
||||
@@ -23,11 +23,7 @@ import 'when_clause.dart';
|
||||
/// to fire when the sequence matches and the when-clause is true.
|
||||
@immutable
|
||||
class KeymapBinding {
|
||||
const KeymapBinding({
|
||||
required this.sequence,
|
||||
required this.intent,
|
||||
this.when,
|
||||
});
|
||||
const KeymapBinding({required this.sequence, required this.intent, this.when});
|
||||
|
||||
/// Convenience constructor for a single-chord binding.
|
||||
KeymapBinding.chord(KeyChord chord, {required this.intent, this.when}) : sequence = [chord];
|
||||
@@ -204,9 +200,7 @@ class Keymap {
|
||||
/// must come first. We don't dedupe — a no-op match in a later layer
|
||||
/// just earns the first slot.
|
||||
static List<KeymapBinding> _flatten(List<KeymapLayer> layers) {
|
||||
return [
|
||||
for (final l in layers.reversed) ...l.bindings,
|
||||
];
|
||||
return [for (final l in layers.reversed) ...l.bindings];
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -41,13 +41,10 @@ const String kKeymapOverridesSetting = 'app.keymap.overrides';
|
||||
const String kKeymapUserFile = 'keybindings.yaml';
|
||||
|
||||
class KeymapService extends ChangeNotifier {
|
||||
KeymapService({
|
||||
required SettingsStore settings,
|
||||
required Directory appDir,
|
||||
AssetBundle? bundle,
|
||||
}) : _settings = settings,
|
||||
_appDir = appDir,
|
||||
_bundle = bundle ?? rootBundle;
|
||||
KeymapService({required SettingsStore settings, required Directory appDir, AssetBundle? bundle})
|
||||
: _settings = settings,
|
||||
_appDir = appDir,
|
||||
_bundle = bundle ?? rootBundle;
|
||||
|
||||
final SettingsStore _settings;
|
||||
final Directory _appDir;
|
||||
@@ -127,11 +124,7 @@ class KeymapService extends ChangeNotifier {
|
||||
/// defaults, the user can override either via the user file or
|
||||
/// settings overlay.
|
||||
void registerCommandBinding(String chordSpec, String commandId, {String? when}) {
|
||||
_contributions.add(KeymapBinding(
|
||||
sequence: KeyChord.parseSequence(chordSpec),
|
||||
intent: InvokeCommandIntent(commandId),
|
||||
when: WhenExpr.tryParse(when),
|
||||
));
|
||||
_contributions.add(KeymapBinding(sequence: KeyChord.parseSequence(chordSpec), intent: InvokeCommandIntent(commandId), when: WhenExpr.tryParse(when)));
|
||||
_rebuildActive();
|
||||
}
|
||||
|
||||
@@ -149,12 +142,7 @@ class KeymapService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _rebuildActive() {
|
||||
final layers = <KeymapLayer>[
|
||||
if (_preset != null) _preset!,
|
||||
KeymapLayer(name: 'contributions', bindings: List.unmodifiable(_contributions)),
|
||||
if (_userFile != null) _userFile!,
|
||||
if (_settingsOverlay != null) _settingsOverlay!,
|
||||
];
|
||||
final layers = <KeymapLayer>[?_preset, KeymapLayer(name: 'contributions', bindings: List.unmodifiable(_contributions)), ?_userFile, ?_settingsOverlay];
|
||||
_active = Keymap(layers);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -33,18 +33,9 @@ enum SeqOutcome {
|
||||
|
||||
@immutable
|
||||
class SeqResult {
|
||||
const SeqResult.fired(Intent this.intent, this.count)
|
||||
: outcome = SeqOutcome.fired,
|
||||
passKey = null;
|
||||
const SeqResult.pending()
|
||||
: outcome = SeqOutcome.pending,
|
||||
intent = null,
|
||||
count = 1,
|
||||
passKey = null;
|
||||
const SeqResult.unmatched(this.passKey)
|
||||
: outcome = SeqOutcome.unmatched,
|
||||
intent = null,
|
||||
count = 1;
|
||||
const SeqResult.fired(Intent this.intent, this.count) : outcome = SeqOutcome.fired, passKey = null;
|
||||
const SeqResult.pending() : outcome = SeqOutcome.pending, intent = null, count = 1, passKey = null;
|
||||
const SeqResult.unmatched(this.passKey) : outcome = SeqOutcome.unmatched, intent = null, count = 1;
|
||||
|
||||
final SeqOutcome outcome;
|
||||
final Intent? intent;
|
||||
@@ -53,12 +44,9 @@ class SeqResult {
|
||||
}
|
||||
|
||||
class SequenceMatcher {
|
||||
SequenceMatcher({
|
||||
required Keymap Function() keymap,
|
||||
required Map<String, bool> Function() context,
|
||||
this.captureCounts = true,
|
||||
}) : _keymap = keymap,
|
||||
_context = context;
|
||||
SequenceMatcher({required Keymap Function() keymap, required Map<String, bool> Function() context, this.captureCounts = true})
|
||||
: _keymap = keymap,
|
||||
_context = context;
|
||||
|
||||
final Keymap Function() _keymap;
|
||||
final Map<String, bool> Function() _context;
|
||||
|
||||
+2
-16
@@ -4,14 +4,7 @@ import 'dart:io';
|
||||
enum LogLevel { trace, debug, info, warn, error }
|
||||
|
||||
class LogRecord {
|
||||
LogRecord({
|
||||
required this.level,
|
||||
required this.source,
|
||||
required this.message,
|
||||
required this.timestamp,
|
||||
this.error,
|
||||
this.stackTrace,
|
||||
});
|
||||
LogRecord({required this.level, required this.source, required this.message, required this.timestamp, this.error, this.stackTrace});
|
||||
|
||||
final LogLevel level;
|
||||
final String source;
|
||||
@@ -51,14 +44,7 @@ class Logger {
|
||||
|
||||
void _emit(LogLevel level, String source, String message, {Object? error, StackTrace? stackTrace}) {
|
||||
if (level.index < minLevel.index) return;
|
||||
final rec = LogRecord(
|
||||
level: level,
|
||||
source: source,
|
||||
message: message,
|
||||
timestamp: DateTime.now().toUtc(),
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
final rec = LogRecord(level: level, source: source, message: message, timestamp: DateTime.now().toUtc(), error: error, stackTrace: stackTrace);
|
||||
for (final sink in _sinks) {
|
||||
try {
|
||||
sink(rec);
|
||||
|
||||
@@ -6,13 +6,8 @@ enum NotificationLevel { info, warning, error, success }
|
||||
|
||||
@immutable
|
||||
class ClideNotification {
|
||||
ClideNotification({
|
||||
required this.id,
|
||||
required this.level,
|
||||
required this.message,
|
||||
this.title,
|
||||
this.duration = const Duration(seconds: 4),
|
||||
}) : createdAt = DateTime.now().toUtc();
|
||||
ClideNotification({required this.id, required this.level, required this.message, this.title, this.duration = const Duration(seconds: 4)})
|
||||
: createdAt = DateTime.now().toUtc();
|
||||
|
||||
final String id;
|
||||
final NotificationLevel level;
|
||||
@@ -41,20 +36,9 @@ class Notifications extends ChangeNotifier {
|
||||
if (_active.length != before) notifyListeners();
|
||||
}
|
||||
|
||||
void _push(
|
||||
NotificationLevel level,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration? duration,
|
||||
}) {
|
||||
void _push(NotificationLevel level, String message, {String? title, Duration? duration}) {
|
||||
final id = 'n${_seq++}';
|
||||
final n = ClideNotification(
|
||||
id: id,
|
||||
level: level,
|
||||
message: message,
|
||||
title: title,
|
||||
duration: duration ?? const Duration(seconds: 4),
|
||||
);
|
||||
final n = ClideNotification(id: id, level: level, message: message, title: title, duration: duration ?? const Duration(seconds: 4));
|
||||
_active.add(n);
|
||||
_timers[id] = Timer(n.duration, () => dismiss(id));
|
||||
notifyListeners();
|
||||
|
||||
@@ -16,9 +16,7 @@ class OsLifecycleEvent extends ClideEvent {
|
||||
}
|
||||
|
||||
class OsBridge {
|
||||
OsBridge({required Logger log, required DaemonBus events})
|
||||
: _log = log,
|
||||
_events = events;
|
||||
OsBridge({required Logger log, required DaemonBus events}) : _log = log, _events = events;
|
||||
|
||||
final Logger _log;
|
||||
final DaemonBus _events;
|
||||
|
||||
@@ -19,13 +19,7 @@ class LayoutArrangement extends ChangeNotifier {
|
||||
_focusModeSnapshot = null;
|
||||
_focusModeSlot = null;
|
||||
for (final slot in preset.slots) {
|
||||
_state[slot.slot] = _SlotState(
|
||||
position: slot.position,
|
||||
size: slot.defaultSize,
|
||||
minSize: slot.minSize,
|
||||
maxSize: slot.maxSize,
|
||||
visible: slot.visible,
|
||||
);
|
||||
_state[slot.slot] = _SlotState(position: slot.position, size: slot.defaultSize, minSize: slot.minSize, maxSize: slot.maxSize, visible: slot.visible);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -131,26 +125,15 @@ class LayoutArrangement extends ChangeNotifier {
|
||||
|
||||
void registerSlotsInto(PanelRegistry registry, LayoutPresetContribution preset) {
|
||||
for (final slot in preset.slots) {
|
||||
registry.registerSlot(SlotDefinition(
|
||||
id: slot.slot,
|
||||
position: slot.position,
|
||||
defaultSize: slot.defaultSize,
|
||||
minSize: slot.minSize,
|
||||
maxSize: slot.maxSize,
|
||||
));
|
||||
registry.registerSlot(
|
||||
SlotDefinition(id: slot.slot, position: slot.position, defaultSize: slot.defaultSize, minSize: slot.minSize, maxSize: slot.maxSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SlotState {
|
||||
const _SlotState({
|
||||
required this.position,
|
||||
this.size,
|
||||
this.minSize,
|
||||
this.maxSize,
|
||||
this.visible = true,
|
||||
this.collapsed = false,
|
||||
});
|
||||
const _SlotState({required this.position, this.size, this.minSize, this.maxSize, this.visible = true, this.collapsed = false});
|
||||
|
||||
final SlotPosition position;
|
||||
final double? size;
|
||||
@@ -159,14 +142,7 @@ class _SlotState {
|
||||
final bool visible;
|
||||
final bool collapsed;
|
||||
|
||||
_SlotState copyWith({
|
||||
SlotPosition? position,
|
||||
double? size,
|
||||
double? minSize,
|
||||
double? maxSize,
|
||||
bool? visible,
|
||||
bool? collapsed,
|
||||
}) {
|
||||
_SlotState copyWith({SlotPosition? position, double? size, double? minSize, double? maxSize, bool? visible, bool? collapsed}) {
|
||||
return _SlotState(
|
||||
position: position ?? this.position,
|
||||
size: size ?? this.size,
|
||||
|
||||
@@ -12,13 +12,7 @@ import 'package:flutter/widgets.dart';
|
||||
/// coarse step). Exposes a `slider` Semantics node so screen readers
|
||||
/// announce the current width.
|
||||
class DragResizeHandle extends StatefulWidget {
|
||||
const DragResizeHandle({
|
||||
super.key,
|
||||
required this.arrangement,
|
||||
required this.slot,
|
||||
required this.axis,
|
||||
this.thickness = DragResizeHandle.defaultThickness,
|
||||
});
|
||||
const DragResizeHandle({super.key, required this.arrangement, required this.slot, required this.axis, this.thickness = DragResizeHandle.defaultThickness});
|
||||
|
||||
final LayoutArrangement arrangement;
|
||||
final SlotId slot;
|
||||
@@ -155,11 +149,7 @@ class _BumpIntent extends Intent {
|
||||
/// delta = right/down. Context-panel sits on the right edge of the
|
||||
/// app, so we flip the sign there — right-arrow should *shrink* it,
|
||||
/// matching how dragging the left-edge handle rightward works.
|
||||
double bumpedSlotSize({
|
||||
required SlotId slot,
|
||||
required double current,
|
||||
required double rawDelta,
|
||||
}) {
|
||||
double bumpedSlotSize({required SlotId slot, required double current, required double rawDelta}) {
|
||||
final delta = slot == Slots.contextPanel ? -rawDelta : rawDelta;
|
||||
return current + delta;
|
||||
}
|
||||
|
||||
@@ -11,41 +11,13 @@ import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||
/// context 420 (drag 220–420)
|
||||
/// statusbar 26 (fixed height strip)
|
||||
LayoutPresetContribution classicPreset() => const LayoutPresetContribution(
|
||||
id: 'builtin.default-layout.classic',
|
||||
displayName: 'Classic',
|
||||
slots: [
|
||||
LayoutSlot(
|
||||
slot: Slots.sidebar,
|
||||
position: SlotPosition.left,
|
||||
defaultSize: 400,
|
||||
minSize: 180,
|
||||
maxSize: 400,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.workspace,
|
||||
position: SlotPosition.center,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.contextPanel,
|
||||
position: SlotPosition.right,
|
||||
defaultSize: 420,
|
||||
minSize: 220,
|
||||
maxSize: 1000,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.dock,
|
||||
position: SlotPosition.bottom,
|
||||
defaultSize: 200,
|
||||
minSize: 100,
|
||||
maxSize: 600,
|
||||
visible: false,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.statusbar,
|
||||
position: SlotPosition.bottom,
|
||||
defaultSize: 26,
|
||||
minSize: 26,
|
||||
maxSize: 26,
|
||||
),
|
||||
],
|
||||
);
|
||||
id: 'builtin.default-layout.classic',
|
||||
displayName: 'Classic',
|
||||
slots: [
|
||||
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 400, minSize: 180, maxSize: 400),
|
||||
LayoutSlot(slot: Slots.workspace, position: SlotPosition.center),
|
||||
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 420, minSize: 220, maxSize: 1000),
|
||||
LayoutSlot(slot: Slots.dock, position: SlotPosition.bottom, defaultSize: 200, minSize: 100, maxSize: 600, visible: false),
|
||||
LayoutSlot(slot: Slots.statusbar, position: SlotPosition.bottom, defaultSize: 26, minSize: 26, maxSize: 26),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -4,13 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class SlotDefinition {
|
||||
const SlotDefinition({
|
||||
required this.id,
|
||||
required this.position,
|
||||
this.defaultSize,
|
||||
this.minSize,
|
||||
this.maxSize,
|
||||
});
|
||||
const SlotDefinition({required this.id, required this.position, this.defaultSize, this.minSize, this.maxSize});
|
||||
|
||||
final SlotId id;
|
||||
final SlotPosition position;
|
||||
|
||||
@@ -20,13 +20,7 @@ List<ViewPane> snapshotViewPanes(PanelRegistry panels, LayoutArrangement arrange
|
||||
final activeId = panels.activeTabIn(slot.id);
|
||||
final visible = arrangement.isVisible(slot.id);
|
||||
for (final tab in panels.tabsFor(slot.id)) {
|
||||
out.add(ViewPane(
|
||||
id: tab.id,
|
||||
slot: slot.id.value,
|
||||
title: tab.title,
|
||||
active: tab.id == activeId,
|
||||
visible: visible,
|
||||
));
|
||||
out.add(ViewPane(id: tab.id, slot: slot.id.value, title: tab.title, active: tab.id == activeId, visible: visible));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
+26
-35
@@ -9,13 +9,7 @@ import 'package:clide/kernel/src/toolchain.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class RecentProject {
|
||||
const RecentProject({
|
||||
required this.path,
|
||||
required this.name,
|
||||
this.branch,
|
||||
required this.lastOpened,
|
||||
this.startupSticky = false,
|
||||
});
|
||||
const RecentProject({required this.path, required this.name, this.branch, required this.lastOpened, this.startupSticky = false});
|
||||
|
||||
final String path;
|
||||
final String name;
|
||||
@@ -28,28 +22,28 @@ class RecentProject {
|
||||
final bool startupSticky;
|
||||
|
||||
RecentProject copyWith({bool? startupSticky, DateTime? lastOpened, String? branch}) => RecentProject(
|
||||
path: path,
|
||||
name: name,
|
||||
branch: branch ?? this.branch,
|
||||
lastOpened: lastOpened ?? this.lastOpened,
|
||||
startupSticky: startupSticky ?? this.startupSticky,
|
||||
);
|
||||
path: path,
|
||||
name: name,
|
||||
branch: branch ?? this.branch,
|
||||
lastOpened: lastOpened ?? this.lastOpened,
|
||||
startupSticky: startupSticky ?? this.startupSticky,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'path': path,
|
||||
'name': name,
|
||||
'branch': branch,
|
||||
'lastOpened': lastOpened.toIso8601String(),
|
||||
if (startupSticky) 'startupSticky': true,
|
||||
};
|
||||
'path': path,
|
||||
'name': name,
|
||||
'branch': branch,
|
||||
'lastOpened': lastOpened.toIso8601String(),
|
||||
if (startupSticky) 'startupSticky': true,
|
||||
};
|
||||
|
||||
factory RecentProject.fromJson(Map<String, dynamic> json) => RecentProject(
|
||||
path: json['path'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
branch: json['branch'] as String?,
|
||||
lastOpened: DateTime.tryParse(json['lastOpened'] as String? ?? '') ?? DateTime.now(),
|
||||
startupSticky: json['startupSticky'] as bool? ?? false,
|
||||
);
|
||||
path: json['path'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
branch: json['branch'] as String?,
|
||||
lastOpened: DateTime.tryParse(json['lastOpened'] as String? ?? '') ?? DateTime.now(),
|
||||
startupSticky: json['startupSticky'] as bool? ?? false,
|
||||
);
|
||||
|
||||
String get relativePath {
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
@@ -77,12 +71,12 @@ class ProjectManager extends ChangeNotifier {
|
||||
required Toolchain toolchain,
|
||||
Future<void> Function(String path)? onProjectOpen,
|
||||
Future<String?> Function(String path)? onValidateProject,
|
||||
}) : _log = log,
|
||||
_events = events,
|
||||
_settings = settings,
|
||||
_toolchain = toolchain,
|
||||
_onProjectOpen = onProjectOpen,
|
||||
_onValidateProject = onValidateProject;
|
||||
}) : _log = log,
|
||||
_events = events,
|
||||
_settings = settings,
|
||||
_toolchain = toolchain,
|
||||
_onProjectOpen = onProjectOpen,
|
||||
_onValidateProject = onValidateProject;
|
||||
|
||||
final Logger _log;
|
||||
final DaemonBus _events;
|
||||
@@ -137,10 +131,7 @@ class ProjectManager extends ChangeNotifier {
|
||||
// project (T-115).
|
||||
final wasSticky = isStickyStartup(root);
|
||||
_recents.removeWhere((r) => r.path == root);
|
||||
_recents.insert(
|
||||
0,
|
||||
RecentProject(path: root, name: name, branch: branch, lastOpened: DateTime.now(), startupSticky: wasSticky),
|
||||
);
|
||||
_recents.insert(0, RecentProject(path: root, name: name, branch: branch, lastOpened: DateTime.now(), startupSticky: wasSticky));
|
||||
if (_recents.length > 10) _recents = _recents.sublist(0, 10);
|
||||
await _settings.set<String>('app.recentProjects', jsonEncode(_recents.map((r) => r.toJson()).toList()));
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class ReaderNav extends ChangeNotifier {
|
||||
ReaderNav({
|
||||
required MessageBus messages,
|
||||
required this.publisherId,
|
||||
required this.dataKey,
|
||||
}) : _messages = messages {
|
||||
ReaderNav({required MessageBus messages, required this.publisherId, required this.dataKey}) : _messages = messages {
|
||||
// Retained recorder: every selection for this reader is captured
|
||||
// here whether or not the reader widget is mounted.
|
||||
_selectionSub = _messages.subscribe(publisher: publisherId, channel: 'selection').listen((m) {
|
||||
@@ -132,17 +128,14 @@ class ReaderNavRegistry {
|
||||
/// `clide status` to surface what the user is reading (T-221, D-6 parity) —
|
||||
/// viewer files aren't editor buffers, so they don't live in EditorRegistry.
|
||||
Map<String, String> get currentByReader => {
|
||||
for (final e in _navs.entries)
|
||||
if (e.value.current != null) e.key: e.value.current!,
|
||||
};
|
||||
for (final e in _navs.entries)
|
||||
if (e.value.current != null) e.key: e.value.current!,
|
||||
};
|
||||
|
||||
/// The retained [ReaderNav] for [publisherId], created on first use.
|
||||
/// [dataKey] is the bus-payload key for this reader's entry.
|
||||
ReaderNav navFor(String publisherId, {required String dataKey}) {
|
||||
return _navs.putIfAbsent(
|
||||
publisherId,
|
||||
() => ReaderNav(messages: _messages, publisherId: publisherId, dataKey: dataKey),
|
||||
);
|
||||
return _navs.putIfAbsent(publisherId, () => ReaderNav(messages: _messages, publisherId: publisherId, dataKey: dataKey));
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -6,25 +6,15 @@
|
||||
class SecretsVault {
|
||||
final Map<String, String> _memory = {};
|
||||
|
||||
Future<void> write({
|
||||
required String extensionId,
|
||||
required String key,
|
||||
required String value,
|
||||
}) async {
|
||||
Future<void> write({required String extensionId, required String key, required String value}) async {
|
||||
_memory['$extensionId/$key'] = value;
|
||||
}
|
||||
|
||||
Future<String?> read({
|
||||
required String extensionId,
|
||||
required String key,
|
||||
}) async {
|
||||
Future<String?> read({required String extensionId, required String key}) async {
|
||||
return _memory['$extensionId/$key'];
|
||||
}
|
||||
|
||||
Future<void> delete({
|
||||
required String extensionId,
|
||||
required String key,
|
||||
}) async {
|
||||
Future<void> delete({required String extensionId, required String key}) async {
|
||||
_memory.remove('$extensionId/$key');
|
||||
}
|
||||
|
||||
|
||||
@@ -124,28 +124,28 @@ typedef DWasmEngineDelete = void Function(Pointer<TSWasmEngine>);
|
||||
|
||||
class TreeSitterLib {
|
||||
TreeSitterLib._(DynamicLibrary lib)
|
||||
: parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>('ts_parser_new'),
|
||||
parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>('ts_parser_delete'),
|
||||
parserSetLanguage = lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>('ts_parser_set_language'),
|
||||
parserSetWasmStore = lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>('ts_parser_set_wasm_store'),
|
||||
parserParseString = lib.lookupFunction<_TsParserParseString, DTsParserParseString>('ts_parser_parse_string'),
|
||||
treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>('ts_tree_delete'),
|
||||
treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>('ts_tree_root_node'),
|
||||
nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>('ts_node_start_byte'),
|
||||
nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>('ts_node_end_byte'),
|
||||
queryNew = lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
|
||||
queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>('ts_query_delete'),
|
||||
queryCaptureCount = lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>('ts_query_capture_count'),
|
||||
queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId, DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
|
||||
queryCursorNew = lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>('ts_query_cursor_new'),
|
||||
queryCursorDelete = lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>('ts_query_cursor_delete'),
|
||||
queryCursorExec = lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>('ts_query_cursor_exec'),
|
||||
queryCursorNextMatch = lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>('ts_query_cursor_next_match'),
|
||||
wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>('ts_wasm_store_new'),
|
||||
wasmStoreDelete = lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>('ts_wasm_store_delete'),
|
||||
wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage, DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
|
||||
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
|
||||
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
|
||||
: parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>('ts_parser_new'),
|
||||
parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>('ts_parser_delete'),
|
||||
parserSetLanguage = lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>('ts_parser_set_language'),
|
||||
parserSetWasmStore = lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>('ts_parser_set_wasm_store'),
|
||||
parserParseString = lib.lookupFunction<_TsParserParseString, DTsParserParseString>('ts_parser_parse_string'),
|
||||
treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>('ts_tree_delete'),
|
||||
treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>('ts_tree_root_node'),
|
||||
nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>('ts_node_start_byte'),
|
||||
nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>('ts_node_end_byte'),
|
||||
queryNew = lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
|
||||
queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>('ts_query_delete'),
|
||||
queryCaptureCount = lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>('ts_query_capture_count'),
|
||||
queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId, DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
|
||||
queryCursorNew = lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>('ts_query_cursor_new'),
|
||||
queryCursorDelete = lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>('ts_query_cursor_delete'),
|
||||
queryCursorExec = lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>('ts_query_cursor_exec'),
|
||||
queryCursorNextMatch = lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>('ts_query_cursor_next_match'),
|
||||
wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>('ts_wasm_store_new'),
|
||||
wasmStoreDelete = lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>('ts_wasm_store_delete'),
|
||||
wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage, DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
|
||||
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
|
||||
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
|
||||
|
||||
/// Constructs a [TreeSitterLib] from caller-supplied Dart closures. Used by
|
||||
/// tests to substitute the FFI surface without dlopen'ing the real library;
|
||||
@@ -176,30 +176,30 @@ class TreeSitterLib {
|
||||
DTsWasmStoreLoadLanguage? wasmStoreLoadLanguage,
|
||||
DWasmEngineNew? wasmEngineNew,
|
||||
DWasmEngineDelete? wasmEngineDelete,
|
||||
}) : parserNew = parserNew ?? (() => nullptr),
|
||||
parserDelete = parserDelete ?? ((_) {}),
|
||||
parserSetLanguage = parserSetLanguage ?? ((_, __) => false),
|
||||
parserSetWasmStore = parserSetWasmStore ?? ((_, __) {}),
|
||||
parserParseString = parserParseString ?? ((_, __, ___, ____) => nullptr),
|
||||
treeDelete = treeDelete ?? ((_) {}),
|
||||
// Leaks a zeroed TSNode allocation — only hit when the test supplies
|
||||
// a non-null parserParseString without also supplying treeRootNode.
|
||||
treeRootNode = treeRootNode ?? ((_) => calloc<TSNode>().ref),
|
||||
nodeStartByte = nodeStartByte ?? ((_) => 0),
|
||||
nodeEndByte = nodeEndByte ?? ((_) => 0),
|
||||
queryNew = queryNew ?? ((_, __, ___, ____, _____) => nullptr),
|
||||
queryDelete = queryDelete ?? ((_) {}),
|
||||
queryCaptureCount = queryCaptureCount ?? ((_) => 0),
|
||||
queryCaptureNameForId = queryCaptureNameForId ?? ((_, __, ___) => nullptr),
|
||||
queryCursorNew = queryCursorNew ?? (() => nullptr),
|
||||
queryCursorDelete = queryCursorDelete ?? ((_) {}),
|
||||
queryCursorExec = queryCursorExec ?? ((_, __, ___) {}),
|
||||
queryCursorNextMatch = queryCursorNextMatch ?? ((_, __) => false),
|
||||
wasmStoreNew = wasmStoreNew ?? ((_, __) => nullptr),
|
||||
wasmStoreDelete = wasmStoreDelete ?? ((_) {}),
|
||||
wasmStoreLoadLanguage = wasmStoreLoadLanguage ?? ((_, __, ___, ____, _____) => nullptr),
|
||||
wasmEngineNew = wasmEngineNew ?? (() => nullptr),
|
||||
wasmEngineDelete = wasmEngineDelete ?? ((_) {});
|
||||
}) : parserNew = parserNew ?? (() => nullptr),
|
||||
parserDelete = parserDelete ?? ((_) {}),
|
||||
parserSetLanguage = parserSetLanguage ?? ((_, _) => false),
|
||||
parserSetWasmStore = parserSetWasmStore ?? ((_, _) {}),
|
||||
parserParseString = parserParseString ?? ((_, _, _, _) => nullptr),
|
||||
treeDelete = treeDelete ?? ((_) {}),
|
||||
// Leaks a zeroed TSNode allocation — only hit when the test supplies
|
||||
// a non-null parserParseString without also supplying treeRootNode.
|
||||
treeRootNode = treeRootNode ?? ((_) => calloc<TSNode>().ref),
|
||||
nodeStartByte = nodeStartByte ?? ((_) => 0),
|
||||
nodeEndByte = nodeEndByte ?? ((_) => 0),
|
||||
queryNew = queryNew ?? ((_, _, _, _, _) => nullptr),
|
||||
queryDelete = queryDelete ?? ((_) {}),
|
||||
queryCaptureCount = queryCaptureCount ?? ((_) => 0),
|
||||
queryCaptureNameForId = queryCaptureNameForId ?? ((_, _, _) => nullptr),
|
||||
queryCursorNew = queryCursorNew ?? (() => nullptr),
|
||||
queryCursorDelete = queryCursorDelete ?? ((_) {}),
|
||||
queryCursorExec = queryCursorExec ?? ((_, _, _) {}),
|
||||
queryCursorNextMatch = queryCursorNextMatch ?? ((_, _) => false),
|
||||
wasmStoreNew = wasmStoreNew ?? ((_, _) => nullptr),
|
||||
wasmStoreDelete = wasmStoreDelete ?? ((_) {}),
|
||||
wasmStoreLoadLanguage = wasmStoreLoadLanguage ?? ((_, _, _, _, _) => nullptr),
|
||||
wasmEngineNew = wasmEngineNew ?? (() => nullptr),
|
||||
wasmEngineDelete = wasmEngineDelete ?? ((_) {});
|
||||
|
||||
final DTsParserNew parserNew;
|
||||
final DTsParserDelete parserDelete;
|
||||
@@ -256,10 +256,10 @@ class TreeSitterLib {
|
||||
final libName = Platform.isLinux
|
||||
? 'libtree-sitter.so'
|
||||
: Platform.isMacOS
|
||||
? 'libtree-sitter.dylib'
|
||||
: Platform.isWindows
|
||||
? 'tree-sitter.dll'
|
||||
: null;
|
||||
? 'libtree-sitter.dylib'
|
||||
: Platform.isWindows
|
||||
? 'tree-sitter.dll'
|
||||
: null;
|
||||
if (libName == null) {
|
||||
lastOpenError = 'unsupported platform ${Platform.operatingSystem}';
|
||||
lastOpenErrorPath = null;
|
||||
|
||||
@@ -20,11 +20,7 @@ typedef GrammarBytesLoader = Future<Uint8List> Function(String language);
|
||||
typedef GrammarQueryLoader = Future<String?> Function(String language);
|
||||
|
||||
class SyntaxSpan {
|
||||
const SyntaxSpan({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.role,
|
||||
});
|
||||
const SyntaxSpan({required this.start, required this.end, required this.role});
|
||||
|
||||
final int start;
|
||||
final int end;
|
||||
@@ -39,11 +35,7 @@ class SyntaxResult {
|
||||
}
|
||||
|
||||
class _LoadedGrammar {
|
||||
_LoadedGrammar({
|
||||
required this.language,
|
||||
required this.query,
|
||||
required this.captureNames,
|
||||
});
|
||||
_LoadedGrammar({required this.language, required this.query, required this.captureNames});
|
||||
|
||||
final Pointer<Void> language;
|
||||
final Pointer<TSQuery> query;
|
||||
@@ -56,13 +48,10 @@ class TreeSitterService {
|
||||
/// Production constructor: uses the dlopen'd [TreeSitterLib.instance] and
|
||||
/// the Flutter [rootBundle]. Tests pass [lib] / [grammarBytes] /
|
||||
/// [grammarQuery] to substitute a fake FFI surface and in-memory assets.
|
||||
TreeSitterService({
|
||||
TreeSitterLib? lib,
|
||||
GrammarBytesLoader? grammarBytes,
|
||||
GrammarQueryLoader? grammarQuery,
|
||||
}) : _injectedLib = lib,
|
||||
_grammarBytes = grammarBytes ?? _defaultGrammarBytes,
|
||||
_grammarQuery = grammarQuery ?? _defaultGrammarQuery;
|
||||
TreeSitterService({TreeSitterLib? lib, GrammarBytesLoader? grammarBytes, GrammarQueryLoader? grammarQuery})
|
||||
: _injectedLib = lib,
|
||||
_grammarBytes = grammarBytes ?? _defaultGrammarBytes,
|
||||
_grammarQuery = grammarQuery ?? _defaultGrammarQuery;
|
||||
|
||||
final TreeSitterLib? _injectedLib;
|
||||
final GrammarBytesLoader _grammarBytes;
|
||||
@@ -142,13 +131,7 @@ class TreeSitterService {
|
||||
wasmNative.asTypedList(wasmBytes.length).setAll(0, wasmBytes);
|
||||
final error = calloc<TSWasmError>();
|
||||
|
||||
final lang = lib.wasmStoreLoadLanguage(
|
||||
_store!,
|
||||
nameNative.cast(),
|
||||
wasmNative,
|
||||
wasmBytes.length,
|
||||
error,
|
||||
);
|
||||
final lang = lib.wasmStoreLoadLanguage(_store!, nameNative.cast(), wasmNative, wasmBytes.length, error);
|
||||
|
||||
calloc.free(wasmNative);
|
||||
calloc.free(nameNative);
|
||||
@@ -174,13 +157,7 @@ class TreeSitterService {
|
||||
final errorOffset = calloc<Uint32>();
|
||||
final errorType = calloc<Int32>();
|
||||
|
||||
query = lib.queryNew(
|
||||
lang,
|
||||
queryNative.cast(),
|
||||
queryLen,
|
||||
errorOffset,
|
||||
errorType,
|
||||
);
|
||||
query = lib.queryNew(lang, queryNative.cast(), queryLen, errorOffset, errorType);
|
||||
|
||||
calloc.free(queryNative);
|
||||
calloc.free(errorOffset);
|
||||
@@ -198,11 +175,7 @@ class TreeSitterService {
|
||||
}
|
||||
}
|
||||
|
||||
final grammar = _LoadedGrammar(
|
||||
language: lang,
|
||||
query: query,
|
||||
captureNames: captureNames,
|
||||
);
|
||||
final grammar = _LoadedGrammar(language: lang, query: query, captureNames: captureNames);
|
||||
_grammars[language] = grammar;
|
||||
return grammar;
|
||||
} catch (_) {
|
||||
@@ -244,12 +217,7 @@ class TreeSitterService {
|
||||
// Parse source.
|
||||
final sourceNative = source.toNativeUtf8();
|
||||
final sourceLen = utf8.encode(source).length;
|
||||
final tree = lib.parserParseString(
|
||||
parser,
|
||||
nullptr,
|
||||
sourceNative.cast(),
|
||||
sourceLen,
|
||||
);
|
||||
final tree = lib.parserParseString(parser, nullptr, sourceNative.cast(), sourceLen);
|
||||
|
||||
if (tree == nullptr) {
|
||||
calloc.free(sourceNative);
|
||||
@@ -270,11 +238,7 @@ class TreeSitterService {
|
||||
final cap = m.captures[i];
|
||||
final captureIndex = cap.index;
|
||||
if (captureIndex < grammar.captureNames.length) {
|
||||
spans.add(SyntaxSpan(
|
||||
start: lib.nodeStartByte(cap.node),
|
||||
end: lib.nodeEndByte(cap.node),
|
||||
role: grammar.captureNames[captureIndex],
|
||||
));
|
||||
spans.add(SyntaxSpan(start: lib.nodeStartByte(cap.node), end: lib.nodeEndByte(cap.node), role: grammar.captureNames[captureIndex]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,7 @@ import 'package:flutter/foundation.dart';
|
||||
/// A foreground/background token pair the a11y contrast suite walks.
|
||||
@immutable
|
||||
class ContrastPair {
|
||||
const ContrastPair({
|
||||
required this.name,
|
||||
required this.foreground,
|
||||
required this.background,
|
||||
this.largeText = false,
|
||||
});
|
||||
const ContrastPair({required this.name, required this.foreground, required this.background, this.largeText = false});
|
||||
|
||||
final String name;
|
||||
final Color foreground;
|
||||
@@ -49,62 +44,18 @@ double minimumRatio(ContrastPair pair) => pair.largeText ? 3.0 : 4.5;
|
||||
/// muted text, status chips, syntax tokens, and the focus border lives
|
||||
/// in [extendedPairs], which only `-hc`/`-cb` variants must pass.
|
||||
List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
|
||||
ContrastPair(
|
||||
name: 'global.text_on_background',
|
||||
foreground: s.globalForeground,
|
||||
background: s.globalBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'panel.header_foreground_on_panel',
|
||||
foreground: s.panelHeaderForeground,
|
||||
background: s.panelHeader,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'sidebar.foreground_on_sidebar',
|
||||
foreground: s.sidebarForeground,
|
||||
background: s.sidebarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'statusbar.foreground_on_statusbar',
|
||||
foreground: s.statusBarForeground,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'tab.active_text_on_active_bg',
|
||||
foreground: s.tabActiveForeground,
|
||||
background: s.tabActive,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'tab.inactive_text_on_inactive_bg',
|
||||
foreground: s.tabInactiveForeground,
|
||||
background: s.tabInactive,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'button.text_on_button',
|
||||
foreground: s.buttonForeground,
|
||||
background: s.buttonBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'listItem.selected_text_on_selected_bg',
|
||||
foreground: s.listItemSelectedForeground,
|
||||
background: s.listItemSelectedBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'listItem.text_on_list',
|
||||
foreground: s.listItemForeground,
|
||||
background: s.listItemBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'tooltip.text_on_tooltip',
|
||||
foreground: s.tooltipForeground,
|
||||
background: s.tooltipBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'dropdown.text_on_dropdown',
|
||||
foreground: s.dropdownForeground,
|
||||
background: s.dropdownBackground,
|
||||
),
|
||||
];
|
||||
ContrastPair(name: 'global.text_on_background', foreground: s.globalForeground, background: s.globalBackground),
|
||||
ContrastPair(name: 'panel.header_foreground_on_panel', foreground: s.panelHeaderForeground, background: s.panelHeader),
|
||||
ContrastPair(name: 'sidebar.foreground_on_sidebar', foreground: s.sidebarForeground, background: s.sidebarBackground),
|
||||
ContrastPair(name: 'statusbar.foreground_on_statusbar', foreground: s.statusBarForeground, background: s.statusBarBackground),
|
||||
ContrastPair(name: 'tab.active_text_on_active_bg', foreground: s.tabActiveForeground, background: s.tabActive),
|
||||
ContrastPair(name: 'tab.inactive_text_on_inactive_bg', foreground: s.tabInactiveForeground, background: s.tabInactive),
|
||||
ContrastPair(name: 'button.text_on_button', foreground: s.buttonForeground, background: s.buttonBackground),
|
||||
ContrastPair(name: 'listItem.selected_text_on_selected_bg', foreground: s.listItemSelectedForeground, background: s.listItemSelectedBackground),
|
||||
ContrastPair(name: 'listItem.text_on_list', foreground: s.listItemForeground, background: s.listItemBackground),
|
||||
ContrastPair(name: 'tooltip.text_on_tooltip', foreground: s.tooltipForeground, background: s.tooltipBackground),
|
||||
ContrastPair(name: 'dropdown.text_on_dropdown', foreground: s.dropdownForeground, background: s.dropdownBackground),
|
||||
];
|
||||
|
||||
/// Stricter pair set — only the high-contrast (`-hc`) and colour-blind
|
||||
/// (`-cb`) theme variants must clear it. See D-69. These are the
|
||||
@@ -112,94 +63,37 @@ List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
|
||||
/// text, status chip foregrounds, syntax tokens on the code-block
|
||||
/// surface, and the focus-indicating panel border.
|
||||
List<ContrastPair> extendedPairs(SurfaceTokens s) => [
|
||||
ContrastPair(
|
||||
name: 'global.text_muted_on_background',
|
||||
foreground: s.globalTextMuted,
|
||||
background: s.globalBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'global.text_muted_on_panel',
|
||||
foreground: s.globalTextMuted,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.success_on_statusbar',
|
||||
foreground: s.statusSuccess,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.warning_on_statusbar',
|
||||
foreground: s.statusWarning,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.error_on_statusbar',
|
||||
foreground: s.statusError,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.info_on_statusbar',
|
||||
foreground: s.statusInfo,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.keyword_on_panel',
|
||||
foreground: s.syntaxKeyword,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.type_on_panel',
|
||||
foreground: s.syntaxType,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.string_on_panel',
|
||||
foreground: s.syntaxString,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.number_on_panel',
|
||||
foreground: s.syntaxNumber,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.comment_on_panel',
|
||||
foreground: s.syntaxComment,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.method_on_panel',
|
||||
foreground: s.syntaxMethod,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.punct_on_panel',
|
||||
foreground: s.syntaxPunct,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
// WCAG 1.4.11 wants 3:1 for non-text UI components like a focus
|
||||
// border against the adjacent surface.
|
||||
ContrastPair(
|
||||
name: 'panel.active_border_on_background',
|
||||
foreground: s.panelActiveBorder,
|
||||
background: s.globalBackground,
|
||||
largeText: true,
|
||||
),
|
||||
// selection.foreground_on_selection is intentionally omitted here.
|
||||
//
|
||||
// The `selectionBackground` token defaults to `globalFocus.withAlpha(0x66)`
|
||||
// — a semi-transparent tint composited onto the real content background at
|
||||
// runtime. The WCAG compositor in contrastRatio() blends onto neutral grey
|
||||
// (0x808080) rather than the actual dark panel background, which
|
||||
// systematically understates the readable contrast for all current bundled
|
||||
// themes. Adding the pair here would require retuning palettes, which D-69
|
||||
// forbids for user-contract themes.
|
||||
//
|
||||
// Enforcement is deferred to a follow-up ticket: -hc/-cb variants will
|
||||
// declare an explicit `surface.selectionBackground` override that is
|
||||
// opaque enough to clear 3:1 against the grey compositor, at which point
|
||||
// the pair can be added to extendedPairs.
|
||||
];
|
||||
ContrastPair(name: 'global.text_muted_on_background', foreground: s.globalTextMuted, background: s.globalBackground),
|
||||
ContrastPair(name: 'global.text_muted_on_panel', foreground: s.globalTextMuted, background: s.panelBackground),
|
||||
ContrastPair(name: 'status.success_on_statusbar', foreground: s.statusSuccess, background: s.statusBarBackground),
|
||||
ContrastPair(name: 'status.warning_on_statusbar', foreground: s.statusWarning, background: s.statusBarBackground),
|
||||
ContrastPair(name: 'status.error_on_statusbar', foreground: s.statusError, background: s.statusBarBackground),
|
||||
ContrastPair(name: 'status.info_on_statusbar', foreground: s.statusInfo, background: s.statusBarBackground),
|
||||
ContrastPair(name: 'syntax.keyword_on_panel', foreground: s.syntaxKeyword, background: s.panelBackground),
|
||||
ContrastPair(name: 'syntax.type_on_panel', foreground: s.syntaxType, background: s.panelBackground),
|
||||
ContrastPair(name: 'syntax.string_on_panel', foreground: s.syntaxString, background: s.panelBackground),
|
||||
ContrastPair(name: 'syntax.number_on_panel', foreground: s.syntaxNumber, background: s.panelBackground),
|
||||
ContrastPair(name: 'syntax.comment_on_panel', foreground: s.syntaxComment, background: s.panelBackground),
|
||||
ContrastPair(name: 'syntax.method_on_panel', foreground: s.syntaxMethod, background: s.panelBackground),
|
||||
ContrastPair(name: 'syntax.punct_on_panel', foreground: s.syntaxPunct, background: s.panelBackground),
|
||||
// WCAG 1.4.11 wants 3:1 for non-text UI components like a focus
|
||||
// border against the adjacent surface.
|
||||
ContrastPair(name: 'panel.active_border_on_background', foreground: s.panelActiveBorder, background: s.globalBackground, largeText: true),
|
||||
// selection.foreground_on_selection is intentionally omitted here.
|
||||
//
|
||||
// The `selectionBackground` token defaults to `globalFocus.withAlpha(0x66)`
|
||||
// — a semi-transparent tint composited onto the real content background at
|
||||
// runtime. The WCAG compositor in contrastRatio() blends onto neutral grey
|
||||
// (0x808080) rather than the actual dark panel background, which
|
||||
// systematically understates the readable contrast for all current bundled
|
||||
// themes. Adding the pair here would require retuning palettes, which D-69
|
||||
// forbids for user-contract themes.
|
||||
//
|
||||
// Enforcement is deferred to a follow-up ticket: -hc/-cb variants will
|
||||
// declare an explicit `surface.selectionBackground` override that is
|
||||
// opaque enough to clear 3:1 against the grey compositor, at which point
|
||||
// the pair can be added to extendedPairs.
|
||||
];
|
||||
|
||||
/// Convenience for tests: returns the list of [canonicalPairs] that
|
||||
/// fail WCAG AA.
|
||||
@@ -223,18 +117,15 @@ List<ContrastFailure> _failures(List<ContrastPair> pairs) {
|
||||
|
||||
@immutable
|
||||
class ContrastFailure {
|
||||
const ContrastFailure({
|
||||
required this.pair,
|
||||
required this.ratio,
|
||||
required this.minimum,
|
||||
});
|
||||
const ContrastFailure({required this.pair, required this.ratio, required this.minimum});
|
||||
|
||||
final ContrastPair pair;
|
||||
final double ratio;
|
||||
final double minimum;
|
||||
|
||||
@override
|
||||
String toString() => 'contrast ${pair.name}: ${ratio.toStringAsFixed(2)} < '
|
||||
String toString() =>
|
||||
'contrast ${pair.name}: ${ratio.toStringAsFixed(2)} < '
|
||||
'${minimum.toStringAsFixed(1)}';
|
||||
}
|
||||
|
||||
@@ -244,12 +135,7 @@ Color _composite(Color src, Color dst) {
|
||||
final a = src.a;
|
||||
if (a >= 0.999) return src;
|
||||
double mix(double s, double d) => s * a + d * (1 - a);
|
||||
return Color.from(
|
||||
alpha: 1.0,
|
||||
red: mix(src.r, dst.r),
|
||||
green: mix(src.g, dst.g),
|
||||
blue: mix(src.b, dst.b),
|
||||
);
|
||||
return Color.from(alpha: 1.0, red: mix(src.r, dst.r), green: mix(src.g, dst.g), blue: mix(src.b, dst.b));
|
||||
}
|
||||
|
||||
double _relativeLuminance(Color c) {
|
||||
|
||||
@@ -5,12 +5,7 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
class ClideThemeData {
|
||||
const ClideThemeData({
|
||||
required this.name,
|
||||
required this.displayName,
|
||||
required this.dark,
|
||||
required this.surface,
|
||||
});
|
||||
const ClideThemeData({required this.name, required this.displayName, required this.dark, required this.surface});
|
||||
|
||||
final String name;
|
||||
final String displayName;
|
||||
@@ -19,12 +14,9 @@ class ClideThemeData {
|
||||
}
|
||||
|
||||
class ThemeController extends ChangeNotifier {
|
||||
ThemeController({
|
||||
required List<ThemeDefinition> bundled,
|
||||
ThemeResolver resolver = const ThemeResolver(),
|
||||
String? initialName,
|
||||
}) : _resolver = resolver,
|
||||
_defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) {
|
||||
ThemeController({required List<ThemeDefinition> bundled, ThemeResolver resolver = const ThemeResolver(), String? initialName})
|
||||
: _resolver = resolver,
|
||||
_defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) {
|
||||
final first = initialName != null && _defs.containsKey(initialName) ? initialName : bundled.first.name;
|
||||
_currentName = first;
|
||||
_current = _build(first);
|
||||
@@ -68,21 +60,12 @@ class ThemeController extends ChangeNotifier {
|
||||
surfaceOverride: def.surfaceOverride,
|
||||
extensionOverride: def.extensionOverride,
|
||||
);
|
||||
return ClideThemeData(
|
||||
name: def.name,
|
||||
displayName: def.displayName,
|
||||
dark: def.dark,
|
||||
surface: tokens,
|
||||
);
|
||||
return ClideThemeData(name: def.name, displayName: def.displayName, dark: def.dark, surface: tokens);
|
||||
}
|
||||
}
|
||||
|
||||
class ClideTheme extends InheritedNotifier<ThemeController> {
|
||||
const ClideTheme({
|
||||
super.key,
|
||||
required ThemeController controller,
|
||||
required super.child,
|
||||
}) : super(notifier: controller);
|
||||
const ClideTheme({super.key, required ThemeController controller, required super.child}) : super(notifier: controller);
|
||||
|
||||
static ClideThemeData of(BuildContext context) {
|
||||
final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>();
|
||||
|
||||
@@ -70,10 +70,7 @@ class ThemeLoader {
|
||||
final surface = doc['surface'];
|
||||
final extension = doc['extension'];
|
||||
|
||||
final mergedSurface = <String, String>{
|
||||
...syntaxSurface,
|
||||
if (surface is Map) ..._parseRefMap(surface),
|
||||
};
|
||||
final mergedSurface = <String, String>{...syntaxSurface, if (surface is Map) ..._parseRefMap(surface)};
|
||||
|
||||
return ThemeDefinition(
|
||||
name: name,
|
||||
|
||||
@@ -28,12 +28,7 @@ class ThemeResolver {
|
||||
final semantic = _buildSemantic(palette, semanticOverride);
|
||||
final surface = <String, Color>{};
|
||||
for (final key in TokenKeys.all) {
|
||||
surface[key] = _resolveSurface(
|
||||
key: key,
|
||||
palette: palette,
|
||||
semantic: semantic,
|
||||
surfaceOverride: surfaceOverride,
|
||||
);
|
||||
surface[key] = _resolveSurface(key: key, palette: palette, semantic: semantic, surfaceOverride: surfaceOverride);
|
||||
}
|
||||
|
||||
// selectionBackground defaults to globalFocus at ~40 % opacity (0x66 alpha)
|
||||
@@ -149,12 +144,7 @@ class ThemeResolver {
|
||||
return SemanticRoles(roles);
|
||||
}
|
||||
|
||||
Color _resolveSurface({
|
||||
required String key,
|
||||
required Palette palette,
|
||||
required SemanticRoles semantic,
|
||||
Map<String, String>? surfaceOverride,
|
||||
}) {
|
||||
Color _resolveSurface({required String key, required Palette palette, required SemanticRoles semantic, Map<String, String>? surfaceOverride}) {
|
||||
final override = surfaceOverride?[key];
|
||||
if (override != null) {
|
||||
final resolved = _resolveRef(override, palette, semantic);
|
||||
|
||||
@@ -24,17 +24,5 @@ abstract class SemanticKeys {
|
||||
static const error = 'error';
|
||||
static const info = 'info';
|
||||
|
||||
static const all = <String>[
|
||||
mainchrome,
|
||||
calltoaction,
|
||||
focus,
|
||||
background,
|
||||
surface,
|
||||
text,
|
||||
textMuted,
|
||||
success,
|
||||
warning,
|
||||
error,
|
||||
info,
|
||||
];
|
||||
static const all = <String>[mainchrome, calltoaction, focus, background, surface, text, textMuted, success, warning, error, info];
|
||||
}
|
||||
|
||||
@@ -13,27 +13,17 @@ enum ToastSeverity { success, warning, error, info }
|
||||
const String toastChannel = 'toast';
|
||||
|
||||
ToastSeverity _severityFromName(Object? name) => switch (name) {
|
||||
'success' => ToastSeverity.success,
|
||||
'warning' => ToastSeverity.warning,
|
||||
'error' => ToastSeverity.error,
|
||||
_ => ToastSeverity.info,
|
||||
};
|
||||
'success' => ToastSeverity.success,
|
||||
'warning' => ToastSeverity.warning,
|
||||
'error' => ToastSeverity.error,
|
||||
_ => ToastSeverity.info,
|
||||
};
|
||||
|
||||
/// Raise a toast by publishing to the MessageBus — the decoupled path:
|
||||
/// emitters depend only on the bus, never on the [ToastService]. [publisher]
|
||||
/// is the emitter id (e.g. `builtin.git`), kept for provenance/filtering.
|
||||
void publishToast(
|
||||
MessageBus messages,
|
||||
String publisher,
|
||||
String message, {
|
||||
ToastSeverity severity = ToastSeverity.info,
|
||||
Duration? duration,
|
||||
}) {
|
||||
messages.publish(publisher, toastChannel, {
|
||||
'message': message,
|
||||
'severity': severity.name,
|
||||
if (duration != null) 'durationMs': duration.inMilliseconds,
|
||||
});
|
||||
void publishToast(MessageBus messages, String publisher, String message, {ToastSeverity severity = ToastSeverity.info, Duration? duration}) {
|
||||
messages.publish(publisher, toastChannel, {'message': message, 'severity': severity.name, if (duration != null) 'durationMs': duration.inMilliseconds});
|
||||
}
|
||||
|
||||
/// One live toast. Immutable; the [ToastService] owns the list.
|
||||
|
||||
@@ -12,11 +12,7 @@ class ToolCheck extends ChangeNotifier {
|
||||
|
||||
bool get allOk => pqlOk && tmuxOk && gitOk;
|
||||
|
||||
List<String> get errors => [
|
||||
if (!pqlOk) 'pql not found',
|
||||
if (!tmuxOk) 'tmux not found',
|
||||
if (!gitOk) 'git not found',
|
||||
];
|
||||
List<String> get errors => [if (!pqlOk) 'pql not found', if (!tmuxOk) 'tmux not found', if (!gitOk) 'git not found'];
|
||||
|
||||
/// Workspace root, set by the app at boot. Falls back to cwd.
|
||||
static String? workspaceRoot;
|
||||
|
||||
@@ -44,11 +44,7 @@ class Toolchain extends ChangeNotifier implements ToolchainView {
|
||||
bool get allOk => _resolved && missing.isEmpty;
|
||||
|
||||
@override
|
||||
List<String> get missing => [
|
||||
if (_git == null) 'git',
|
||||
if (_pql == null) 'pql',
|
||||
if (_tmux == null) 'tmux',
|
||||
];
|
||||
List<String> get missing => [if (_git == null) 'git', if (_pql == null) 'pql', if (_tmux == null) 'tmux'];
|
||||
|
||||
/// Returns a Future that completes when resolution finishes.
|
||||
Future<void> waitForResolution() {
|
||||
|
||||
@@ -12,13 +12,7 @@ import 'dart:io';
|
||||
|
||||
/// Serializable result of tool resolution (crosses isolate boundary).
|
||||
class ResolvedPaths {
|
||||
const ResolvedPaths({
|
||||
this.git,
|
||||
this.pql,
|
||||
this.tmux,
|
||||
this.shell,
|
||||
this.gitEnv,
|
||||
});
|
||||
const ResolvedPaths({this.git, this.pql, this.tmux, this.shell, this.gitEnv});
|
||||
|
||||
final String? git;
|
||||
final String? pql;
|
||||
@@ -66,11 +60,7 @@ class _StaticToolchain implements ToolchainView {
|
||||
@override
|
||||
bool get allOk => missing.isEmpty;
|
||||
@override
|
||||
List<String> get missing => [
|
||||
if (_paths.git == null) 'git',
|
||||
if (_paths.pql == null) 'pql',
|
||||
if (_paths.tmux == null) 'tmux',
|
||||
];
|
||||
List<String> get missing => [if (_paths.git == null) 'git', if (_paths.pql == null) 'pql', if (_paths.tmux == null) 'tmux'];
|
||||
}
|
||||
|
||||
/// Top-level function for compute/isolate use. Returns a plain-data
|
||||
@@ -88,10 +78,7 @@ ResolvedPaths resolveToolchainPaths() {
|
||||
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',
|
||||
};
|
||||
gitEnv = {'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core', 'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates'};
|
||||
} else {
|
||||
git = _findOnPath('git');
|
||||
}
|
||||
@@ -145,12 +132,8 @@ String? _firstExisting(List<String> candidates) {
|
||||
}
|
||||
|
||||
/// Build expanded PATH inline — must be self-contained for isolate use.
|
||||
String _expandedPath() => expandToolPath(
|
||||
Platform.environment['PATH'] ?? '',
|
||||
isMac: Platform.isMacOS,
|
||||
isLinux: Platform.isLinux,
|
||||
home: Platform.environment['HOME'],
|
||||
);
|
||||
String _expandedPath() =>
|
||||
expandToolPath(Platform.environment['PATH'] ?? '', isMac: Platform.isMacOS, isLinux: Platform.isLinux, home: Platform.environment['HOME']);
|
||||
|
||||
/// Pure PATH-expansion logic, extracted so it's testable without touching the
|
||||
/// process environment.
|
||||
@@ -164,12 +147,7 @@ String _expandedPath() => expandToolPath(
|
||||
String expandToolPath(String base, {required bool isMac, required bool isLinux, String? home}) {
|
||||
if (!isMac && !isLinux) return base;
|
||||
final h = home ?? '';
|
||||
final extras = <String>[
|
||||
if (h.isNotEmpty) '$h/.local/bin',
|
||||
if (isMac) '/opt/homebrew/bin',
|
||||
if (isMac) '/opt/homebrew/sbin',
|
||||
'/usr/local/bin',
|
||||
];
|
||||
final extras = <String>[if (h.isNotEmpty) '$h/.local/bin', if (isMac) '/opt/homebrew/bin', if (isMac) '/opt/homebrew/sbin', '/usr/local/bin'];
|
||||
final existing = base.split(':').toSet();
|
||||
final missing = extras.where((p) => !existing.contains(p));
|
||||
if (missing.isEmpty) return base;
|
||||
|
||||
Reference in New Issue
Block a user