From 80e065b537b3aad6aa67f0f21db6912f85c52d5a Mon Sep 17 00:00:00 2001 From: Nikolaos Protopapas Date: Sat, 20 Jun 2026 15:09:48 +0300 Subject: [PATCH] Fix workbench theme switcher against current and upcoming SharpConsoleUI The Help-menu and keyboard theme switchers built themes by concrete type (new ClassicTheme(), new ModernGrayTheme()) and by reflective type name (SharpConsoleUI.Plugins.DeveloperTools.DevDarkTheme). SharpConsoleUI's upcoming release removes ClassicTheme, moves DevDarkTheme out of the library, and replaces the process-global static ThemeRegistry with a per-instance windowSystem.ThemeRegistryService. Referencing any of those types directly compiles against only one version. Resolve everything at runtime through a new WorkbenchThemes helper so the same source compiles and runs against both versions. Theme application uses the version-stable ThemeStateService.SwitchTheme(name). The three primary F9/F10/F11 slots are resolved per version: on the current package they keep the original Modern Gray / Classic / Dev Dark behaviour; on the next release, where Classic and Dev Dark no longer exist, they map to Modern Gray / Forest / Crimson. The Help menu also gains a "More Themes" submenu listing every registered theme, and the keyboard-shortcuts overlay reflects whichever primary themes are active. The reflection bridge is temporary and scoped to the 2.4.78 -> 2.4.79 transition; once the dependency is pinned to 2.4.79+ it can be simplified to direct ThemeRegistryService calls. --- .../windows/WorkbenchKeyDispatcher.cs | 18 +-- .../Workbench/windows/WorkbenchMenuBar.cs | 32 ++-- .../Workbench/windows/WorkbenchOverlays.cs | 4 +- .../Workbench/windows/WorkbenchThemes.cs | 138 ++++++++++++++++++ 4 files changed, 167 insertions(+), 25 deletions(-) create mode 100644 Source/Cli/Commands/Chronicle/Workbench/windows/WorkbenchThemes.cs 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().Select(n => n?.ToString() ?? string.Empty)]; + } + + return null; + } +}