scaffold Flutter app — kernel, extensions, widgets, Tier 0 built-ins
First real content under app/. Lays the whole Tier 0 foundation in one
commit because the pieces depend on each other circularly (kernel →
extension → widgets → built-ins all reference types from the layer
below); splitting would leave intermediate commits that don't compile.
Key shapes:
* bare WidgetsApp root — no Material, no Cupertino, no Scaffold.
ClideTheme InheritedWidget is the only source of tokens.
* ClideKernel InheritedWidget aggregates 18 services (settings,
project, extensions, theme, panels, events, ipc, commands +
palette + keybindings, clipboard, files, notify, dialog, tray,
secrets, os, net, focus, log, i18n). ExtensionContext exposes
them through a stable interface.
* ClideExtension + sealed ContributionPoint hierarchy (Tab,
StatusItem, Toolbar, Command, TrayItem, LayoutPreset). One
manifest ships N contributions into kernel slots.
* Three-tier theme pipeline: palette (named colors) → semantic
roles → ~60 surface tokens. Defaults at each layer so legacy
palette-only themes produce a complete SurfaceTokens.
* A11y baked in from day one — every interactive primitive wraps
in Semantics(label:, hint:, button:); ensureSemantics() at boot;
theme/contrast.dart helper exposes token pairs for the WCAG
gate in the a11y test suite.
* i18n ported from fframe's L10n pattern (text-driven, namespaced
JSON catalogs, caller-supplied placeholders) with a proper
locale fallback chain (exact → language → default → placeholder)
fframe lacks.
* Four Tier-0 built-ins live (default-layout, welcome, ipc-status,
theme-picker) plus 17 id-reserving stubs so later tiers fill in
without rename churn.
.gitignore extended to cover app/ sub-package artefacts and the
Playwright harness scratch dirs.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide_app/kernel/src/clipboard.dart';
|
||||
import 'package:clide_app/kernel/src/commands/keybindings.dart';
|
||||
import 'package:clide_app/kernel/src/commands/palette.dart';
|
||||
import 'package:clide_app/kernel/src/commands/registry.dart';
|
||||
import 'package:clide_app/kernel/src/dialog.dart';
|
||||
import 'package:clide_app/kernel/src/events/bus.dart';
|
||||
import 'package:clide_app/kernel/src/extensions_manager.dart';
|
||||
import 'package:clide_app/kernel/src/files.dart';
|
||||
import 'package:clide_app/kernel/src/focus.dart';
|
||||
import 'package:clide_app/kernel/src/i18n/catalog_loader.dart';
|
||||
import 'package:clide_app/kernel/src/i18n/i18n.dart';
|
||||
import 'package:clide_app/kernel/src/ipc/client.dart';
|
||||
import 'package:clide_app/kernel/src/log.dart';
|
||||
import 'package:clide_app/kernel/src/net.dart';
|
||||
import 'package:clide_app/kernel/src/notify.dart';
|
||||
import 'package:clide_app/kernel/src/os.dart';
|
||||
import 'package:clide_app/kernel/src/panels/arrangement.dart';
|
||||
import 'package:clide_app/kernel/src/panels/registry.dart';
|
||||
import 'package:clide_app/kernel/src/project.dart';
|
||||
import 'package:clide_app/kernel/src/secrets.dart';
|
||||
import 'package:clide_app/kernel/src/settings.dart';
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:clide_app/kernel/src/theme/loader.dart';
|
||||
import 'package:clide_app/kernel/src/tray.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Aggregated kernel services. Feature code that runs outside a
|
||||
/// BuildContext (extensions, background tasks) holds a [KernelServices]
|
||||
/// ref directly; widget code reaches them via [ClideKernel.of].
|
||||
class KernelServices {
|
||||
KernelServices({
|
||||
required this.log,
|
||||
required this.settings,
|
||||
required this.events,
|
||||
required this.ipc,
|
||||
required this.theme,
|
||||
required this.i18n,
|
||||
required this.panels,
|
||||
required this.arrangement,
|
||||
required this.commands,
|
||||
required this.palette,
|
||||
required this.keybindings,
|
||||
required this.clipboard,
|
||||
required this.files,
|
||||
required this.notify,
|
||||
required this.dialog,
|
||||
required this.tray,
|
||||
required this.secrets,
|
||||
required this.os,
|
||||
required this.net,
|
||||
required this.focus,
|
||||
required this.project,
|
||||
required this.extensions,
|
||||
});
|
||||
|
||||
final Logger log;
|
||||
final SettingsStore settings;
|
||||
final EventBus events;
|
||||
final DaemonClient ipc;
|
||||
final ThemeController theme;
|
||||
final I18n i18n;
|
||||
final PanelRegistry panels;
|
||||
final LayoutArrangement arrangement;
|
||||
final CommandRegistry commands;
|
||||
final PaletteController palette;
|
||||
final KeybindingResolver keybindings;
|
||||
final ClideClipboard clipboard;
|
||||
final FileServices files;
|
||||
final Notifications notify;
|
||||
final DialogRouter dialog;
|
||||
final TrayRegistry tray;
|
||||
final SecretsVault secrets;
|
||||
final OsBridge os;
|
||||
final NetworkStatus net;
|
||||
final FocusTracker focus;
|
||||
final ProjectManager project;
|
||||
final ExtensionManager extensions;
|
||||
|
||||
static Future<KernelServices> boot({
|
||||
required Directory appDir,
|
||||
required List<ThemeDefinition> bundledThemes,
|
||||
required CatalogLoader i18nLoader,
|
||||
List<String> preloadNamespaces = const [],
|
||||
Locale defaultLocale = const Locale('en', 'US'),
|
||||
Locale? initialLocale,
|
||||
List<Locale> availableLocales = const [Locale('en', 'US')],
|
||||
String? socketPath,
|
||||
DaemonClient Function(Logger, EventBus)? daemonClientFactory,
|
||||
bool autoStartDaemonClient = true,
|
||||
}) async {
|
||||
final log = Logger();
|
||||
final events = EventBus();
|
||||
|
||||
final settings = SettingsStore(appDir: appDir);
|
||||
await settings.load();
|
||||
|
||||
final i18n = I18n(
|
||||
loader: i18nLoader,
|
||||
log: log,
|
||||
defaultLocale: defaultLocale,
|
||||
initialLocale: initialLocale,
|
||||
availableLocales: availableLocales,
|
||||
);
|
||||
for (final ns in preloadNamespaces) {
|
||||
await i18n.ensureNamespaceLoaded(ns);
|
||||
}
|
||||
|
||||
final theme = ThemeController(bundled: bundledThemes);
|
||||
final panels = PanelRegistry();
|
||||
final arrangement = LayoutArrangement();
|
||||
final commands = CommandRegistry();
|
||||
final keybindings = KeybindingResolver();
|
||||
final palette = PaletteController(commands);
|
||||
final clipboard = ClideClipboard();
|
||||
final files = FileServices(events);
|
||||
final notify = Notifications();
|
||||
final dialog = DialogRouter();
|
||||
final tray = TrayRegistry();
|
||||
final secrets = SecretsVault();
|
||||
final os = OsBridge(log: log, events: events);
|
||||
final net = NetworkStatus();
|
||||
final focus = FocusTracker();
|
||||
final project = ProjectManager(
|
||||
log: log,
|
||||
events: events,
|
||||
settings: settings,
|
||||
);
|
||||
final ipc = daemonClientFactory != null
|
||||
? daemonClientFactory(log, events)
|
||||
: DaemonClient(
|
||||
socketPath: socketPath ?? defaultSocketPath(),
|
||||
log: log,
|
||||
events: events,
|
||||
);
|
||||
final extensions = ExtensionManager(
|
||||
log: log,
|
||||
events: events,
|
||||
settings: settings,
|
||||
theme: theme,
|
||||
i18n: i18n,
|
||||
panels: panels,
|
||||
arrangement: arrangement,
|
||||
commands: commands,
|
||||
palette: palette,
|
||||
keybindings: keybindings,
|
||||
clipboard: clipboard,
|
||||
files: files,
|
||||
notify: notify,
|
||||
dialog: dialog,
|
||||
tray: tray,
|
||||
secrets: secrets,
|
||||
os: os,
|
||||
net: net,
|
||||
focus: focus,
|
||||
project: project,
|
||||
ipc: ipc,
|
||||
);
|
||||
|
||||
if (autoStartDaemonClient) {
|
||||
unawaited(ipc.start());
|
||||
}
|
||||
|
||||
return KernelServices(
|
||||
log: log,
|
||||
settings: settings,
|
||||
events: events,
|
||||
ipc: ipc,
|
||||
theme: theme,
|
||||
i18n: i18n,
|
||||
panels: panels,
|
||||
arrangement: arrangement,
|
||||
commands: commands,
|
||||
palette: palette,
|
||||
keybindings: keybindings,
|
||||
clipboard: clipboard,
|
||||
files: files,
|
||||
notify: notify,
|
||||
dialog: dialog,
|
||||
tray: tray,
|
||||
secrets: secrets,
|
||||
os: os,
|
||||
net: net,
|
||||
focus: focus,
|
||||
project: project,
|
||||
extensions: extensions,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await ipc.stop();
|
||||
ipc.dispose();
|
||||
settings.dispose();
|
||||
theme.dispose();
|
||||
panels.dispose();
|
||||
arrangement.dispose();
|
||||
commands.dispose();
|
||||
palette.dispose();
|
||||
i18n.dispose();
|
||||
notify.dispose();
|
||||
dialog.dispose();
|
||||
tray.dispose();
|
||||
net.dispose();
|
||||
focus.dispose();
|
||||
project.dispose();
|
||||
extensions.dispose();
|
||||
await log.dispose();
|
||||
await events.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ClideKernel extends InheritedWidget {
|
||||
const ClideKernel({
|
||||
super.key,
|
||||
required this.services,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final KernelServices services;
|
||||
|
||||
static KernelServices of(BuildContext context) {
|
||||
final w = context.dependOnInheritedWidgetOfExactType<ClideKernel>();
|
||||
if (w == null) {
|
||||
throw FlutterError(
|
||||
'ClideKernel.of() called with a context that is not a descendant of a ClideKernel.');
|
||||
}
|
||||
return w.services;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ClideKernel oldWidget) =>
|
||||
services != oldWidget.services;
|
||||
}
|
||||
Reference in New Issue
Block a user