Skip to repository content448 lines · 16.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:32:16.528Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
theme_settings.rs
1#![deny(missing_docs)]
2
3//! # Theme Settings
4//!
5//! This crate provides theme settings integration for Zed,
6//! bridging the theme system with the settings infrastructure.
7
8mod schema;
9mod settings;
10
11use std::sync::Arc;
12
13use ::settings::{IntoGpui, Settings, SettingsStore};
14use anyhow::{Context as _, Result};
15use gpui::{App, Font, HighlightStyle, Pixels, Refineable, px};
16use gpui_util::ResultExt;
17use theme::{
18 AccentColors, Appearance, AppearanceContent, DEFAULT_DARK_THEME, DEFAULT_ICON_THEME_NAME,
19 GlobalTheme, LoadThemes, PlayerColor, PlayerColors, StatusColors, SyntaxTheme,
20 SystemAppearance, SystemColors, Theme, ThemeColors, ThemeFamily, ThemeRegistry,
21 ThemeSettingsProvider, ThemeStyles, default_color_scales, try_parse_color,
22};
23
24pub use crate::schema::{
25 FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent,
26 ThemeColorsContent, ThemeContent, ThemeFamilyContent, ThemeStyleContent,
27 WindowBackgroundContent, status_colors_refinement, syntax_overrides, theme_colors_refinement,
28};
29use crate::settings::adjust_buffer_font_size;
30pub use crate::settings::{
31 AgentBufferFontSize, AgentUiFontSize, BufferLineHeight, FontFamilyName,
32 GitCommitBufferFontSize, IconThemeName, IconThemeSelection, MarkdownPreviewFontSize,
33 ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, adjust_agent_buffer_font_size,
34 adjust_agent_ui_font_size, adjust_git_commit_buffer_font_size,
35 adjust_markdown_preview_font_size, adjust_ui_font_size, adjusted_font_size, appearance_to_mode,
36 clamp_font_size, default_theme, observe_buffer_font_size_adjustment,
37 reset_agent_buffer_font_size, reset_agent_ui_font_size, reset_buffer_font_size,
38 reset_git_commit_buffer_font_size, reset_markdown_preview_font_size, reset_ui_font_size,
39 set_icon_theme, set_mode, set_theme, setup_ui_font,
40};
41pub use theme::UiDensity;
42
43struct ThemeSettingsProviderImpl;
44
45impl ThemeSettingsProvider for ThemeSettingsProviderImpl {
46 fn ui_font<'a>(&'a self, cx: &'a App) -> &'a Font {
47 &ThemeSettings::get_global(cx).ui_font
48 }
49
50 fn buffer_font<'a>(&'a self, cx: &'a App) -> &'a Font {
51 &ThemeSettings::get_global(cx).buffer_font
52 }
53
54 fn ui_font_size(&self, cx: &App) -> Pixels {
55 ThemeSettings::get_global(cx).ui_font_size(cx)
56 }
57
58 fn buffer_font_size(&self, cx: &App) -> Pixels {
59 ThemeSettings::get_global(cx).buffer_font_size(cx)
60 }
61
62 fn ui_density(&self, cx: &App) -> UiDensity {
63 ThemeSettings::get_global(cx).ui_density
64 }
65}
66
67/// Initialize the theme system with settings integration.
68///
69/// This is the full initialization for the application. It calls [`theme::init`]
70/// and then wires up settings observation for theme/font changes.
71pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
72 let load_user_themes = matches!(&themes_to_load, LoadThemes::All(_));
73
74 theme::init(themes_to_load, cx);
75 theme::set_theme_settings_provider(Box::new(ThemeSettingsProviderImpl), cx);
76
77 if load_user_themes {
78 let registry = ThemeRegistry::global(cx);
79 load_bundled_themes(®istry);
80 }
81
82 let theme = configured_theme(cx);
83 let icon_theme = configured_icon_theme(cx);
84 GlobalTheme::update_theme(cx, theme);
85 GlobalTheme::update_icon_theme(cx, icon_theme);
86
87 let settings = ThemeSettings::get_global(cx);
88
89 let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings();
90 let mut prev_ui_font_size_settings = settings.ui_font_size_settings();
91 let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
92 let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
93 let mut prev_git_commit_buffer_font_size_settings =
94 settings.git_commit_buffer_font_size_settings();
95 let mut prev_markdown_preview_font_size_settings =
96 settings.markdown_preview_font_size_settings();
97 let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0);
98 let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
99 let mut prev_theme_overrides = (
100 settings.experimental_theme_overrides.clone(),
101 settings.theme_overrides.clone(),
102 );
103
104 cx.observe_global::<SettingsStore>(move |cx| {
105 let settings = ThemeSettings::get_global(cx);
106
107 let buffer_font_size_settings = settings.buffer_font_size_settings();
108 let ui_font_size_settings = settings.ui_font_size_settings();
109 let agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
110 let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
111 let git_commit_buffer_font_size_settings = settings.git_commit_buffer_font_size_settings();
112 let markdown_preview_font_size_settings = settings.markdown_preview_font_size_settings();
113 let theme_name = settings.theme.name(SystemAppearance::global(cx).0);
114 let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
115 let theme_overrides = (
116 settings.experimental_theme_overrides.clone(),
117 settings.theme_overrides.clone(),
118 );
119
120 if buffer_font_size_settings != prev_buffer_font_size_settings {
121 prev_buffer_font_size_settings = buffer_font_size_settings;
122 reset_buffer_font_size(cx);
123 }
124
125 if ui_font_size_settings != prev_ui_font_size_settings {
126 prev_ui_font_size_settings = ui_font_size_settings;
127 reset_ui_font_size(cx);
128 }
129
130 if agent_ui_font_size_settings != prev_agent_ui_font_size_settings {
131 prev_agent_ui_font_size_settings = agent_ui_font_size_settings;
132 reset_agent_ui_font_size(cx);
133 }
134
135 if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings {
136 prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings;
137 reset_agent_buffer_font_size(cx);
138 }
139
140 if git_commit_buffer_font_size_settings != prev_git_commit_buffer_font_size_settings {
141 prev_git_commit_buffer_font_size_settings = git_commit_buffer_font_size_settings;
142 reset_git_commit_buffer_font_size(cx);
143 }
144
145 if markdown_preview_font_size_settings != prev_markdown_preview_font_size_settings {
146 prev_markdown_preview_font_size_settings = markdown_preview_font_size_settings;
147 reset_markdown_preview_font_size(cx);
148 }
149
150 if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides {
151 prev_theme_name = theme_name;
152 prev_theme_overrides = theme_overrides;
153 reload_theme(cx);
154 }
155
156 if icon_theme_name != prev_icon_theme_name {
157 prev_icon_theme_name = icon_theme_name;
158 reload_icon_theme(cx);
159 }
160 })
161 .detach();
162}
163
164fn configured_theme(cx: &mut App) -> Arc<Theme> {
165 let themes = ThemeRegistry::default_global(cx);
166 let theme_settings = ThemeSettings::get_global(cx);
167 let system_appearance = SystemAppearance::global(cx);
168
169 let theme_name = theme_settings.theme.name(*system_appearance);
170
171 let theme = match themes.get(&theme_name.0) {
172 Ok(theme) => theme,
173 Err(err) => {
174 if themes.extensions_loaded() {
175 log::error!("{err}");
176 }
177 themes
178 .get(default_theme(*system_appearance))
179 .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap())
180 }
181 };
182 theme_settings.apply_theme_overrides(theme)
183}
184
185fn configured_icon_theme(cx: &mut App) -> Arc<theme::IconTheme> {
186 let themes = ThemeRegistry::default_global(cx);
187 let theme_settings = ThemeSettings::get_global(cx);
188 let system_appearance = SystemAppearance::global(cx);
189
190 let icon_theme_name = theme_settings.icon_theme.name(*system_appearance);
191
192 match themes.get_icon_theme(&icon_theme_name.0) {
193 Ok(theme) => theme,
194 Err(err) => {
195 if themes.extensions_loaded() {
196 log::error!("{err}");
197 }
198 themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()
199 }
200 }
201}
202
203/// Reloads the current theme from settings.
204pub fn reload_theme(cx: &mut App) {
205 let theme = configured_theme(cx);
206 GlobalTheme::update_theme(cx, theme);
207 cx.refresh_windows();
208}
209
210/// Reloads the current icon theme from settings.
211pub fn reload_icon_theme(cx: &mut App) {
212 let icon_theme = configured_icon_theme(cx);
213 GlobalTheme::update_icon_theme(cx, icon_theme);
214 cx.refresh_windows();
215}
216
217/// Loads the themes bundled with the Zed binary into the registry.
218pub fn load_bundled_themes(registry: &ThemeRegistry) {
219 let theme_paths = registry
220 .assets()
221 .list("themes/")
222 .expect("failed to list theme assets")
223 .into_iter()
224 .filter(|path| path.ends_with(".json"));
225
226 for path in theme_paths {
227 let Some(theme) = registry.assets().load(&path).log_err().flatten() else {
228 continue;
229 };
230
231 let Some(theme_family) = serde_json::from_slice(&theme)
232 .with_context(|| format!("failed to parse theme at path \"{path}\""))
233 .log_err()
234 else {
235 continue;
236 };
237
238 let refined = refine_theme_family(theme_family);
239 registry.insert_theme_families([refined]);
240 }
241}
242
243/// Loads a user theme from the given bytes into the registry.
244pub fn load_user_theme(registry: &ThemeRegistry, bytes: &[u8]) -> Result<()> {
245 let theme = deserialize_user_theme(bytes)?;
246 let refined = refine_theme_family(theme);
247 registry.insert_theme_families([refined]);
248 Ok(())
249}
250
251/// Deserializes a user theme from the given bytes.
252pub fn deserialize_user_theme(bytes: &[u8]) -> Result<ThemeFamilyContent> {
253 let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
254
255 for theme in &theme_family.themes {
256 if theme
257 .style
258 .colors
259 .deprecated_scrollbar_thumb_background
260 .is_some()
261 {
262 log::warn!(
263 r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
264 theme_name = theme.name
265 )
266 }
267 }
268
269 Ok(theme_family)
270}
271
272/// Refines a [`ThemeFamilyContent`] and its [`ThemeContent`]s into a [`ThemeFamily`].
273pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily {
274 let id = uuid::Uuid::new_v4().to_string();
275 let name = theme_family_content.name.clone();
276 let author = theme_family_content.author.clone();
277
278 let themes: Vec<Theme> = theme_family_content
279 .themes
280 .iter()
281 .map(|theme_content| refine_theme(theme_content))
282 .collect();
283
284 ThemeFamily {
285 id,
286 name: name.into(),
287 author: author.into(),
288 themes,
289 scales: default_color_scales(),
290 }
291}
292
293/// Refines a [`ThemeContent`] into a [`Theme`].
294pub fn refine_theme(theme: &ThemeContent) -> Theme {
295 let appearance = match theme.appearance {
296 AppearanceContent::Light => Appearance::Light,
297 AppearanceContent::Dark => Appearance::Dark,
298 };
299
300 let mut refined_status_colors = match theme.appearance {
301 AppearanceContent::Light => StatusColors::light(),
302 AppearanceContent::Dark => StatusColors::dark(),
303 };
304 let mut status_colors_refinement = status_colors_refinement(&theme.style.status);
305 theme::apply_status_color_defaults(&mut status_colors_refinement);
306 refined_status_colors.refine(&status_colors_refinement);
307
308 let mut refined_player_colors = match theme.appearance {
309 AppearanceContent::Light => PlayerColors::light(),
310 AppearanceContent::Dark => PlayerColors::dark(),
311 };
312 merge_player_colors(&mut refined_player_colors, &theme.style.players);
313
314 let mut refined_theme_colors = match theme.appearance {
315 AppearanceContent::Light => ThemeColors::light(),
316 AppearanceContent::Dark => ThemeColors::dark(),
317 };
318 let mut theme_colors_refinement = theme_colors_refinement(
319 &theme.style.colors,
320 &status_colors_refinement,
321 theme.appearance == AppearanceContent::Light,
322 );
323 theme::apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
324 refined_theme_colors.refine(&theme_colors_refinement);
325
326 let mut refined_accent_colors = match theme.appearance {
327 AppearanceContent::Light => AccentColors::light(),
328 AppearanceContent::Dark => AccentColors::dark(),
329 };
330 merge_accent_colors(&mut refined_accent_colors, &theme.style.accents);
331
332 let syntax_highlights = theme.style.syntax.iter().map(|(syntax_token, highlight)| {
333 (
334 syntax_token.clone(),
335 HighlightStyle {
336 color: highlight
337 .color
338 .as_ref()
339 .and_then(|color| try_parse_color(color).ok()),
340 background_color: highlight
341 .background_color
342 .as_ref()
343 .and_then(|color| try_parse_color(color).ok()),
344 font_style: highlight.font_style.map(|s| s.into_gpui()),
345 font_weight: highlight.font_weight.map(|w| w.into_gpui()),
346 ..Default::default()
347 },
348 )
349 });
350 let syntax_theme = Arc::new(SyntaxTheme::new(syntax_highlights));
351
352 let window_background_appearance = theme
353 .style
354 .window_background_appearance
355 .map(|w| w.into_gpui())
356 .unwrap_or_default();
357
358 Theme {
359 id: uuid::Uuid::new_v4().to_string(),
360 name: theme.name.clone().into(),
361 appearance,
362 styles: ThemeStyles {
363 system: SystemColors::default(),
364 window_background_appearance,
365 accents: refined_accent_colors,
366 colors: refined_theme_colors,
367 status: refined_status_colors,
368 player: refined_player_colors,
369 syntax: syntax_theme,
370 },
371 }
372}
373
374/// Merges player color overrides into the given [`PlayerColors`].
375pub fn merge_player_colors(
376 player_colors: &mut PlayerColors,
377 user_player_colors: &[::settings::PlayerColorContent],
378) {
379 if user_player_colors.is_empty() {
380 return;
381 }
382
383 for (idx, player) in user_player_colors.iter().enumerate() {
384 let cursor = player
385 .cursor
386 .as_ref()
387 .and_then(|color| try_parse_color(color).ok());
388 let background = player
389 .background
390 .as_ref()
391 .and_then(|color| try_parse_color(color).ok());
392 let selection = player
393 .selection
394 .as_ref()
395 .and_then(|color| try_parse_color(color).ok());
396
397 if let Some(player_color) = player_colors.0.get_mut(idx) {
398 *player_color = PlayerColor {
399 cursor: cursor.unwrap_or(player_color.cursor),
400 background: background.unwrap_or(player_color.background),
401 selection: selection.unwrap_or(player_color.selection),
402 };
403 } else {
404 player_colors.0.push(PlayerColor {
405 cursor: cursor.unwrap_or_default(),
406 background: background.unwrap_or_default(),
407 selection: selection.unwrap_or_default(),
408 });
409 }
410 }
411}
412
413/// Merges accent color overrides into the given [`AccentColors`].
414pub fn merge_accent_colors(
415 accent_colors: &mut AccentColors,
416 user_accent_colors: &[::settings::AccentContent],
417) {
418 if user_accent_colors.is_empty() {
419 return;
420 }
421
422 let colors = user_accent_colors
423 .iter()
424 .filter_map(|accent_color| {
425 accent_color
426 .0
427 .as_ref()
428 .and_then(|color| try_parse_color(color).ok())
429 })
430 .collect::<Vec<_>>();
431
432 if !colors.is_empty() {
433 accent_colors.0 = Arc::from(colors);
434 }
435}
436
437/// Increases the buffer font size by 1 pixel, without persisting the result in the settings.
438/// This will be effective until the app is restarted.
439pub fn increase_buffer_font_size(cx: &mut App) {
440 adjust_buffer_font_size(cx, |size| size + px(1.0));
441}
442
443/// Decreases the buffer font size by 1 pixel, without persisting the result in the settings.
444/// This will be effective until the app is restarted.
445pub fn decrease_buffer_font_size(cx: &mut App) {
446 adjust_buffer_font_size(cx, |size| size - px(1.0));
447}
448