scripts/SYSTEMSTYLE.md
The current implementation relies almost exclusively on CLI wrapping (std::process::Command) to query system state. While this avoids link-time dependencies on heavy UI toolkits (Cocoa, GTK, Windows SDK), it is performantly expensive (spawn costs), fragile (text parsing stdout), and often retrieves "saved configuration" rather than "resolved active styling."
A robust production-grade discovery system should use native IPC (Inter-Process Communication) or FFI (Foreign Function Interface) calls to OS APIs.
Current Status: Parses the Registry (HKCU\Software\Microsoft\...) via reg.exe.
Critique: Registry keys are internal implementation details. They do not account for high-contrast overrides, transient states, or correct alpha blending of accent colors.
Windows.UI.ViewManagement.UISettings (WinRT).
GetColorValue method.UIColorType (Background, Foreground, Accent, AccentDark1, AccentLight1, etc.).ColorValuesChanged) for real-time updates.GetSystemMetrics (User32).
SM_CXVSCROLL), border padding, and icon sizing.SystemParametersInfoW (User32).
SPI_GETNONCLIENTMETRICS returns a NONCLIENTMETRICSW struct containing LOGFONT data for MessageFonts, CaptionFonts, and MenuFonts (the actual fonts the OS uses, rather than hardcoded "Segoe UI").SystemParametersInfoW with SPI_GETHIGHCONTRAST.Current Status: Parses defaults read -g and sw_vers.
Critique: defaults reads the plist on disk. It does not resolve semantic colors. For example, NSColor.windowBackgroundColor is not a single hex code; it is a dynamic proxy that resolves differently based on the active appearance (Dark/Light/High Contrast) and vibrancy settings.
NSAppearance (AppKit).
[NSApp effectiveAppearance] to determine if the resolved appearance is Aqua (Light) or Dark Aqua.NSColor (AppKit).
[NSColor labelColor], [NSColor controlAccentColor], [NSColor windowBackgroundColor].CGColor extraction to get actual RGB values.NSFont (AppKit).
[NSFont systemFontOfSize: 0.0] returns the user's preferred UI font (usually SF Pro).[NSFont monospacedSystemFontOfSize: ...] returns SF Mono.NSScroller
[NSScroller scrollerWidthForControlSize: ...] is the canonical way to get the scrollbar width, which changes based on input device settings (mouse vs. trackpad).Current Status: Wraps gsettings (GNOME) and kreadconfig5 (KDE).
Critique: While gsettings is stable, spawning processes is slow. The implementation also manually parses specific config files for Hyprland/Pywal, which is brittle.
org.freedesktop.portal.Settings.Read("org.freedesktop.appearance", "color-scheme") returns 0 (No Preference), 1 (Dark), or 2 (Light). This works on GNOME 42+, KDE Plasma 6, and Sway/Hyprland (via xdg-desktop-portal-wlr or gtk).GtkSettings (via FFI/GObject Introspection).
gtk-theme-name and gtk-font-name directly via the C API is faster than shelling out.KConfig (C++) is hard to bind to Rust.
~/.config/kdeglobals is acceptable here if avoiding C++, but utilizing the XDG Portal is preferred for the theme switch.Current Status: Limited registry checks and gsettings keys.
IDWriteFactory::GetSystemFontCollection reflects user scaling preferences better than fixed point sizes.org.gnome.desktop.interface/text-scaling-factor (float).SystemParametersInfo(SPI_GETCLIENTAREAANIMATION).prefers-reduced-motion, but the discovery method needs to be via system APIs (like UIAccessibilityIsReduceMotionEnabled on macOS) to be compliant with store guidelines.Instead of std::process::Command, the module should be refactored to use standard Rust FFI crates that wrap these APIs without requiring a full GUI toolkit dependency:
windows or windows-sys crate. These are zero-cost bindings to the Win32 and WinRT APIs mentioned above.objc2 and objc2-app-kit crates. This allows sending messages to NSColor and NSFont dynamically at runtime without linking the entire Cocoa framework statically if strict separation is needed.zbus crate (pure Rust DBus implementation) to query org.freedesktop.portal.Settings. This removes the dependency on gsettings or KDE binaries existing in the PATH.This approach transforms the system from "best-guess parsing" to "native OS integration," ensuring that when a user changes their accent color or switches to Dark Mode, the framework receives the exact values the OS intends.
Based on the code provided, your discovery system focuses primarily on static colors and basic font names. However, a native-feeling UI framework requires dynamic behavioral metrics and rendering hints that are currently missing.
Here is a report on the missing customizations and the specific APIs required to retrieve them.
The current implementation treats backgrounds as solid RGB colors. Modern OS designs (Windows 11, macOS, recent GNOME) rely on material effects that blend the window with the desktop wallpaper or windows behind it.
Missing Data:
How to retrieve it:
windows crate to call DwmSetWindowAttribute with DWMWA_SYSTEMBACKDROP_TYPE.objc2. You need to check [NSVisualEffectView material] types to emulate them, or more accurately, simply flag your window to use these native backing layers.decoration:blur and decoration:opacity.Your code contains DoubleClick logic (implied by the framework context), but it likely uses hardcoded timing (e.g., 500ms). OS users customize this, and ignoring it makes the app feel "laggy" or "jittery."
Missing Data:
How to retrieve it:
GetDoubleClickTime(), GetSystemMetrics(SM_CXDOUBLECLK), GetCaretBlinkTime().NSEvent.doubleClickInterval.gsettings get org.gnome.settings-daemon.peripherals.mouse double-click.XGetDefault settings.The current code has a ScrollbarInfo struct, but it misses the visibility trigger. macOS users often set scrollbars to "Show automatically based on mouse or trackpad."
Missing Data:
Automatic (only when scrolling), WhenScrolling, or Always.How to retrieve it:
[NSScroller preferredScrollerStyle] returns NSScrollerStyleOverlay (iPhone-like) or NSScrollerStyleLegacy (always visible).SystemParametersInfo with SPI_GETWHEELSCROLLLINES to determine how many lines to scroll per notch (defaults to 3, but often customized).Your code detects text_scale_factor, but misses the deeper rendering hints required for crisp text that matches the OS.
Missing Data:
How to retrieve it:
SystemParametersInfo(SPI_GETFONTSMOOTHING...). To get the actual ClearType parameters, you access the registry at HKCU\Control Panel\Desktop\FontSmoothingGamma, etc.[[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast].Your SystemColors struct includes accent and selection, but often the Focus Ring is distinct.
Missing Data:
How to retrieve it:
[NSColor keyboardFocusIndicatorColor].SystemParametersInfo(SPI_GETFOCUSBORDERHEIGHT).The code checks for Hyprland borders but misses the two most common customizations in the Linux community: Icons and Cursors.
Missing Data:
How to retrieve it:
org.freedesktop.appearance via DBus.org.gnome.desktop.interface icon-theme and cursor-theme.XCURSOR_THEME and XCURSOR_SIZE are standard across almost all WMs (Hyprland, Sway, i3).To get these without slowing down startup (shelling out is slow), I recommend moving SystemStyle::detect() to a threaded initialization or using FFI crates:
For Windows: Use the windows crate.
unsafe {
let mut time = 0;
// Near-instant retrieval via User32
windows::Win32::UI::Input::KeyboardAndMouse::GetCaretBlinkTime();
}
For macOS: Use objc2 and objc2-app-kit.
use objc2_app_kit::NSScroller;
// Check if scrollbars should overlay or take up space
let style = unsafe { NSScroller::preferredScrollerStyle() };
For Linux: Use zbus (pure Rust DBus) to hit the XDG Portal. This is the only way to get "System Theme" reliably across KDE, GNOME, and Hyprland without specific hacks for each.
To implement a "soft fallback" discovery system—where you try to load OS libraries at runtime and fall back to hardcoded defaults if they fail—you need to use the libloading crate (or raw dlopen/LoadLibrary calls).
Here are the specific dynamic libraries and the symbols (functions) you need to load to get the missing customizations mentioned in the previous report.
On Windows, system DLLs are guaranteed to exist, but specific functions (like Dark Mode detection in Dwmapi) might be missing on older versions (Windows 7).
Library: User32.dll (Core UI Metrics)
GetSystemMetrics: Retrieves scrollbar width (SM_CXVSCROLL), double-click dimensions (SM_CXDOUBLECLK), and high-contrast status.SystemParametersInfoW: Retrieves "SPI" values (Caret blink time, Font smoothing, Animation effects).GetSysColor: Retrieves standard RGB colors (ButtonFace, WindowText).Library: Dwmapi.dll (Desktop Window Manager)
DwmGetColorizationColor: Gets the user's accent color and transparency blend.DwmGetWindowAttribute: Used with DWMWA_USE_IMMERSIVE_DARK_MODE (20) to check if the app should draw a dark titlebar.Library: UxTheme.dll (Visual Styles)
IsThemeActive: Checks if the user is using a High Contrast theme (returns false) or a visual style.macOS does not use "DLLs" in the Windows sense. You interact with Frameworks. You cannot easily dlopen just the style logic; you must load the Objective-C runtime and message the AppKit framework.
Library: /usr/lib/libobjc.A.dylib (The Runtime)
objc_getClass: To get class references (e.g., NSColor, NSFont, NSScroller).sel_registerName: To register selectors (method names like systemFontOfSize:, currentControlTint).objc_msgSend: To actually call the methods.Library: /System/Library/Frameworks/AppKit.framework/AppKit
dlopen this path to ensure the framework is loaded into memory. Once loaded, you use the libobjc functions above to query classes like NSColor.NSColor: Ask for controlAccentColor (returns a dynamic color object).NSScroller: Ask for preferredScrollerStyle (Overlay vs Legacy).NSWorkspace: Ask accessibilityDisplayShouldIncreaseContrast.Avoid loading GTK. Loading libgtk-3.so or libgtk-4.so purely for settings is dangerous—it may try to open a display connection to X11/Wayland, which can crash your app if you are initializing your own windowing system (like Winit) simultaneously.
Instead, load GLib/GIO. This allows you to query GSettings (the registry of GNOME/Unity/Budgie) without initializing a GUI.
Library: libgio-2.0.so.0 (or libgio-2.0.so)
org.gnome.desktop.interface without spawning the gsettings CLI process (which is slow).g_settings_new: Open a schema (e.g., "org.gnome.desktop.interface").g_settings_get_value: Read a variant (color, font string).g_settings_get_string: Helper to get strings directly.g_settings_get_int: Helper for metrics (cursor size).Library: libgobject-2.0.so.0
g_object_unref: To clean up the settings objects (memory management).libloading| Platform | Filename to Load | Critical Symbols (dlsym) | Usage |
|---|---|---|---|
| Windows | User32.dll | GetSystemMetrics, SystemParametersInfoW | Metrics (Scrollbar size, double click) |
| Windows | Dwmapi.dll | DwmGetColorizationColor | Accent color & glass effects |
| macOS | libobjc.A.dylib | objc_msgSend, sel_registerName | Calling AppKit methods dynamically |
| macOS | AppKit.framework | (None - just dlopen to load classes) | Ensures NSColor class exists |
| Linux | libgio-2.0.so.0 | g_settings_new, g_settings_get_value | Reading GNOME/GTK config efficiently |
On Linux, even dynamic loading of libgio can be brittle due to ABI versioning.
Better Alternative: Use the crate zbus.
It is a pure Rust implementation of DBus. It requires no dynamic linking to C libraries. You can talk directly to the freedesktop portals or org.gtk.Settings over the DBus socket. This is the most robust, crash-proof method for Linux system discovery.
This is a great initiative. To build a true "Superset" SystemStyle that captures the distinct "feel" of Qt/KDE, GNOME, Windows, and macOS, you need to aggregate Behavioral, Visual, and Input preferences.
Here is the breakdown of the fields you should add to your SystemStyle struct and exactly how to dynamically load them on each OS without linking.
First, expand your SystemStyle to include these categories:
pub struct SystemStyle {
// ... existing fields (colors, fonts) ...
pub visual_hints: VisualHints,
pub input_metrics: InputMetrics,
pub animation_metrics: AnimationMetrics,
pub audio_metrics: AudioMetrics,
}
pub struct VisualHints {
/// Show icons on push buttons? (Common in KDE, rare in Win/Mac)
pub show_button_images: bool,
/// Show icons in context menus? (GNOME defaults off, Win/Mac/KDE usually on)
pub show_menu_images: bool,
/// Toolbar style: Icons only, Text only, Text beside Icon, Text below Icon
pub toolbar_style: ToolbarStyle,
/// Should tooltips be shown?
pub show_tooltips: bool,
/// Flash the window taskbar entry on alert?
pub flash_on_alert: bool,
}
pub struct InputMetrics {
/// Max ms between clicks to count as double-click (e.g., 500ms)
pub double_click_time_ms: u32,
/// Max pixels cursor can move during double-click (e.g., 4px)
pub double_click_distance: u32,
/// Pixels mouse must move to start a drag operation
pub drag_threshold: u32,
/// Ms to wait before a hover event triggers (tooltips)
pub hover_time_ms: u32,
/// Text cursor blink interval (0 = no blink)
pub caret_blink_time_ms: u32,
/// Width of the text cursor
pub caret_width: u32,
}
pub struct AnimationMetrics {
/// Global enable/disable for UI animations
pub animations_enabled: bool,
/// Global animation speed factor (1.0 = normal, 0.5 = slow, 2.0 = fast)
/// Heavily used in KDE.
pub animation_duration_factor: f32,
/// Focus rectangle behavior (Always visible vs. Only on keyboard nav)
pub focus_indicator_behavior: FocusBehavior,
}
pub struct AudioMetrics {
/// Should the app make sounds on events? (Error ping, etc.)
pub event_sounds_enabled: bool,
/// Should the app make sounds on input? (Clicking, typing)
pub input_feedback_sounds_enabled: bool,
}
This is where the "Qt/KDE vs GNOME" fight happens. Windows/macOS are more opinionated.
GSettings (GNOME) or KConfig (KDE).org.gnome.desktop.interface keys:
gtk-enable-primary-paste (bool)menus-have-icons (bool) - Often deprecated in newer GNOME, but check legacy.buttons-have-icons (bool)toolbar-style (string: "both", "icons", "text", "both-horiz")~/.config/kdeglobals:
[KDE]: ShowIconsOnPushButtons[Toolbar style]: ToolButtonStyleSystemParametersInfo (User32).SPI_GETMENUDROPALIGNMENT (Left/Right alignment). Windows doesn't explicitly expose "show images on buttons" as a system-wide flag; you should default to false (standard Win32/UWP look) or true if imitating older styles.This is critical for the app not feeling "laggy."
User32.dllGetDoubleClickTime() (returns UINT ms).GetSystemMetrics(SM_CXDOUBLECLK) / SM_CYDOUBLECLK (rect size).GetSystemMetrics(SM_CXDRAG) (drag threshold).GetCaretBlinkTime() (ms).SystemParametersInfo(SPI_GETMOUSEHOVERTIME).AppKit (via objc2).NSEvent.doubleClickInterval (NSTimeInterval).NSValuedKey in NSGlobalDomain: NSTextInsertionPointBlinkPeriod.org.gnome.settings-daemon.peripherals.mouse double-click (int).org.gnome.desktop.interface cursor-blink-time (int).org.gnome.desktop.interface cursor-blink (bool).gtk-dnd-drag-threshold (int).User32.dllSystemParametersInfo(SPI_GETCLIENTAREAANIMATION): Global animation toggle.SystemParametersInfo(SPI_GETKEYBOARDCUES): Returns TRUE if focus rectangles should only show after a key press, FALSE if always visible. (Very important for Windows feel!)AppKit.NSWorkspace.accessibilityDisplayShouldReduceMotion (Logic inversion: if true, animations_enabled = false).org.gnome.desktop.interface enable-animations (bool).[KDE] AnimationDurationFactor in kdeglobals. (e.g., 0.5 makes animations 2x faster).User32.SystemParametersInfo(SPI_GETBEEP) for simple beeps. Complex sound schemes are in Registry AppEvents\Schemes.AppKit.NSSound.soundEffectAudioVolume > 0.org.gnome.desktop.sound event-sounds (bool).org.gnome.desktop.sound input-feedback-sounds (bool).Since you want to avoid linking, use libloading for Windows/macOS and zbus (pure Rust, no C-link) for Linux.
libloading wrapper)#[cfg(target_os = "windows")]
fn get_windows_metrics() -> InputMetrics {
unsafe {
// Load User32 dynamically
let user32 = libloading::Library::new("user32.dll").ok();
// Define function signatures
type GetDoubleClickTime = unsafe extern "system" fn() -> u32;
type GetSystemMetrics = unsafe extern "system" fn(i32) -> i32;
type GetCaretBlinkTime = unsafe extern "system" fn() -> u32;
let mut metrics = InputMetrics::default();
if let Some(lib) = user32 {
if let Ok(func) = lib.get::<GetDoubleClickTime>(b"GetDoubleClickTime") {
metrics.double_click_time_ms = func();
}
if let Ok(func) = lib.get::<GetSystemMetrics>(b"GetSystemMetrics") {
// SM_CXDRAG = 68
metrics.drag_threshold = func(68) as u32;
}
if let Ok(func) = lib.get::<GetCaretBlinkTime>(b"GetCaretBlinkTime") {
metrics.caret_blink_time_ms = func();
}
}
metrics
}
}
Don't dlopen GTK. It's unsafe.
zbus to check org.freedesktop.portal.Settings. This is the future-proof way.~/.config/gtk-3.0/settings.ini. It is a standard INI file.[Settings]
gtk-double-click-time=400
gtk-enable-animations=1
gtk-menu-images=0
Parsing this text file is faster and safer than loading libgtk.so.
objc2 / block crates)You don't need libloading for macOS frameworks in the same way; you need the Objective-C runtime.
#[cfg(target_os = "macos")]
fn get_macos_visuals() -> VisualHints {
use objc2::rc::Retained;
use objc2_app_kit::NSWorkspace; // Requires objc2-app-kit crate (safe bindings)
// Note: objc2 links to Foundation/AppKit, but these are system frameworks
// guaranteed to be there. It's effectively dynamic loading.
let workspace = unsafe { NSWorkspace::sharedWorkspace() };
let reduce_motion = unsafe { workspace.accessibilityDisplayShouldReduceMotion() };
VisualHints {
animations_enabled: !reduce_motion,
// macOS standard overrides:
show_button_images: false,
show_menu_images: true,
// ...
}
}
SPI_GETKEYBOARDCUES): This is the most often missed "native feel" feature. If you don't respect this, your app looks cluttered compared to native Windows apps which hide underlines/dotted boxes until Alt/Tab is pressed.gsettings. KDE users might not have the GNOME schema installed, causing your discovery to crash or return defaults. Always check XDG_CURRENT_DESKTOP and parse kdeglobals if it's KDE.