persist the chosen theme per repo across loads (T-293)
ThemeController.select() applied live but never persisted, so every restart reset to the first bundled theme. Add wireThemePersistence (facade): on theme change write app.theme (global default) + project.theme (the open repo's .clide/settings.yaml); on settings change — notably when a repo opens and its project values load — restore the most specific saved theme (project then app). The name encodes the -hc variant so high-contrast persists; an unknown/removed theme is ignored so a stale value can't wedge startup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The chosen theme now persists across restarts**, per repo. Picking a theme
|
||||
(status-bar switcher or Settings) writes it to the repo's
|
||||
`.clide/settings.yaml` (and a global default), and reopening the repo restores
|
||||
it — including the high-contrast variant. A removed theme falls back to the
|
||||
default instead of resetting silently. (T-293)
|
||||
- Folded activity and agent-run cards in the Claude conversation now use the
|
||||
same bottom spacing as the prose cards around them, instead of sitting
|
||||
cramped 3px below the next card. (T-282)
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:clide/kernel/src/clipboard.dart';
|
||||
import 'package:clide/kernel/src/commands/keybindings.dart';
|
||||
import 'package:clide/kernel/src/keymap/keymap_service.dart';
|
||||
import 'package:clide/kernel/src/text_zoom.dart';
|
||||
import 'package:clide/kernel/src/theme/theme_persistence.dart';
|
||||
import 'package:clide/kernel/src/toast.dart';
|
||||
import 'package:clide/kernel/src/commands/palette.dart';
|
||||
import 'package:clide/kernel/src/commands/registry.dart';
|
||||
@@ -158,6 +159,7 @@ class KernelServices {
|
||||
}
|
||||
|
||||
final theme = ThemeController(bundled: bundledThemes);
|
||||
wireThemePersistence(theme, settings);
|
||||
final panels = PanelRegistry();
|
||||
final arrangement = LayoutArrangement();
|
||||
final commands = CommandRegistry();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/// Persist the chosen theme and restore it across loads (T-293).
|
||||
///
|
||||
/// clide runs a single live [ThemeController]; this bridges it to the
|
||||
/// [SettingsStore] so a choice survives a restart. On every theme change we
|
||||
/// write `app.theme` (the global last-used default) and, when a repo is open,
|
||||
/// `project.theme` in that repo's `.clide/settings.yaml`. Whenever the settings
|
||||
/// change — notably when a repo opens and its project values load — we restore
|
||||
/// the most specific saved theme: `project.theme`, else the global `app.theme`.
|
||||
/// The current name already encodes the high-contrast variant (`<base>-hc`), so
|
||||
/// the toggle persists for free. An unknown/removed theme name is ignored, so a
|
||||
/// stale value can't wedge startup.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/src/settings.dart';
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
|
||||
void wireThemePersistence(ThemeController theme, SettingsStore settings) {
|
||||
theme.addListener(() {
|
||||
final name = theme.currentName;
|
||||
// Global default — lets a brand-new / unthemed repo inherit the last choice.
|
||||
if (settings.get<String>('app.theme') != name) {
|
||||
unawaited(settings.set<String>('app.theme', name));
|
||||
}
|
||||
// Per-repo, when one is open.
|
||||
if (settings.projectDir != null && settings.get<String>('project.theme') != name) {
|
||||
unawaited(settings.set<String>('project.theme', name));
|
||||
}
|
||||
});
|
||||
|
||||
void restore() {
|
||||
final saved = settings.get<String>('project.theme') ?? settings.get<String>('app.theme');
|
||||
if (saved == null || saved == theme.currentName) return;
|
||||
if (theme.available.any((d) => d.name == saved)) theme.select(saved);
|
||||
}
|
||||
|
||||
settings.addListener(restore);
|
||||
restore(); // apply the global default already loaded at boot
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/kernel/src/theme/theme_persistence.dart';
|
||||
import 'package:flutter/widgets.dart' show Color;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
ThemeDefinition _def(String name) => ThemeDefinition(
|
||||
name: name,
|
||||
displayName: name,
|
||||
dark: true,
|
||||
palette: Palette(const {
|
||||
'primary': Color(0xFF00A3D2),
|
||||
'accent': Color(0xFFFA5F8B),
|
||||
'background': Color(0xFF21262F),
|
||||
'surface': Color(0xFF393E48),
|
||||
'panel': Color(0xFF292E38),
|
||||
'foreground': Color(0xFFE2E8F5),
|
||||
'success': Color(0xFF00AB9A),
|
||||
'warning': Color(0xFFD08447),
|
||||
'error': Color(0xFFF06C6F),
|
||||
}),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('wireThemePersistence (T-293)', () {
|
||||
late Directory appTmp;
|
||||
late Directory repoTmp;
|
||||
late SettingsStore settings;
|
||||
late ThemeController theme;
|
||||
|
||||
setUp(() async {
|
||||
appTmp = await Directory.systemTemp.createTemp('clide_theme_app_');
|
||||
repoTmp = await Directory.systemTemp.createTemp('clide_theme_repo_');
|
||||
settings = SettingsStore(appDir: appTmp);
|
||||
await settings.load();
|
||||
theme = ThemeController(bundled: [_def('summer-night'), _def('forest'), _def('paper'), _def('paper-hc')]);
|
||||
wireThemePersistence(theme, settings);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await pumpEventQueue(); // flush any in-flight unawaited set() file writes
|
||||
settings.dispose();
|
||||
theme.dispose();
|
||||
for (final d in [appTmp, repoTmp]) {
|
||||
if (await d.exists()) {
|
||||
try {
|
||||
await d.delete(recursive: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('selecting with no repo open persists app.theme (global default)', () async {
|
||||
theme.select('forest');
|
||||
expect(settings.get<String>('app.theme'), 'forest');
|
||||
expect(settings.get<String>('project.theme'), isNull);
|
||||
});
|
||||
|
||||
test('selecting with a repo open persists project.theme into that repo', () async {
|
||||
await settings.setProjectDir(repoTmp);
|
||||
theme.select('forest');
|
||||
expect(settings.get<String>('project.theme'), 'forest');
|
||||
await pumpEventQueue(); // let the unawaited file write flush
|
||||
expect(await File('${repoTmp.path}/.clide/settings.yaml').exists(), isTrue);
|
||||
});
|
||||
|
||||
test('the high-contrast variant persists (the name encodes -hc)', () async {
|
||||
theme.select('paper-hc');
|
||||
expect(settings.get<String>('app.theme'), 'paper-hc');
|
||||
});
|
||||
|
||||
test('opening a repo restores project.theme over the global app.theme', () async {
|
||||
await settings.set<String>('app.theme', 'forest');
|
||||
await File('${repoTmp.path}/.clide/settings.yaml').create(recursive: true);
|
||||
await File('${repoTmp.path}/.clide/settings.yaml').writeAsString('project:\n theme: paper\n');
|
||||
|
||||
await settings.setProjectDir(repoTmp); // loads project values + notifies → restore
|
||||
|
||||
expect(theme.currentName, 'paper');
|
||||
});
|
||||
|
||||
test('restores the global app.theme at wiring time (boot)', () async {
|
||||
await File('${appTmp.path}/settings.yaml').writeAsString('app:\n theme: forest\n');
|
||||
final s2 = SettingsStore(appDir: appTmp);
|
||||
await s2.load();
|
||||
final t2 = ThemeController(bundled: [_def('summer-night'), _def('forest')]);
|
||||
expect(t2.currentName, 'summer-night'); // bundled.first before wiring
|
||||
|
||||
wireThemePersistence(t2, s2);
|
||||
|
||||
expect(t2.currentName, 'forest'); // restored from app.theme
|
||||
s2.dispose();
|
||||
t2.dispose();
|
||||
});
|
||||
|
||||
test('an unknown saved theme is ignored (no throw, keeps current)', () async {
|
||||
await File('${repoTmp.path}/.clide/settings.yaml').create(recursive: true);
|
||||
await File('${repoTmp.path}/.clide/settings.yaml').writeAsString('project:\n theme: gone-theme\n');
|
||||
|
||||
await settings.setProjectDir(repoTmp);
|
||||
|
||||
expect(theme.currentName, 'summer-night'); // unchanged
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user