Forge
csharpdeeb25a2
1using System;
2using System.Diagnostics.CodeAnalysis;
3using System.Text.Json;
4using Avalonia.Media;
5using Avalonia.Threading;
6
7namespace CascadeIDE.Services;
8
9/// <summary>
10/// Применение темы UI из JSON (тот же формат, что ide_get_ui_theme).
11/// Обновляет Application.Resources; элементы с DynamicResource перерисуются.
12/// </summary>
13public static class UiThemeApply
14{
15 private static (string path, DateTime lastWrite, string json)? _themeCache;
16
17 /// <summary>После успешного обновления <see cref="Application.Current"/>.<c>Resources</c> (смена пресета / MCP).</summary>
18 public static event EventHandler? ThemeApplied;
19
20 /// <summary>Загружает тему из файла рядом с приложением; при отсутствии — из встроенного ресурса (<see cref="BundledAppContent"/>). Перечитывает с диска только если изменились путь или дата модификации.</summary>
21 public static string GetThemeJsonFromFile(string filePath)
22 {
23 var path = CanonicalFilePath.Normalize(filePath);
24 if (File.Exists(path))
25 {
26 var lastWrite = File.GetLastWriteTimeUtc(path);
27 if (_themeCache is { } c && c.path == path && c.lastWrite == lastWrite)
28 return c.json;
29 var json = File.ReadAllText(path);
30 _themeCache = (path, lastWrite, json);
31 return json;
32 }
33
34 if (TryRelativeUnderAppBase(path, out var rel) && BundledAppContent.TryReadEmbeddedText(rel, out var embedded))
35 return embedded;
36
37 return "{}";
38 }
39
40 private static bool TryRelativeUnderAppBase(string fullPath, [NotNullWhen(true)] out string? relative)
41 {
42 relative = null;
43 try
44 {
45 var baseDir = CanonicalFilePath.Normalize(AppContext.BaseDirectory);
46 var fp = CanonicalFilePath.Normalize(fullPath);
47 if (!fp.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase))
48 return false;
49 relative = Path.GetRelativePath(baseDir, fp).Replace('\\', '/');
50 return relative.Length > 0;
51 }
52 catch
53 {
54 return false;
55 }
56 }
57
58 /// <summary>Тёмная тема: Themes/dark-theme.json рядом с exe.</summary>
59 public static string GetDarkThemeJson() =>
60 GetThemeJsonFromFile(Path.Combine(AppContext.BaseDirectory, "Themes", "dark-theme.json"));
61
62 /// <summary>Светлая тема: Themes/light-theme.json рядом с exe.</summary>
63 public static string GetLightThemeJson() =>
64 GetThemeJsonFromFile(Path.Combine(AppContext.BaseDirectory, "Themes", "light-theme.json"));
65
66 /// <summary>Тема «как Cursor»: тёмная с акцентом #3794FF. Themes/cursor-like-theme.json.</summary>
67 public static string GetCursorLikeThemeJson() =>
68 GetThemeJsonFromFile(Path.Combine(AppContext.BaseDirectory, "Themes", "cursor-like-theme.json"));
69
70 /// <summary>Тема Power Mode: неоново-тёмная «космическая» палитра (циан). Themes/power-theme.json.</summary>
71 public static string GetPowerThemeJson() =>
72 GetThemeJsonFromFile(Path.Combine(AppContext.BaseDirectory, "Themes", "power-theme.json"));
73
74 /// <summary>Power cockpit по концепт-PNG: фиолет/янтарь/бирюза L1–L3, фуксийные каймы. Themes/power-cockpit-concept-theme.json.</summary>
75 public static string GetPowerCockpitConceptThemeJson() =>
76 GetThemeJsonFromFile(Path.Combine(AppContext.BaseDirectory, "Themes", "power-cockpit-concept-theme.json"));
77
78 /// <summary>Ключи ресурсов в Application.Resources. Должны совпадать с App.axaml и DynamicResource в XAML.</summary>
79 public static class Keys
80 {
81 public const string MainWindowBackground = "CascadeTheme.MainWindowBackground";
82 public const string MenuBackground = "CascadeTheme.MenuBackground";
83 public const string MenuForeground = "CascadeTheme.MenuForeground";
84 public const string ButtonBackground = "CascadeTheme.ButtonBackground";
85 public const string ButtonForeground = "CascadeTheme.ButtonForeground";
86 public const string ButtonBorderBrush = "CascadeTheme.ButtonBorderBrush";
87 public const string ButtonHoverBackground = "CascadeTheme.ButtonHoverBackground";
88 public const string ButtonDisabledBackground = "CascadeTheme.ButtonDisabledBackground";
89 public const string ButtonDisabledForeground = "CascadeTheme.ButtonDisabledForeground";
90 public const string ToolbarBackground = "CascadeTheme.ToolbarBackground";
91 public const string ToolbarTextForeground = "CascadeTheme.ToolbarTextForeground";
92 public const string ToolbarErrorForeground = "CascadeTheme.ToolbarErrorForeground";
93 public const string EditorBackground = "CascadeTheme.EditorBackground";
94 public const string EditorForeground = "CascadeTheme.EditorForeground";
95 public const string EditorSelectionBrush = "CascadeTheme.EditorSelectionBrush";
96 public const string EditorSelectionForeground = "CascadeTheme.EditorSelectionForeground";
97 public const string EditorColumnBorderBrush = "CascadeTheme.EditorColumnBorderBrush";
98 public const string WorkspacePanelBorderBrush = "CascadeTheme.WorkspacePanelBorderBrush";
99 public const string EditorColumnBackground = "CascadeTheme.EditorColumnBackground";
100 public const string CurrentFileForeground = "CascadeTheme.CurrentFileForeground";
101 public const string MarkdownPreviewPanelBackground = "CascadeTheme.MarkdownPreviewPanelBackground";
102 public const string MarkdownPreviewPanelBorderBrush = "CascadeTheme.MarkdownPreviewPanelBorderBrush";
103 public const string SolutionExplorerBorderBrush = "CascadeTheme.SolutionExplorerBorderBrush";
104 public const string SolutionExplorerHeaderForeground = "CascadeTheme.SolutionExplorerHeaderForeground";
105 public const string PanelChromeTitleForeground = "CascadeTheme.PanelChromeTitleForeground";
106 public const string PanelTitleAccentBrush = "CascadeTheme.PanelTitleAccentBrush";
107 public const string PanelChromeHeaderBackground = "CascadeTheme.PanelChromeHeaderBackground";
108 public const string PanelChromeHeaderSeparatorBrush = "CascadeTheme.PanelChromeHeaderSeparatorBrush";
109 public const string PanelChromeMenuGlyphForeground = "CascadeTheme.PanelChromeMenuGlyphForeground";
110 public const string BuildOutputBackground = "CascadeTheme.BuildOutputBackground";
111 public const string BuildOutputBorderBrush = "CascadeTheme.BuildOutputBorderBrush";
112 public const string ChatPanelBackground = "CascadeTheme.ChatPanelBackground";
113 public const string ChatLabelForeground = "CascadeTheme.ChatLabelForeground";
114 public const string ChatMessageBubbleBackground = "CascadeTheme.ChatMessageBubbleBackground";
115 public const string ChatMessageContentForeground = "CascadeTheme.ChatMessageContentForeground";
116 public const string PanelBackgroundBrush = "CascadeTheme.PanelBackgroundBrush";
117 public const string InsetSurfaceBackground = "CascadeTheme.InsetSurfaceBackground";
118 public const string InsetSurfaceBorderBrush = "CascadeTheme.InsetSurfaceBorderBrush";
119 public const string StatusChipBackground = "CascadeTheme.StatusChipBackground";
120 public const string StatusChipBorderBrush = "CascadeTheme.StatusChipBorderBrush";
121 public const string StatusChipForeground = "CascadeTheme.StatusChipForeground";
122 public const string SendButtonBackground = "CascadeTheme.SendButtonBackground";
123 public const string SendButtonForeground = "CascadeTheme.SendButtonForeground";
124 public const string TerminalBackground = "CascadeTheme.TerminalBackground";
125 public const string TerminalForeground = "CascadeTheme.TerminalForeground";
126 public const string TerminalInputBackground = "CascadeTheme.TerminalInputBackground";
127 public const string McpBannerBackground = "CascadeTheme.McpBannerBackground";
128 public const string McpBannerForeground = "CascadeTheme.McpBannerForeground";
129 public const string PreviewWindowBackground = "CascadeTheme.PreviewWindowBackground";
130 /// <summary>Опционально: секция power_cockpit в JSON темы (неон, safety, Workspace Health).</summary>
131 public const string PowerNeonBorder = "CascadeTheme.PowerNeonBorder";
132 public const string PowerNeonAccent = "CascadeTheme.PowerNeonAccent";
133 public const string PowerCockpitPanelBackground = "CascadeTheme.PowerCockpitPanelBackground";
134 public const string PowerSafetyDockBackground = "CascadeTheme.PowerSafetyDockBackground";
135 public const string PowerWorkspaceHealthStripBackground = "CascadeTheme.PowerWorkspaceHealthStripBackground";
136 public const string PowerSafetyObserve = "CascadeTheme.PowerSafetyObserve";
137 public const string PowerSafetyConfirm = "CascadeTheme.PowerSafetyConfirm";
138 public const string PowerSafetyAutonomous = "CascadeTheme.PowerSafetyAutonomous";
139 public const string PowerEmergency = "CascadeTheme.PowerEmergency";
140 }
141
142 /// <summary>Применить тему из JSON (формат ide_get_ui_theme). Вызывать из UI-потока. Возвращает "OK" или сообщение об ошибке (для ответа тулу ide_set_ui_theme).</summary>
143 public static string Apply(string themeJson)
144 {
145 if (string.IsNullOrWhiteSpace(themeJson))
146 return "OK";
147 JsonDocument doc;
148 try
149 {
150 doc = JsonDocument.Parse(themeJson);
151 }
152 catch (JsonException ex)
153 {
154 var msg = $"Invalid JSON: {ex.Message}";
155 System.Diagnostics.Debug.WriteLine($"ide_set_ui_theme: {msg}");
156 return msg;
157 }
158 using (doc)
159 {
160 var root = doc.RootElement;
161 var res = GetResourceDictionary();
162 if (res is null)
163 return "Application resources not available.";
164
165 Set(res, Keys.MainWindowBackground, GetColor(root, "main_window", "background"));
166 Set(res, Keys.MenuBackground, GetColor(root, "menu", "background"));
167 Set(res, Keys.MenuForeground, GetColor(root, "menu", "foreground"));
168 Set(res, Keys.ButtonBackground, GetColor(root, "button", "background"));
169 Set(res, Keys.ButtonForeground, GetColor(root, "button", "foreground"));
170 Set(res, Keys.ButtonBorderBrush, GetColor(root, "button", "border_brush"));
171 Set(res, Keys.ButtonHoverBackground, GetColor(root, "button", "hover_background"));
172 Set(res, Keys.ButtonDisabledBackground, GetColor(root, "button", "disabled_background"));
173 Set(res, Keys.ButtonDisabledForeground, GetColor(root, "button", "disabled_foreground"));
174 Set(res, Keys.ToolbarBackground, GetColor(root, "toolbar", "background"));
175 Set(res, Keys.ToolbarTextForeground, GetColor(root, "toolbar_text", "foreground"));
176 Set(res, Keys.ToolbarErrorForeground, GetColor(root, "toolbar_text", "error_foreground"));
177 Set(res, Keys.EditorBackground, GetColor(root, "editor", "background"));
178 Set(res, Keys.EditorForeground, GetColor(root, "editor", "foreground"));
179 Set(res, Keys.EditorSelectionBrush, GetColor(root, "editor", "selection_brush"));
180 Set(res, Keys.EditorSelectionForeground, GetColor(root, "editor", "selection_foreground"));
181 Set(res, Keys.EditorColumnBorderBrush, GetColor(root, "editor_column", "border_brush"));
182 Set(res, Keys.WorkspacePanelBorderBrush, GetWorkspacePanelBorderBrush(root));
183 Set(res, Keys.EditorColumnBackground, GetColor(root, "editor_column", "background"));
184 Set(res, Keys.CurrentFileForeground, GetColor(root, "editor_column", "current_file_foreground"));
185 Set(res, Keys.MarkdownPreviewPanelBackground, GetColor(root, "markdown_preview_panel", "background"));
186 Set(res, Keys.MarkdownPreviewPanelBorderBrush, GetColor(root, "markdown_preview_panel", "border_brush"));
187 Set(res, Keys.SolutionExplorerBorderBrush, GetColor(root, "solution_explorer", "border_brush"));
188 Set(res, Keys.SolutionExplorerHeaderForeground, GetColor(root, "solution_explorer", "header_foreground"));
189 Set(res, Keys.PanelChromeTitleForeground, GetColor(root, "panel_chrome", "title_foreground"));
190 Set(res, Keys.PanelTitleAccentBrush, GetColor(root, "panel_chrome", "accent_brush"));
191 Set(res, Keys.PanelChromeHeaderBackground, GetColor(root, "panel_chrome", "header_background"));
192 Set(res, Keys.PanelChromeHeaderSeparatorBrush, GetColor(root, "panel_chrome", "header_separator"));
193 Set(res, Keys.PanelChromeMenuGlyphForeground, GetColor(root, "panel_chrome", "menu_glyph_foreground"));
194 Set(res, Keys.BuildOutputBackground, GetColor(root, "build_output", "background"));
195 Set(res, Keys.BuildOutputBorderBrush, GetColor(root, "build_output", "border_brush"));
196 Set(res, Keys.ChatPanelBackground, GetColor(root, "chat_panel", "background"));
197 Set(res, Keys.ChatLabelForeground, GetColor(root, "chat_panel", "label_foreground"));
198 Set(res, Keys.ChatMessageBubbleBackground, GetColor(root, "chat_panel", "message_bubble_background"));
199 Set(res, Keys.ChatMessageContentForeground, GetColor(root, "chat_panel", "message_content_foreground"));
200 Set(res, Keys.PanelBackgroundBrush, GetIdeChromeColor(root, "panel_background")
201 ?? GetColor(root, "chat_panel", "message_bubble_background"));
202 Set(res, Keys.InsetSurfaceBackground, GetIdeChromeColor(root, "inset_surface_background"));
203 Set(res, Keys.InsetSurfaceBorderBrush, GetIdeChromeColor(root, "inset_surface_border_brush"));
204 Set(res, Keys.StatusChipBackground, GetIdeChromeColor(root, "status_chip_background"));
205 Set(res, Keys.StatusChipBorderBrush, GetIdeChromeColor(root, "status_chip_border_brush"));
206 Set(res, Keys.StatusChipForeground, GetIdeChromeColor(root, "status_chip_foreground"));
207 Set(res, Keys.SendButtonBackground, GetColor(root, "chat_panel", "send_button_background"));
208 Set(res, Keys.SendButtonForeground, GetColor(root, "chat_panel", "send_button_foreground"));
209 Set(res, Keys.TerminalBackground, GetColor(root, "terminal", "background"));
210 Set(res, Keys.TerminalForeground, GetColor(root, "terminal", "foreground"));
211 Set(res, Keys.TerminalInputBackground, GetColor(root, "terminal", "input_background"));
212 Set(res, Keys.McpBannerBackground, GetColor(root, "mcp_banner", "background"));
213 Set(res, Keys.McpBannerForeground, GetColor(root, "mcp_banner", "foreground"));
214 Set(res, Keys.PreviewWindowBackground, GetColor(root, "preview_window", "background"));
215 if (root.TryGetProperty("power_cockpit", out var pc))
216 {
217 Set(res, Keys.PowerNeonBorder, GetColorFrom(pc, "neon_border"));
218 Set(res, Keys.PowerNeonAccent, GetColorFrom(pc, "neon_accent"));
219 Set(res, Keys.PowerCockpitPanelBackground, GetColorFrom(pc, "panel_background"));
220 Set(res, Keys.PowerSafetyDockBackground, GetColorFrom(pc, "safety_dock_background"));
221 Set(res, Keys.PowerWorkspaceHealthStripBackground, GetColorFrom(pc, "workspace_health_strip_background"));
222 Set(res, Keys.PowerSafetyObserve, GetColorFrom(pc, "safety_observe"));
223 Set(res, Keys.PowerSafetyConfirm, GetColorFrom(pc, "safety_confirm"));
224 Set(res, Keys.PowerSafetyAutonomous, GetColorFrom(pc, "safety_autonomous"));
225 Set(res, Keys.PowerEmergency, GetColorFrom(pc, "emergency"));
226 }
227
228 ThemeApplied?.Invoke(null, EventArgs.Empty);
229 return "OK";
230 }
231 }
232
233 private static string? GetColorFrom(JsonElement parent, string prop)
234 {
235 return parent.TryGetProperty(prop, out var p) ? p.GetString() : null;
236 }
237
238 /// <summary>Запустить применение темы в UI-потоке и вернуть результат (для вызова из MCP).</summary>
239 public static async Task<string> ApplyOnUiThreadAsync(string themeJson)
240 {
241 var json = themeJson ?? "";
242 return await UiScheduler.Default.InvokeAsync(() => Apply(json));
243 }
244
245 private static Avalonia.Controls.IResourceDictionary? GetResourceDictionary()
246 {
247 return Avalonia.Application.Current?.Resources;
248 }
249
250 /// <summary>Рамки рабочей области: workspace_layout.border_brush или, если нет, editor_column.border_brush.</summary>
251 private static string? GetWorkspacePanelBorderBrush(JsonElement root)
252 {
253 var w = GetColor(root, "workspace_layout", "border_brush");
254 return !string.IsNullOrEmpty(w) ? w : GetColor(root, "editor_column", "border_brush");
255 }
256
257 private static string? GetColor(JsonElement root, string obj, string prop)
258 {
259 if (root.TryGetProperty(obj, out var o) && o.TryGetProperty(prop, out var p))
260 return p.GetString();
261 return null;
262 }
263
264 private static string? GetIdeChromeColor(JsonElement root, string prop) =>
265 GetColor(root, "ide_chrome", prop);
266
267 private static void Set(Avalonia.Controls.IResourceDictionary res, string key, string? hex)
268 {
269 if (string.IsNullOrEmpty(hex))
270 return;
271 try
272 {
273 var brush = SolidColorBrush.Parse(hex);
274 res[key] = brush;
275 }
276 catch
277 {
278 // ignore invalid color
279 }
280 }
281}
282
View only · write via MCP/CIDE