chore: audit components

This commit is contained in:
2026-11-10 15:06:16 +07:00
parent af2177c6c8
commit 03369f501e
92 changed files with 2744 additions and 1947 deletions
+15 -21
View File
@@ -77,7 +77,7 @@ impl KdeScheme {
//
// Older setups fall back to the scheme's background lightness.
variant: portal_variant
.or(self.background.map(variant_by_lightness))
.or(self.background.map(crate::ThemeVariant::by_lightness))
.unwrap_or(crate::ThemeVariant::Light),
accent: self.accent.or(portal_accent),
palette: self.palette,
@@ -99,14 +99,6 @@ impl KdeScheme {
}
}
fn variant_by_lightness(background: Hsla) -> crate::ThemeVariant {
if background.l >= 0.5 {
crate::ThemeVariant::Light
} else {
crate::ThemeVariant::Dark
}
}
/// The root font size of a `[General]` group's `font=` QFont string.
fn root_font_size(group: Option<&HashMap<&str, &str>>) -> Option<Pixels> {
let fields: Vec<&str> = group?.get("font")?.split(',').map(str::trim).collect();
@@ -158,7 +150,10 @@ fn color_list(value: &str) -> Option<Hsla> {
let green = components.next()?.parse::<f32>().ok()?;
let blue = components.next()?.parse::<f32>().ok()?;
let alpha = components.next().and_then(|a| a.parse::<f32>().ok());
if red < 0.0 || green < 0.0 || blue < 0.0 {
if !(0.0..=255.0).contains(&red)
|| !(0.0..=255.0).contains(&green)
|| !(0.0..=255.0).contains(&blue)
{
return None;
}
let scale = |component: f32| component / 255.0;
@@ -335,19 +330,18 @@ activeBackground=39,44,49
}
#[test]
fn the_scheme_states_breeze_metrics_and_the_default_font() {
fn the_default_font_applies_without_a_stated_font() {
let system = KdeScheme::parse(BREEZE_DARK).into_system(Default::default(), None, None);
assert!((system.root_font_size.expect("default font") - px(13.33)).abs() < px(0.01));
}
assert_eq!(system.control_height, Some(px(33.)));
assert_eq!(system.input_height, Some(px(29.)));
assert_eq!(system.menu_item_height, Some(px(29.)));
assert_eq!(system.control_radius, Some(px(5.)));
assert_eq!(system.overlay_radius, Some(px(5.)));
assert_eq!(system.switch_height, Some(px(18.)));
assert_eq!(system.switch_width, Some(px(36.)));
assert_eq!(system.switch_knob, Some(px(18.)));
assert_eq!(system.titlebar_height, Some(px(44.)));
assert!((system.root_font_size.unwrap() - px(13.33)).abs() < px(0.01));
#[test]
fn color_components_outside_0_to_255_are_rejected() {
let above = KdeScheme::parse("[Colors:Window]\nBackgroundNormal=300,20,20\n");
assert!(above.palette.background.is_none());
let below = KdeScheme::parse("[Colors:Window]\nBackgroundNormal=-5,20,20\n");
assert!(below.palette.background.is_none());
}
#[test]
+47 -35
View File
@@ -13,9 +13,30 @@ use super::kde;
use super::omarchy;
use super::portal;
use super::signal::ReapplySignal;
use crate::ThemeVariant;
use crate::system::{PartialPalette, SourceKind, SystemTheme};
/// libadwaita's control corner radius, in px at 100% scaling.
const CONTROL_RADIUS_PX: f32 = 9.0;
/// libadwaita's popover corner radius.
const OVERLAY_RADIUS_PX: f32 = 15.0;
/// The checkbox indicator's corner radius.
const SM_RADIUS_PX: f32 = 6.0;
/// libadwaita's standard control and text-field height.
const CONTROL_HEIGHT_PX: f32 = 34.0;
/// The GTK menu row height.
const MENU_ITEM_HEIGHT_PX: f32 = 32.0;
/// The GTK switch: a 46x26 track with a 20px knob.
const SWITCH_HEIGHT_PX: f32 = 26.0;
const SWITCH_WIDTH_PX: f32 = 46.0;
const SWITCH_KNOB_PX: f32 = 20.0;
/// GNOME's headerbar height, in px at 100% scaling.
const TITLEBAR_HEIGHT_PX: f32 = 47.0;
/// The default 11pt interface font, in px.
const ROOT_FONT_SIZE_PX: f32 = 14.67;
pub(crate) struct LinuxSource {
backend: Backend,
portal: Option<portal::io::PortalClient>,
@@ -154,20 +175,20 @@ impl LinuxSource {
variant,
accent,
palette: PartialPalette::default(),
control_radius: Some(gpui::px(9.)),
overlay_radius: Some(gpui::px(15.)),
sm_radius: Some(gpui::px(6.)),
control_height: Some(gpui::px(34.)),
input_height: Some(gpui::px(34.)),
menu_item_height: Some(gpui::px(32.)),
switch_height: Some(gpui::px(26.)),
switch_width: Some(gpui::px(46.)),
switch_knob: Some(gpui::px(20.)),
titlebar_height: Some(gpui::px(47.)),
control_radius: Some(gpui::px(CONTROL_RADIUS_PX)),
overlay_radius: Some(gpui::px(OVERLAY_RADIUS_PX)),
sm_radius: Some(gpui::px(SM_RADIUS_PX)),
control_height: Some(gpui::px(CONTROL_HEIGHT_PX)),
input_height: Some(gpui::px(CONTROL_HEIGHT_PX)),
menu_item_height: Some(gpui::px(MENU_ITEM_HEIGHT_PX)),
switch_height: Some(gpui::px(SWITCH_HEIGHT_PX)),
switch_width: Some(gpui::px(SWITCH_WIDTH_PX)),
switch_knob: Some(gpui::px(SWITCH_KNOB_PX)),
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
root_font_size: Some(
self.gsettings
.and_then(|gsettings| gsettings.root_font_size)
.unwrap_or(gpui::px(14.67)),
.unwrap_or(gpui::px(ROOT_FONT_SIZE_PX)),
),
accessibility,
window_background: None,
@@ -237,41 +258,44 @@ impl ThemeSource for LinuxSource {
fn install_watchers(&mut self, async_cx: gpui::AsyncApp) {
let signal = ReapplySignal::install(&async_cx);
let file_signal = signal.sender();
if let Some(portal) = &self.portal {
portal.spawn_watch(signal.sender());
}
// Stored before any fallible step: an early return below
// must not drop and cancel the reapply loop.
self._signal = Some(signal);
match self.backend {
Backend::Omarchy => {
let Some(current) = omarchy::io::current_dir() else {
return;
};
let reapply_signal = signal.sender();
let watcher = notify::recommended_watcher(
move |event: std::result::Result<notify::Event, notify::Error>| match event {
Ok(_) => ReapplySignal::poke(&reapply_signal),
Err(error) => eprintln!("theme: omarchy watch error: {error}"),
Ok(_) => ReapplySignal::poke(&file_signal),
Err(error) => log::warn!("theme: omarchy watch error: {error}"),
},
);
match watcher {
Ok(mut watcher) => {
if let Err(error) = watcher.watch(&current, RecursiveMode::NonRecursive) {
eprintln!("theme: omarchy watch failed: {error}");
log::warn!("theme: omarchy watch failed: {error}");
return;
}
self.file_watcher = Some(watcher);
}
Err(error) => eprintln!("theme: omarchy watcher unavailable: {error}"),
Err(error) => log::warn!("theme: omarchy watcher unavailable: {error}"),
}
}
Backend::Kde => {
let Some(config) = home_dir().map(|home| home.join(".config")) else {
return;
};
let reapply_signal = signal.sender();
let watcher = notify::recommended_watcher(
move |event: std::result::Result<notify::Event, notify::Error>| match event {
@@ -279,27 +303,25 @@ impl ThemeSource for LinuxSource {
if event.paths.iter().any(|path| {
path.file_name().is_some_and(|name| name == "kdeglobals")
}) {
ReapplySignal::poke(&reapply_signal);
ReapplySignal::poke(&file_signal);
}
}
Err(error) => eprintln!("theme: kde watch error: {error}"),
Err(error) => log::warn!("theme: kde watch error: {error}"),
},
);
match watcher {
Ok(mut watcher) => {
if let Err(error) = watcher.watch(&config, RecursiveMode::NonRecursive) {
eprintln!("theme: kde watch failed: {error}");
log::warn!("theme: kde watch failed: {error}");
return;
}
self.file_watcher = Some(watcher);
}
Err(error) => eprintln!("theme: kde watcher unavailable: {error}"),
Err(error) => log::warn!("theme: kde watcher unavailable: {error}"),
}
}
Backend::Portal | Backend::Fallback => {}
}
self._signal = Some(signal);
}
fn label(&self) -> &'static str {
@@ -354,7 +376,7 @@ mod tests {
}
#[test]
fn the_gnome_snapshot_states_the_libadwaita_metrics() {
fn the_gnome_snapshot_labels_gnome_and_defaults_the_root_font() {
let source = LinuxSource {
backend: Backend::Portal,
portal: None,
@@ -369,17 +391,7 @@ mod tests {
);
assert_eq!(system.source, SourceKind::Gnome);
assert_eq!(system.control_height, Some(gpui::px(34.)));
assert_eq!(system.input_height, Some(gpui::px(34.)));
assert_eq!(system.menu_item_height, Some(gpui::px(32.)));
assert_eq!(system.control_radius, Some(gpui::px(9.)));
assert_eq!(system.overlay_radius, Some(gpui::px(15.)));
assert_eq!(system.sm_radius, Some(gpui::px(6.)));
assert_eq!(system.switch_height, Some(gpui::px(26.)));
assert_eq!(system.switch_width, Some(gpui::px(46.)));
assert_eq!(system.switch_knob, Some(gpui::px(20.)));
assert_eq!(system.titlebar_height, Some(gpui::px(47.)));
// The default 11pt font when gsettings states none.
assert_eq!(system.root_font_size, Some(gpui::px(14.67)));
assert_eq!(system.root_font_size, Some(gpui::px(ROOT_FONT_SIZE_PX)));
}
}
+6 -4
View File
@@ -14,6 +14,8 @@ use objc2_foundation::{
};
use std::ptr::NonNull;
use super::narrow_component;
use super::ThemeSource;
use crate::ThemeVariant;
use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
@@ -46,10 +48,10 @@ impl MacosSource {
let converted = color.colorUsingColorSpace(&srgb)?;
Some(gpui::Hsla::from(gpui::Rgba {
r: converted.redComponent() as f32,
g: converted.greenComponent() as f32,
b: converted.blueComponent() as f32,
a: converted.alphaComponent() as f32,
r: narrow_component(converted.redComponent()),
g: narrow_component(converted.greenComponent()),
b: narrow_component(converted.blueComponent()),
a: narrow_component(converted.alphaComponent()),
}))
}
+2 -18
View File
@@ -27,7 +27,7 @@ impl OmarchyTheme {
.get("mode")
.and_then(toml::Value::as_str)
.and_then(mode_variant)
.unwrap_or_else(|| variant_by_lightness(background));
.unwrap_or_else(|| ThemeVariant::by_lightness(background));
let accent = hex_color(&table, "accent").or_else(|| hex_color(&table, "color4"));
let selection = hex_color(&table, "selection");
@@ -86,14 +86,6 @@ fn mode_variant(mode: &str) -> Option<ThemeVariant> {
}
}
fn variant_by_lightness(background: Hsla) -> ThemeVariant {
if background.l >= 0.5 {
ThemeVariant::Light
} else {
ThemeVariant::Dark
}
}
fn hex_color(table: &Table, key: &str) -> Option<Hsla> {
let value = table.get(key)?.as_str()?;
let digits = value.trim().strip_prefix('#')?;
@@ -224,18 +216,10 @@ color15 = \"#635d6f\"
}
#[test]
fn omarchy_shares_the_gnome_control_metrics() {
fn omarchy_states_no_titlebar_or_root_font() {
let theme = OmarchyTheme::parse(TOKYO_NIGHT).expect("parses");
let system = theme.into_system(Default::default());
assert_eq!(system.control_height, Some(gpui::px(34.)));
assert_eq!(system.input_height, Some(gpui::px(34.)));
assert_eq!(system.menu_item_height, Some(gpui::px(32.)));
assert_eq!(system.control_radius, Some(gpui::px(9.)));
assert_eq!(system.overlay_radius, Some(gpui::px(15.)));
assert_eq!(system.switch_height, Some(gpui::px(26.)));
assert_eq!(system.switch_width, Some(gpui::px(46.)));
assert_eq!(system.switch_knob, Some(gpui::px(20.)));
assert_eq!(system.titlebar_height, None);
assert_eq!(system.root_font_size, None);
}
+16 -9
View File
@@ -5,6 +5,8 @@
use gpui::Hsla;
use super::narrow_component;
use crate::{ThemeVariant, system::AccessibilityPreferences};
/// The raw portal values, already converted off the DBus wire types.
@@ -26,9 +28,9 @@ impl AppearanceValues {
color_scheme: self.color_scheme,
accent: self.accent_rgb.map(|(r, g, b)| {
Hsla::from(gpui::Rgba {
r: r as f32,
g: g as f32,
b: b as f32,
r: narrow_component(r),
g: narrow_component(g),
b: narrow_component(b),
a: 1.0,
})
}),
@@ -98,7 +100,9 @@ pub(crate) mod io {
/// The most recently read appearance.
pub(crate) fn appearance(&self) -> Appearance {
*self.appearance.lock().expect("portal cache poisoned")
// A watch-thread panic must not crash the UI thread.
let cache = self.appearance.lock();
*cache.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Forwards portal `SettingChanged` events to the reapply signal from a dedicated thread.
@@ -107,17 +111,17 @@ pub(crate) mod io {
std::thread::spawn(move || {
let Some(connection) = Connection::session().ok() else {
eprintln!("theme: portal watch skipped, no session bus");
log::warn!("theme: portal watch skipped, no session bus");
return;
};
let Some(proxy) = settings_proxy(connection) else {
eprintln!("theme: portal watch skipped, settings proxy unavailable");
log::warn!("theme: portal watch skipped, settings proxy unavailable");
return;
};
let Ok(signals) = proxy.receive_signal("SettingChanged") else {
eprintln!("theme: portal watch skipped, signal subscription failed");
log::warn!("theme: portal watch skipped, signal subscription failed");
return;
};
@@ -134,10 +138,13 @@ pub(crate) mod io {
match read_appearance(&proxy) {
Some(latest) => {
*appearance.lock().expect("portal cache poisoned") = latest;
let mut cache = appearance
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*cache = latest;
super::super::signal::ReapplySignal::poke(&reapply_signal);
}
None => eprintln!("theme: portal read failed on SettingChanged"),
None => log::warn!("theme: portal read failed on SettingChanged"),
}
}
});
+1 -1
View File
@@ -32,7 +32,7 @@ impl ReapplySignal {
/// Pokes the pipeline from a thread holding a [`Self::sender`] clone.
pub(crate) fn poke(sender: &async_channel::Sender<()>) {
if let Err(error) = sender.try_send(()) {
eprintln!("theme: reapply signal failed: {error}");
log::warn!("theme: reapply signal failed: {error}");
}
}
}
+10 -13
View File
@@ -74,16 +74,19 @@ pub(crate) struct WindowsSource {
}
pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
// SAFETY: WinRT activation requires COM on this thread; the STA matches
// the UI thread's message pump, and RPC_E_CHANGED_MODE means COM is
// already initialized in another apartment.
let initialized = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
if initialized.is_err() && initialized != RPC_E_CHANGED_MODE {
eprintln!("theme: COM initialization failed: {initialized:?}");
log::error!("theme: COM initialization failed: {initialized:?}");
}
match WindowsSource::new() {
Some(source) => Box::new(source),
None => {
eprintln!("theme: Windows system theme unavailable; using fallback");
log::warn!("theme: Windows system theme unavailable; using fallback");
Box::new(super::fallback::FallbackSource)
}
}
@@ -94,7 +97,7 @@ impl WindowsSource {
let ui_settings = match UISettings::new() {
Ok(ui_settings) => ui_settings,
Err(error) => {
eprintln!("theme: UISettings unavailable: {error}");
log::warn!("theme: UISettings unavailable: {error}");
return None;
}
};
@@ -102,7 +105,7 @@ impl WindowsSource {
let accessibility_settings = match AccessibilitySettings::new() {
Ok(accessibility_settings) => accessibility_settings,
Err(error) => {
eprintln!("theme: AccessibilitySettings unavailable: {error}");
log::warn!("theme: AccessibilitySettings unavailable: {error}");
return None;
}
};
@@ -157,13 +160,7 @@ impl ThemeSource for WindowsSource {
let foreground = self.color(UIColorType::Foreground);
let variant = background
.map(|background| {
if background.l >= 0.5 {
ThemeVariant::Light
} else {
ThemeVariant::Dark
}
})
.map(ThemeVariant::by_lightness)
.or(self.registry.apps_use_light_theme.map(|light| {
if light {
ThemeVariant::Light
@@ -212,7 +209,7 @@ impl ThemeSource for WindowsSource {
});
if let Err(error) = self.ui_settings.ColorValuesChanged(&color_handler) {
eprintln!("theme: color change watch failed: {error}");
log::warn!("theme: color change watch failed: {error}");
}
let contrast_signal = signal.sender();
@@ -226,7 +223,7 @@ impl ThemeSource for WindowsSource {
.accessibility_settings
.HighContrastChanged(&contrast_handler)
{
eprintln!("theme: high contrast watch failed: {error}");
log::warn!("theme: high contrast watch failed: {error}");
}
self._signal = Some(signal);