fix: keep the dark default on Linux when the portal states no theme preference

This commit is contained in:
tiennm99 committed 2026-10-21 15:12:40 +07:00
1 parent 1370c15048
commit e805f8de8f
5 files changed
+85 -5

No files matched your search

Generated
+1
View File
@@ -509,6 +509,7 @@ name = "claude-code-usage-bubble"
version = "0.5.0"
dependencies = [
"auto-launch",
"dbus",
"dirs 6.0.0",
"flate2",
"libc",
+2
View File
@@ -40,6 +40,8 @@ libc = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
x11rb = "0.14"
libloading = "0.9"
# Same major as the copy tao pulls; reads the portal colour scheme.
dbus = "0.9"
[target.'cfg(windows)'.dependencies]
# Keep the major in step with the `windows` crate tauri pulls
@@ -34,7 +34,7 @@ corners for the hit area (D4).
- `taskbar_rect()` uses `SHAppBarMessage(ABM_GETTASKBARPOS)`.
- **Linux and macOS** (D3):
- Move end is a 250 ms debounce on `Moved`, armed only by `bubble_drag_start`. On X11, when the debounce fires, `platform::linux::button1_down()` (x11rb `QueryPointer`) re-arms it while Button1 is still held.
- Theme comes from `window.theme()`, defaulting to dark, and is rechecked on `ThemeChanged`.
- Theme defaults to dark and is rechecked on bubble creation and `ThemeChanged`. macOS uses `window.theme()`. Linux reads the portal `org.freedesktop.appearance color-scheme` itself (1 dark, 2 light) and keeps the current theme on "no preference" or an unreachable portal, because tao maps both to light.
- There is no taskbar rect. On Wayland without Xwayland, skip snap, clamp and saving positions.
- **Move end** (Rust): disarm, run `core::geom::snap` with the peers' `outer_position`/`outer_size` in `LIVE_USAGE` order, apply `set_position(Physical)` only if the position changed, then persist `bubble_positions[slug]`.
- **Commands:**
@@ -105,13 +105,13 @@ corners for the hit area (D4).
- [x] `ui/bubble.html`, `ui/bubble.css` (palette per `[data-theme]`, dead corners), `ui/bubble.js` (layout, texts from the payload only, pulse, 60 s time progress, `visibilitychange`, pointer drag/click, `contextmenu`, Ctrl+wheel), `ui/tauri-bridge.js`
- [x] `src/bubble.rs` rewritten: builder options as listed, exact physical size and saved/default position clamped into the work area, refit on `ScaleFactorChanged`, `destroy()` only with recreate after `Destroyed`, show on `bubble_ready` or the 1.5 s fallback, `widget_visible` honoured at creation
- [x] `src/platform/windows.rs`: `apply_no_activate`, `show_no_activate`, `hide`, `install_bubble_hook` (`WM_EXITSIZEMOVE`, `WM_SETTINGCHANGE`, `WM_DISPLAYCHANGE`, plus `MA_NOACTIVATE`), `taskbar`
- [x] Linux/macOS move end: 250 ms debounce armed by `bubble_drag_start`, re-armed while X11 Button1 is held (`platform::linux::button1_down`); theme from `window.theme()` and `ThemeChanged`; native Wayland skips snap, clamp and saving
- [x] Linux/macOS move end: 250 ms debounce armed by `bubble_drag_start`, re-armed while X11 Button1 is held (`platform::linux::button1_down`); theme from `window.theme()` (macOS) or the portal color-scheme (Linux, only a stated preference applies) on creation and `ThemeChanged`; native Wayland skips snap, clamp and saving
- [x] Move end snaps with peers in `LIVE_USAGE` order, moves only on a change, saves the position
- [x] Commands `bubble_click` (logs until the panel exists), `bubble_drag_start`, `bubble_resize_step`, `bubble_ready`; resize around each centre without snap or save
- [x] Reset position moves the existing bubbles to their defaults
- [x] `src/core/visibility.rs` with tests for every row of the transition table; user show/hide goes through it
- [x] Deleted `src/os/mod.rs` and `src/os/string.rs` (`src/os/theme.rs` was already gone)
- [x] Host: `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test` (181 passed, 1 ignored), `npm run lint`, `npm test` (32 passed), `npm run tauri build -- --no-bundle`; CI green on `windows-latest`, `ubuntu-22.04` and `macos-latest`
- [x] Host: `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test` (183 passed, 1 ignored), `npm run lint`, `npm test` (32 passed), `npm run tauri build -- --no-bundle`; CI green on `windows-latest`, `ubuntu-22.04` and `macos-latest`
- [x] Headless Chromium render of the page with a stubbed `__TAURI__` at 100%, 150% and 200% (layout, both themes, dead corners) and synthetic pointer checks (click, drag past 3 px, no drag on a later hover, Ctrl+wheel, right-click on the pill only)
- [ ] Visual comparison against a v0.5.0 screenshot at 100% and 150% (none in the repo; Windows manual check)
- [ ] Linux desktop (X11) and Windows manual checks below
+20 -2
View File
@@ -348,7 +348,11 @@ pub fn create(app: &AppHandle, provider: ProviderId) -> tauri::Result<()> {
);
prepare_native(app, &window, provider);
#[cfg(not(windows))]
#[cfg(target_os = "linux")]
if let Some(is_dark) = platform::linux::portal_prefers_dark() {
runtime.set_dark(is_dark);
}
#[cfg(target_os = "macos")]
match window.theme() {
Ok(theme) => runtime.set_dark(theme != tauri::Theme::Light),
Err(err) => log::debug!("theme of {label} unavailable: {err}"),
@@ -513,7 +517,7 @@ pub fn on_window_event(window: &Window, event: &WindowEvent) {
WindowEvent::Moved(_) => drag::moved(label),
// Windows reads the theme from the registry on WM_SETTINGCHANGE;
// tao's theme follows the apps setting, not the system one.
#[cfg(not(windows))]
#[cfg(target_os = "macos")]
WindowEvent::ThemeChanged(theme) => {
let is_dark = *theme != tauri::Theme::Light;
let app = window.app_handle().clone();
@@ -523,6 +527,20 @@ pub fn on_window_event(window: &Window, event: &WindowEvent) {
}
});
}
// tao reports "no preference" and an unreachable portal as light, so
// the portal is read again and only a stated preference applies.
#[cfg(target_os = "linux")]
WindowEvent::ThemeChanged(_) => {
let app = window.app_handle().clone();
defer("bubble-theme", move || {
let Some(is_dark) = platform::linux::portal_prefers_dark() else {
return;
};
if let Some(runtime) = runtime_of(&app) {
runtime.set_dark(is_dark);
}
});
}
_ => {}
}
}
+59
View File
@@ -106,6 +106,54 @@ pub fn button1_down() -> bool {
}
}
/// How long the portal settings read may block.
const PORTAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
/// Maps the portal's `org.freedesktop.appearance color-scheme` value to a
/// dark flag: 1 prefers dark, 2 prefers light. "No preference" (0), any
/// other value and an unreachable portal give `None`, so the caller keeps
/// its current theme. tao's `Window::theme()` maps all of those to light.
pub fn color_scheme_is_dark(scheme: Option<u32>) -> Option<bool> {
match scheme {
Some(1) => Some(true),
Some(2) => Some(false),
_ => None,
}
}
/// Whether the desktop portal states a dark or light preference; `None`
/// when it states none or cannot be reached. Blocks for up to
/// `PORTAL_TIMEOUT`.
pub fn portal_prefers_dark() -> Option<bool> {
color_scheme_is_dark(read_color_scheme())
}
fn read_color_scheme() -> Option<u32> {
use dbus::arg::Variant;
use dbus::blocking::Connection;
let result = Connection::new_session().and_then(|conn| {
let proxy = conn.with_proxy(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
PORTAL_TIMEOUT,
);
let (value,): (Variant<Variant<u32>>,) = proxy.method_call(
"org.freedesktop.portal.Settings",
"Read",
("org.freedesktop.appearance", "color-scheme"),
)?;
Ok(value.0 .0)
});
match result {
Ok(scheme) => Some(scheme),
Err(err) => {
log::debug!("portal color-scheme unavailable: {err}");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -128,4 +176,15 @@ mod tests {
assert!(!positioning_supported(os("wayland-0"), os("wayland,x11")));
assert!(!positioning_supported(None, os("broadway")));
}
#[test]
fn only_a_stated_portal_preference_sets_the_theme() {
assert_eq!(color_scheme_is_dark(Some(1)), Some(true));
assert_eq!(color_scheme_is_dark(Some(2)), Some(false));
// No preference, unknown values and an unreachable portal keep the
// current theme (dark by default).
assert_eq!(color_scheme_is_dark(Some(0)), None);
assert_eq!(color_scheme_is_dark(Some(7)), None);
assert_eq!(color_scheme_is_dark(None), None);
}
}