diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchKeyDispatcher.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchKeyDispatcher.cs
index ba7e0dd..a306497 100644
--- a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchKeyDispatcher.cs
+++ b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchKeyDispatcher.cs
@@ -3,7 +3,6 @@
using SharpConsoleUI;
using SharpConsoleUI.Controls;
-using SharpConsoleUI.Themes;
namespace Cratis.Cli.Commands.Chronicle.Workbench;
@@ -142,17 +141,17 @@ public void Dispatch(KeyPressedEventArgs e)
// Theme switching
case ConsoleKey.F9:
- ApplyTheme(new ModernGrayTheme());
+ ApplyThemeSlot(0);
e.Handled = true;
break;
case ConsoleKey.F10:
- ApplyTheme(new ClassicTheme());
+ ApplyThemeSlot(1);
e.Handled = true;
break;
case ConsoleKey.F11:
- ApplyThemeByName("SharpConsoleUI.Plugins.DeveloperTools.DevDarkTheme, SharpConsoleUI");
+ ApplyThemeSlot(2);
e.Handled = true;
break;
@@ -304,15 +303,12 @@ bool DispatchCurrentViewAction(ConsoleKey key, ConsoleModifiers modifiers)
return true;
}
- void ApplyTheme(ITheme theme) =>
- windowSystem.ThemeStateService.SetTheme(theme);
-
- void ApplyThemeByName(string typeName)
+ void ApplyThemeSlot(int index)
{
- var type = Type.GetType(typeName);
- if (type is not null && Activator.CreateInstance(type) is ITheme theme)
+ var slots = WorkbenchThemes.GetPrimarySlots(windowSystem);
+ if (index >= 0 && index < slots.Count)
{
- ApplyTheme(theme);
+ slots[index].Apply();
}
}
}
diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchMenuBar.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchMenuBar.cs
index c42c9c0..8813027 100644
--- a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchMenuBar.cs
+++ b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchMenuBar.cs
@@ -4,7 +4,6 @@
using SharpConsoleUI;
using SharpConsoleUI.Builders;
using SharpConsoleUI.Controls;
-using SharpConsoleUI.Themes;
namespace Cratis.Cli.Commands.Chronicle.Workbench;
@@ -44,27 +43,34 @@ public MenuControl Build()
state.Save();
Environment.Exit(0);
}))
- .AddItem("Help", m => m
- .AddItem("Keyboard Shortcuts", "?", () => overlays.OpenHelpOverlay())
- .AddSeparator()
- .AddItem("Theme: Modern Gray", "F9", () => ApplyTheme(new ModernGrayTheme()))
- .AddItem("Theme: Classic", "F10", () => ApplyTheme(new ClassicTheme()))
- .AddItem("Theme: Dev Dark", "F11", () => ApplyThemeByName("SharpConsoleUI.Plugins.DeveloperTools.DevDarkTheme, SharpConsoleUI")))
+ .AddItem("Help", BuildHelpMenu)
.Build();
menu.StickyPosition = StickyPosition.Top;
return menu;
}
- void ApplyTheme(ITheme theme) =>
- windowSystem.ThemeStateService.SetTheme(theme);
+ void BuildHelpMenu(MenuItemBuilder help)
+ {
+ help.AddItem("Keyboard Shortcuts", "?", overlays.OpenHelpOverlay)
+ .AddSeparator();
+
+ var slots = WorkbenchThemes.GetPrimarySlots(windowSystem);
+ string[] shortcuts = ["F9", "F10", "F11"];
+ for (var i = 0; i < slots.Count && i < shortcuts.Length; i++)
+ {
+ var slot = slots[i];
+ help.AddItem($"Theme: {slot.Label}", shortcuts[i], slot.Apply);
+ }
+
+ help.AddItem("More Themes", BuildThemeSubmenu);
+ }
- void ApplyThemeByName(string typeName)
+ void BuildThemeSubmenu(MenuItemBuilder submenu)
{
- var type = Type.GetType(typeName);
- if (type is not null && Activator.CreateInstance(type) is ITheme theme)
+ foreach (var name in WorkbenchThemes.GetAvailableThemeNames(windowSystem))
{
- ApplyTheme(theme);
+ submenu.AddItem(name, () => WorkbenchThemes.Apply(windowSystem, name));
}
}
}
diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchOverlays.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchOverlays.cs
index 3c58471..6874eea 100644
--- a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchOverlays.cs
+++ b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchOverlays.cs
@@ -40,6 +40,8 @@ public void OpenHelpOverlay()
var mut = WorkbenchColors.Muted.ToMarkup();
var acc = WorkbenchColors.Accent.ToMarkup();
+ var themeLabels = string.Join(" / ", WorkbenchThemes.GetPrimarySlots(windowSystem).Select(s => s.Label));
+
var activeIdx = navigation.CurrentViewIndex;
var currentViewHelp = string.Empty;
if (activeIdx >= 0 && activeIdx < views.Length && !string.IsNullOrEmpty(views[activeIdx].ViewHelp))
@@ -78,7 +80,7 @@ public void OpenHelpOverlay()
$" [{mut}]Ctrl+C[/] Copy detail pane content to clipboard\n" +
"\n" +
$"[bold {acc}]THEMES[/]\n" +
- $" [{mut}]F9 / F10 / F11[/] Modern Gray / Classic / Dev Dark theme\n" +
+ $" [{mut}]F9 / F10 / F11[/] {themeLabels} theme\n" +
"\n" +
$"[bold {acc}]GENERAL[/]\n" +
$" [{mut}]+ / -[/] Increase / decrease refresh interval\n" +
diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchThemes.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchThemes.cs
new file mode 100644
index 0000000..0de0997
--- /dev/null
+++ b/Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchThemes.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections;
+using System.Reflection;
+using SharpConsoleUI;
+using SharpConsoleUI.Themes;
+
+namespace Cratis.Cli.Commands.Chronicle.Workbench;
+
+///
+/// A primary theme exposed on an F-key / top-level menu slot: the label to display and the action
+/// that applies it. Resolved per running SharpConsoleUI version so the slots stay valid on both.
+///
+/// The display label, for example Modern Gray.
+/// Applies the theme to the window system.
+public record WorkbenchThemeSlot(string Label, Action Apply);
+
+///
+/// Bridges the two SharpConsoleUI theme APIs via reflection so the workbench compiles and runs
+/// against both the published 2.4.78 package and the upcoming 2.4.79.
+///
+/// 2.4.78 exposes a process-global static SharpConsoleUI.Themes.ThemeRegistry and ships
+/// ClassicTheme plus a DevDarkTheme; 2.4.79 removes those, moves the developer theme
+/// out of the library, and replaces the static registry with a per-instance
+/// windowSystem.ThemeRegistryService. Referencing any of those types directly would break
+/// compilation against the other version, so everything here is resolved at runtime.
+///
+///
+/// Theme application goes through the version-stable ThemeStateService.SwitchTheme(name)
+/// (present in both versions; an unknown name is a safe no-op). The three primary slots preserve the
+/// original 2.4.78 behaviour — Modern Gray / Classic / Dev Dark — and map to Modern Gray / Forest /
+/// Crimson on 2.4.79 where Classic and Dev Dark no longer exist.
+///
+///
+/// This reflection bridge is temporary: it exists only to span the 2.4.78 -> 2.4.79 transition. Once
+/// the SharpConsoleUI dependency is pinned to 2.4.79+ and 2.4.78 is no longer supported, it can be
+/// simplified to direct windowSystem.ThemeRegistryService calls and the static-registry and
+/// Dev Dark fallbacks removed.
+///
+///
+public static class WorkbenchThemes
+{
+ ///
+ /// Returns the three primary theme slots for the F9 / F10 / F11 shortcuts and top-level menu items,
+ /// resolved for the running SharpConsoleUI version.
+ ///
+ /// The window system the slots apply themes to.
+ /// Exactly three slots, in F9, F10, F11 order.
+ public static IReadOnlyList GetPrimarySlots(ConsoleWindowSystem windowSystem)
+ {
+ if (HasInstanceRegistry(windowSystem))
+ {
+ // 2.4.79: Classic and Dev Dark are gone; offer dark palette themes from the catalogue.
+ return
+ [
+ new WorkbenchThemeSlot("Modern Gray", () => Apply(windowSystem, "ModernGray")),
+ new WorkbenchThemeSlot("Forest", () => Apply(windowSystem, "Forest")),
+ new WorkbenchThemeSlot("Crimson", () => Apply(windowSystem, "Crimson"))
+ ];
+ }
+
+ // 2.4.78: preserve the original behaviour — Modern Gray / Classic / Dev Dark.
+ return
+ [
+ new WorkbenchThemeSlot("Modern Gray", () => Apply(windowSystem, "ModernGray")),
+ new WorkbenchThemeSlot("Classic", () => Apply(windowSystem, "Classic")),
+ new WorkbenchThemeSlot("Dev Dark", () => ApplyType(windowSystem, "SharpConsoleUI.Plugins.DeveloperTools.DevDarkTheme, SharpConsoleUI"))
+ ];
+ }
+
+ ///
+ /// Returns the names of every theme registered with the running SharpConsoleUI version, resolving
+ /// the per-instance registry (2.4.79) first and falling back to the static registry (2.4.78).
+ /// Returns an empty list if neither is present.
+ ///
+ /// The window system whose per-instance registry is preferred.
+ /// The available theme names, or an empty list when no registry can be resolved.
+ public static IReadOnlyList GetAvailableThemeNames(ConsoleWindowSystem windowSystem)
+ {
+ var registryService = GetInstanceRegistry(windowSystem);
+ if (registryService is not null &&
+ InvokeGetAvailableThemeNames(registryService.GetType(), registryService) is { } instanceNames)
+ {
+ return instanceNames;
+ }
+
+ var staticRegistry = Type.GetType("SharpConsoleUI.Themes.ThemeRegistry, SharpConsoleUI");
+ if (staticRegistry is not null &&
+ InvokeGetAvailableThemeNames(staticRegistry, target: null) is { } staticNames)
+ {
+ return staticNames;
+ }
+
+ return [];
+ }
+
+ ///
+ /// Applies a theme by name through the version-stable SwitchTheme API. Unknown names are a
+ /// safe no-op (the library returns ), so callers can offer names that only
+ /// exist on a subset of supported versions without guarding each one.
+ ///
+ /// The window system whose theme state service performs the switch.
+ /// The registered theme name to apply.
+ public static void Apply(ConsoleWindowSystem windowSystem, string themeName) =>
+ windowSystem.ThemeStateService.SwitchTheme(themeName);
+
+ static bool HasInstanceRegistry(ConsoleWindowSystem windowSystem) =>
+ GetInstanceRegistry(windowSystem) is not null;
+
+ static object? GetInstanceRegistry(ConsoleWindowSystem windowSystem) =>
+ windowSystem.GetType()
+ .GetProperty("ThemeRegistryService", BindingFlags.Public | BindingFlags.Instance)
+ ?.GetValue(windowSystem);
+
+ static void ApplyType(ConsoleWindowSystem windowSystem, string assemblyQualifiedTypeName)
+ {
+ var type = Type.GetType(assemblyQualifiedTypeName);
+ if (type is not null && Activator.CreateInstance(type) is ITheme theme)
+ {
+ windowSystem.ThemeStateService.SetTheme(theme);
+ }
+ }
+
+ static IReadOnlyList? InvokeGetAvailableThemeNames(Type registryType, object? target)
+ {
+ var result = registryType
+ .GetMethod("GetAvailableThemeNames", BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
+ ?.Invoke(target, parameters: null);
+
+ if (result is IEnumerable names)
+ {
+ return [.. names.Cast