mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-22 08:16:18 +00:00
fix: keep the dark default on Linux when the portal states no theme preference
This commit is contained in:
1 parent
1370c15048
commit
e805f8de8f
5 files changed
+85
-5
No files matched your search
Generated
+1
@@ -509,6 +509,7 @@ name = "claude-code-usage-bubble"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
"dbus",
|
||||
"dirs 6.0.0",
|
||||
"flate2",
|
||||
"libc",
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user