dissolve app/ into repo root (D-056)

Single Flutter package at the repo root. All code, tests, assets,
and platform directories moved from app/ to root. Package renamed
from clide_app to clide — all imports rewritten. Merged pubspec
combines core (ffi) and app (flutter, yaml, xterm) dependencies.
Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon
lifecycle. 317 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 00:37:20 +02:00
co-authored by Claude Opus 4.6
parent a526c5b9b7
commit 46329700d5
394 changed files with 978 additions and 1090 deletions
@@ -0,0 +1,89 @@
import 'dart:ui';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
ThemeDefinition _def(String name, Color primary) => ThemeDefinition(
name: name,
displayName: name,
dark: true,
palette: Palette({
'primary': primary,
'accent': primary,
'background': const Color(0xFF000000),
'surface': const Color(0xFF111111),
'panel': const Color(0xFF222222),
'foreground': const Color(0xFFFFFFFF),
'success': const Color(0xFF00FF00),
'warning': const Color(0xFFFFFF00),
'error': const Color(0xFFFF0000),
}),
);
void main() {
group('ThemeController', () {
test('starts on first bundled theme', () {
final c = ThemeController(bundled: [
_def('a', const Color(0xFF111111)),
_def('b', const Color(0xFF222222)),
]);
expect(c.currentName, 'a');
});
test('honors initialName when present', () {
final c = ThemeController(
bundled: [
_def('a', const Color(0xFF000000)),
_def('b', const Color(0xFF999999))
],
initialName: 'b',
);
expect(c.currentName, 'b');
});
test('silently falls back to first when initialName is unknown', () {
final c = ThemeController(
bundled: [_def('a', const Color(0xFF000000))],
initialName: 'missing',
);
expect(c.currentName, 'a');
});
test('select changes current + notifies listeners', () {
final c = ThemeController(bundled: [
_def('a', const Color(0xFF000000)),
_def('b', const Color(0xFF333333)),
]);
var count = 0;
c.addListener(() => count++);
c.select('b');
expect(c.currentName, 'b');
expect(count, 1);
});
test('select on same theme is a no-op', () {
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000))]);
var count = 0;
c.addListener(() => count++);
c.select('a');
expect(count, 0);
});
test('select throws on unknown name', () {
final c = ThemeController(bundled: [_def('a', const Color(0xFF000000))]);
expect(() => c.select('nope'), throwsA(isA<ArgumentError>()));
});
test('registerTheme adds a new theme and rebuilds current if matching', () {
final c = ThemeController(bundled: [_def('a', const Color(0xFFAAAAAA))]);
expect(c.available.length, 1);
c.registerTheme(_def('b', const Color(0xFFBBBBBB)));
expect(c.available.length, 2);
// re-register 'a' with a different palette — current rebuilds
var count = 0;
c.addListener(() => count++);
c.registerTheme(_def('a', const Color(0xFF123456)));
expect(count, 1);
});
});
}
+80
View File
@@ -0,0 +1,80 @@
import 'dart:ui';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const loader = ThemeLoader();
group('ThemeLoader.fromYamlString', () {
test('parses palette-only theme', () {
final def = loader.fromYamlString('''
name: summer-night
display_name: Summer Night
dark: true
palette:
primary: "#00a3d2"
background: "#21262F"
foreground: "#E2E8F5"
''');
expect(def.name, 'summer-night');
expect(def.displayName, 'Summer Night');
expect(def.dark, true);
expect(def.palette.lookup('primary'), const Color(0xFF00A3D2));
expect(def.semanticOverride, isNull);
expect(def.surfaceOverride, isNull);
});
test('captures semantic overrides as palette-resolved colors', () {
final def = loader.fromYamlString('''
name: t
palette:
red: "#FF0000"
blue: "#0000FF"
semantic:
mainchrome: red
focus: "#123456"
''');
expect(
def.semanticOverride!.lookup('mainchrome'), const Color(0xFFFF0000));
expect(def.semanticOverride!.lookup('focus'), const Color(0xFF123456));
});
test('captures surface overrides as ref strings', () {
final def = loader.fromYamlString('''
name: t
palette:
red: "#FF0000"
surface:
panel.background: "semantic.mainchrome"
panel.border: red
''');
expect(def.surfaceOverride, {
'panel.background': 'semantic.mainchrome',
'panel.border': 'red',
});
});
test('throws when name is missing and no fallback', () {
expect(
() => loader.fromYamlString('palette: { fg: "#fff" }'),
throwsA(isA<FormatException>()),
);
});
test('throws when palette is missing', () {
expect(
() => loader.fromYamlString('name: t'),
throwsA(isA<FormatException>()),
);
});
test('fallback name is used when name is absent', () {
final def = loader.fromYamlString(
'palette: { fg: "#fff" }',
fallbackName: 'inferred',
);
expect(def.name, 'inferred');
});
});
}
+146
View File
@@ -0,0 +1,146 @@
import 'dart:ui';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const resolver = ThemeResolver();
Palette paletteOf(Map<String, Color> colors) => Palette(colors);
group('ThemeResolver default fallbacks', () {
test('design-shape palette resolves every token', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'bg': Color(0xFF20202C),
'bgSunken': Color(0xFF1A1A24),
'surface': Color(0xFF242838),
'surfaceHi': Color(0xFF2C3046),
'border': Color(0xFF343850),
'borderHi': Color(0xFF3C445C),
'textHi': Color(0xFFE6E8F2),
'text': Color(0xFFB1BBE3),
'textDim': Color(0xFF78809C),
'textMute': Color(0xFF545C84),
'accent': Color(0xFF78A0F8),
'accentPress': Color(0xFF6C90DC),
'accentSoft': Color(0x2178A0F8),
'onAccent': Color(0xFF0D1020),
'ok': Color(0xFF7DD3A8),
'warn': Color(0xFFE6C370),
'err': Color(0xFFE87D7D),
'info': Color(0xFF78A0F8),
}),
);
expect(tokens.globalBackground, const Color(0xFF20202C));
expect(tokens.globalForeground, const Color(0xFFE6E8F2));
expect(tokens.panelBackground, const Color(0xFF1A1A24));
expect(tokens.sidebarItemSelected, const Color(0xFF2C3046));
expect(tokens.statusSuccess, const Color(0xFF7DD3A8));
expect(tokens.statusError, const Color(0xFFE87D7D));
expect(tokens.globalTextMuted, const Color(0xFF78809C));
expect(tokens.buttonBackground, const Color(0xFF78A0F8));
expect(tokens.buttonForeground, const Color(0xFF0D1020));
});
test('legacy palette keys still resolve via fallback chains', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'primary': Color(0xFF00A3D2),
'accent': Color(0xFFFA5F8B),
'background': Color(0xFF21262F),
'surface': Color(0xFF393E48),
'panel': Color(0xFF292E38),
'foreground': Color(0xFFE2E8F5),
'muted': Color(0xFF6A7280),
'success': Color(0xFF00AB9A),
'warning': Color(0xFFD08447),
'error': Color(0xFFF06C6F),
'info': Color(0xFF00A3D2),
}),
);
expect(tokens.globalBackground, const Color(0xFF21262F));
expect(tokens.globalForeground, const Color(0xFFE2E8F5));
expect(tokens.panelBackground, const Color(0xFF292E38));
expect(tokens.statusSuccess, const Color(0xFF00AB9A));
expect(tokens.statusError, const Color(0xFFF06C6F));
});
});
group('ThemeResolver overrides', () {
test('surface override wins over default', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'bg': Color(0xFF333333),
'bgSunken': Color(0xFF222222),
'surface': Color(0xFF444444),
'border': Color(0xFF555555),
'textHi': Color(0xFFFFFFFF),
'accent': Color(0xFF111111),
'ok': Color(0xFF008800),
'warn': Color(0xFFFF8800),
'err': Color(0xFFFF0000),
'info': Color(0xFF111111),
}),
surfaceOverride: const {
'panel.background': '#00FF00',
},
);
expect(tokens.panelBackground, const Color(0xFF00FF00));
});
test('semantic override propagates when palette key is absent', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'bg': Color(0xFF333333),
'bgSunken': Color(0xFF222222),
'surface': Color(0xFF444444),
'surfaceHi': Color(0xFF555555),
'border': Color(0xFF666666),
'textHi': Color(0xFFFFFFFF),
'ok': Color(0xFF008800),
'warn': Color(0xFFFF8800),
'err': Color(0xFFFF0000),
'info': Color(0xFF111111),
}),
semanticOverride: const SemanticRoles({
'focus': Color(0xFF007777),
}),
);
// No 'accent' in palette, so globalFocus falls through to
// semantic.focus which is overridden.
expect(tokens.globalFocus, const Color(0xFF007777));
});
test('extensionOverride populates extensionTokens map', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'bg': Color(0xFF333333),
'bgSunken': Color(0xFF222222),
'surface': Color(0xFF444444),
'border': Color(0xFF555555),
'textHi': Color(0xFFFFFFFF),
'accent': Color(0xFF111111),
}),
extensionOverride: const {
'ext.sqlite.table.background': '#ABCDEF',
},
);
expect(tokens.extensionTokens['ext.sqlite.table.background'],
const Color(0xFFABCDEF));
});
});
group('Palette.parseHex', () {
test('parses 6-digit hex as opaque', () {
expect(Palette.parseHex('#112233'), const Color(0xFF112233));
});
test('parses 8-digit hex with alpha', () {
expect(Palette.parseHex('#80112233'), const Color(0x80112233));
});
test('returns null on invalid', () {
expect(Palette.parseHex('nope'), isNull);
expect(Palette.parseHex('#12'), isNull);
});
});
}