Files
claude-code-usage-bubble/src/platform/mod.rs
T
tiennm99 d1f48dfb2f fix: keep bubbles and the panel on their own monitor and follow the Linux theme
- Place the details panel inside the work area of the bubble's monitor
  instead of the union of all monitors, so it flips above the bubble
  rather than landing over the taskbar or off-screen in mixed layouts.
- Clamp bubbles into their monitor's work area after a resize and update
  the saved position of bubbles that already have one.
- Snap to the taskbar only when it borders the bubble's work area, so the
  primary monitor's taskbar no longer pulls bubbles on other monitors.
- Follow live theme changes on Linux through the desktop portal's
  SettingChanged signal, since tao's ThemeChanged never reaches a window.
2026-10-23 03:12:45 +07:00

211 lines
5.7 KiB
Rust

//! OS-specific code. Each platform file is compiled only on its own target,
//! and portable callers reach it through `cfg`-gated calls.
use std::time::{Duration, Instant};
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "linux")]
pub mod linux_theme;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(windows)]
pub mod windows;
/// How often the Unix wait checks whether the process is still there.
#[cfg(unix)]
const WAIT_POLL_INTERVAL: Duration = Duration::from_millis(100);
/// Wait until process `pid` has exited, at most `timeout`. Returns true when
/// it is gone, false when it was still running at the deadline.
pub fn wait_pid(pid: u32, timeout: Duration) -> bool {
#[cfg(windows)]
{
windows::wait_pid(pid, timeout)
}
#[cfg(unix)]
{
wait_until_gone(timeout, WAIT_POLL_INTERVAL, || unix_pid_alive(pid))
}
}
/// Poll `alive` every `interval` until it returns false or `timeout` passes.
/// Returns true when `alive` reported false.
pub fn wait_until_gone(
timeout: Duration,
interval: Duration,
mut alive: impl FnMut() -> bool,
) -> bool {
let deadline = Instant::now() + timeout;
loop {
if !alive() {
return true;
}
let now = Instant::now();
if now >= deadline {
return false;
}
std::thread::sleep(interval.min(deadline - now));
}
}
/// Whether `pid` names a live (or zombie) process. A process owned by
/// another user still counts as alive.
#[cfg(unix)]
fn unix_pid_alive(pid: u32) -> bool {
// Zero and negative ids address process groups, never one process.
let Ok(pid) = libc::pid_t::try_from(pid) else {
return false;
};
if pid <= 0 {
return false;
}
// SAFETY: signal 0 only checks for existence and permission.
if unsafe { libc::kill(pid, 0) } == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
/// Whether the system theme is dark at startup. Platforms without a
/// readable system setting default to dark.
pub fn system_is_dark() -> bool {
#[cfg(windows)]
{
windows::is_dark()
}
#[cfg(not(windows))]
{
true
}
}
/// Whether the app can place its windows. False only under a native
/// Wayland GTK backend (no Xwayland), where moves are ignored and positions
/// read back as `(0, 0)`, so snapping, clamping and saving are skipped.
pub fn positioning_supported() -> bool {
#[cfg(target_os = "linux")]
{
linux::positioning_supported(
std::env::var_os("WAYLAND_DISPLAY"),
std::env::var_os("GDK_BACKEND"),
)
}
#[cfg(not(target_os = "linux"))]
{
true
}
}
/// Whether the primary mouse button is held right now, where the platform
/// can tell; false otherwise.
pub fn primary_button_down() -> bool {
#[cfg(target_os = "linux")]
{
linux::button1_down()
}
#[cfg(target_os = "macos")]
{
macos::primary_button_down()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
false
}
}
/// The result of one fullscreen check: whether a fullscreen window of
/// another process is in the foreground, and why.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FullscreenCheck {
pub fullscreen: bool,
pub reason: &'static str,
/// The window and geometry behind the decision for the diagnostic log;
/// empty when there is nothing to add.
pub detail: String,
}
impl FullscreenCheck {
pub fn new(fullscreen: bool, reason: &'static str) -> Self {
Self {
fullscreen,
reason,
detail: String::new(),
}
}
}
/// Whether the foreground window is a fullscreen window that is not ours.
/// Native Wayland and macOS cannot tell and always report false.
pub fn foreground_is_fullscreen() -> FullscreenCheck {
#[cfg(windows)]
{
windows::foreground_is_fullscreen()
}
#[cfg(target_os = "linux")]
{
if positioning_supported() {
linux::foreground_is_fullscreen()
} else {
FullscreenCheck::new(false, "native Wayland cannot report the foreground")
}
}
#[cfg(target_os = "macos")]
{
macos::foreground_is_fullscreen()
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
{
FullscreenCheck::new(false, "unsupported platform")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wait_until_gone_returns_once_the_process_is_gone() {
let mut checks = 0;
let gone = wait_until_gone(Duration::from_secs(5), Duration::from_millis(1), || {
checks += 1;
checks < 3
});
assert!(gone);
assert_eq!(checks, 3);
}
#[test]
fn wait_until_gone_gives_up_at_the_deadline() {
let start = Instant::now();
let gone = wait_until_gone(Duration::from_millis(30), Duration::from_millis(5), || true);
assert!(!gone);
assert!(start.elapsed() >= Duration::from_millis(30));
assert!(start.elapsed() < Duration::from_secs(2));
}
#[test]
fn a_running_process_is_waited_for_until_the_timeout() {
assert!(!wait_pid(std::process::id(), Duration::from_millis(50)));
}
#[cfg(unix)]
#[test]
fn an_exited_process_is_gone() {
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn true");
let pid = child.id();
// Reap it, so the pid no longer names even a zombie.
child.wait().expect("wait");
assert!(wait_pid(pid, Duration::from_secs(2)));
}
#[cfg(unix)]
#[test]
fn group_ids_are_never_alive() {
assert!(!unix_pid_alive(0));
assert!(!unix_pid_alive(u32::MAX));
}
}