mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-24 16:17:27 +00:00
A 1.5 s thread asks the platform whether the foreground window is a fullscreen window of another process and feeds the result through the visibility state machine. Windows keeps the proven evaluation with an own-process check in place of the bubble class check; Linux reads the EWMH active window state over X11; native Wayland and macOS report false.
123 lines
4.3 KiB
Rust
123 lines
4.3 KiB
Rust
//! Fullscreen auto-hide: a background thread checks every 1.5 s whether a
|
|
//! fullscreen window of another process is in the foreground, and hides the
|
|
//! bubbles while one is. They come back without activation when it leaves,
|
|
//! unless the user hid them in the meantime.
|
|
|
|
use std::io;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use crate::core::visibility::Event;
|
|
use crate::platform::{self, FullscreenCheck};
|
|
use crate::runtime::Runtime;
|
|
|
|
/// How often the foreground window is checked.
|
|
pub const CHECK_INTERVAL: Duration = Duration::from_millis(1500);
|
|
|
|
/// Start the check thread. It runs until the process starts exiting.
|
|
pub fn spawn(runtime: Arc<Runtime>) -> io::Result<()> {
|
|
std::thread::Builder::new()
|
|
.name("fullscreen".into())
|
|
.spawn(move || {
|
|
while !runtime.is_exiting() {
|
|
tick(&runtime);
|
|
std::thread::sleep(CHECK_INTERVAL);
|
|
}
|
|
})
|
|
.map(drop)
|
|
}
|
|
|
|
/// One check: read the foreground state and, when it differs from what the
|
|
/// bubbles reflect, apply the matching visibility event. The state is read
|
|
/// and released before any window call (`apply_visibility` does the same).
|
|
fn tick(runtime: &Runtime) {
|
|
let check = platform::foreground_is_fullscreen();
|
|
let visibility = runtime.with_state(|s| s.visibility);
|
|
let Some(event) = event_for(visibility.hidden_by_fullscreen, check.fullscreen) else {
|
|
return;
|
|
};
|
|
log_decision(event, &check, visibility.user_hidden);
|
|
runtime.apply_visibility(event);
|
|
}
|
|
|
|
/// The visibility event for a check result, or `None` when the bubbles
|
|
/// already reflect it.
|
|
pub fn event_for(hidden_by_fullscreen: bool, fullscreen: bool) -> Option<Event> {
|
|
match (hidden_by_fullscreen, fullscreen) {
|
|
(false, true) => Some(Event::FullscreenOn),
|
|
(true, false) => Some(Event::FullscreenOff),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn log_decision(event: Event, check: &FullscreenCheck, user_hidden: bool) {
|
|
let action = if event == Event::FullscreenOn {
|
|
"hide"
|
|
} else {
|
|
"show"
|
|
};
|
|
log::info!(
|
|
"bubble fullscreen {action} reason={} {} user_hidden={user_hidden}",
|
|
check.reason,
|
|
check.detail
|
|
);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::core::visibility::{self, Action, Visibility};
|
|
|
|
#[test]
|
|
fn only_a_change_produces_an_event() {
|
|
assert_eq!(event_for(false, true), Some(Event::FullscreenOn));
|
|
assert_eq!(event_for(true, false), Some(Event::FullscreenOff));
|
|
assert_eq!(event_for(false, false), None);
|
|
assert_eq!(event_for(true, true), None);
|
|
}
|
|
|
|
/// Feed a sequence of check results through the state machine and
|
|
/// collect the actions the bubbles receive.
|
|
fn run(start: Visibility, checks: &[bool]) -> (Visibility, Vec<Action>) {
|
|
let mut state = start;
|
|
let mut actions = Vec::new();
|
|
for &fullscreen in checks {
|
|
if let Some(event) = event_for(state.hidden_by_fullscreen, fullscreen) {
|
|
let (next, action) = visibility::next(state, event);
|
|
state = next;
|
|
actions.extend(action);
|
|
}
|
|
}
|
|
(state, actions)
|
|
}
|
|
|
|
#[test]
|
|
fn a_fullscreen_spell_hides_once_and_shows_once() {
|
|
let (state, actions) = run(Visibility::default(), &[false, true, true, true, false]);
|
|
assert_eq!(actions, [Action::Hide, Action::Show]);
|
|
assert_eq!(state, Visibility::default());
|
|
}
|
|
|
|
#[test]
|
|
fn user_hidden_bubbles_stay_hidden_after_fullscreen() {
|
|
let start = Visibility::from_widget_visible(false);
|
|
let (state, actions) = run(start, &[true, false]);
|
|
assert_eq!(actions, [Action::Hide]);
|
|
assert_eq!(state, start);
|
|
}
|
|
|
|
#[test]
|
|
fn a_user_show_during_fullscreen_is_not_undone_by_the_next_check() {
|
|
let (hidden, _) = run(Visibility::default(), &[true]);
|
|
let (shown, action) = visibility::next(hidden, Event::UserShow);
|
|
assert_eq!(action, Some(Action::Show));
|
|
// Fullscreen is still on: no event, so the bubbles stay shown.
|
|
let (state, actions) = run(shown, &[true, true]);
|
|
assert!(actions.is_empty());
|
|
// Leaving fullscreen clears the flag; the bubbles are already shown.
|
|
let (state, actions) = run(state, &[false]);
|
|
assert_eq!(actions, [Action::Show]);
|
|
assert!(state.shows());
|
|
}
|
|
}
|