- D1 (HIGH): Linux no-sleep was completely non-functional — the zbus
ScreenSaver inhibit was bound to a function-local D-Bus connection dropped on
return, so the screensaver service auto-released it instantly. Keep a
long-lived Connection in managed state (InhibitState { conn, cookie }) so the
same connection holds Inhibit and issues UnInhibit; created once, reused.
- D3: tray "Do Not Disturb" desynced from the web manualDndAtom after any reload
(custom-chrome toggle / logout) — the atom is in-memory and reset while the
tray stayed checked. Added TrayDndState + a get_tray_dnd command so the web
hook re-hydrates the atom on mount.
- D5: install_update now calls app.restart() after a successful install so the
new version actually runs (Linux AppImage kept running the old binary; the UI
hung on "installing").
CI-compile-verified (windows + linux). Web-side wiring (get_tray_dnd query,
updater terminal state) landed on cinny:lotus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
7.6 KiB
Rust
176 lines
7.6 KiB
Rust
//! P5-46 / P6-1 — System power management (call continuity).
|
|
//!
|
|
//! Prevents the system from sleeping / turning off the display while a voice or
|
|
//! video call is active, then releases the request when the call ends. The web
|
|
//! client calls `set_call_active(true|false)` from `useTauriCallPower` as the
|
|
//! call-embed atom transitions.
|
|
//!
|
|
//! Windows: `SetThreadExecutionState`. The request is per-thread and persists
|
|
//! until cleared, so we run every set/clear on the **main thread** (via
|
|
//! `run_on_main_thread`) to guarantee the clear cancels the matching set even
|
|
//! though Tauri commands otherwise run on a pool thread.
|
|
//!
|
|
//! Linux (P6-1): `org.freedesktop.ScreenSaver` `Inhibit`/`UnInhibit` over the
|
|
//! session bus (zbus, blocking API). The inhibit cookie returned by `Inhibit`
|
|
//! is stored in Tauri managed state (`ScreenSaverInhibit`) so the later
|
|
//! `UnInhibit` can release exactly that request. The owning D-Bus **connection**
|
|
//! is stored alongside the cookie and kept alive for the inhibit's whole
|
|
//! duration: `org.freedesktop.ScreenSaver` auto-releases an inhibit the instant
|
|
//! the connection that took it disappears, so a per-call function-local
|
|
//! connection would drop the inhibit immediately. The one connection is opened
|
|
//! lazily on first inhibit and reused for the matching `UnInhibit`. All D-Bus
|
|
//! failures are logged and swallowed — a missing/absent screensaver service must
|
|
//! never break a call.
|
|
//!
|
|
//! macOS is out of scope for P6-1 (would use `IOPMAssertionCreate`) and stays a
|
|
//! no-op; the command stays cross-platform so the web side is unconditional.
|
|
|
|
use tauri::AppHandle;
|
|
|
|
/// The long-lived screensaver-inhibit state (Linux only). Both fields live
|
|
/// behind ONE mutex so the connection and the cookie it produced can never
|
|
/// desync: `conn` is the session-bus connection that *owns* the inhibit, and
|
|
/// `cookie` is the handle returned by `Inhibit`. The connection is opened once
|
|
/// (lazily, on the first inhibit) and reused for the matching `UnInhibit`;
|
|
/// keeping it alive here is what stops the screensaver service from
|
|
/// auto-releasing the inhibit.
|
|
#[cfg(target_os = "linux")]
|
|
#[derive(Default)]
|
|
struct InhibitState {
|
|
conn: Option<zbus::blocking::Connection>,
|
|
cookie: Option<u32>,
|
|
}
|
|
|
|
/// Tauri managed state wrapper. Registered in `setup()` and read by
|
|
/// `set_call_active` via `AppHandle::state()`.
|
|
#[cfg(target_os = "linux")]
|
|
pub struct ScreenSaverInhibit(std::sync::Mutex<InhibitState>);
|
|
|
|
/// Register the Linux screensaver-inhibit managed state. No-op elsewhere.
|
|
/// Called once from `native::setup()`.
|
|
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use tauri::Manager;
|
|
app.manage(ScreenSaverInhibit(std::sync::Mutex::new(
|
|
InhibitState::default(),
|
|
)));
|
|
}
|
|
#[cfg(not(target_os = "linux"))]
|
|
let _ = app;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn set_call_active(app: AppHandle, active: bool) {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
let _ = app.run_on_main_thread(move || {
|
|
use windows::Win32::System::Power::{
|
|
SetThreadExecutionState, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED,
|
|
};
|
|
let flags = if active {
|
|
ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
|
} else {
|
|
// Clearing to ES_CONTINUOUS alone releases the sleep/display
|
|
// requirement while leaving no lingering per-thread state.
|
|
ES_CONTINUOUS
|
|
};
|
|
// Safety: FFI call with no pointers; returns the previous state,
|
|
// which we don't need.
|
|
unsafe {
|
|
SetThreadExecutionState(flags);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use tauri::Manager;
|
|
|
|
// Serialize access to the stored connection+cookie for the duration of
|
|
// the D-Bus round-trip. This command is the only touch point, so holding
|
|
// the lock across the (short, blocking) call cannot deadlock.
|
|
let state = app.state::<ScreenSaverInhibit>();
|
|
let mut inner = match state.0.lock() {
|
|
Ok(guard) => guard,
|
|
Err(e) => {
|
|
eprintln!("power: ScreenSaverInhibit mutex poisoned: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if active {
|
|
// Only take a new inhibit if one isn't already held, so repeated
|
|
// set_call_active(true) calls don't leak cookies.
|
|
if inner.cookie.is_none() {
|
|
// Lazily open the ONE long-lived session connection. Because the
|
|
// screensaver service auto-releases an inhibit when the owning
|
|
// connection disappears, this connection must outlive the
|
|
// inhibit — it stays in managed state and is reused below for
|
|
// UnInhibit. Never reopened once established.
|
|
if inner.conn.is_none() {
|
|
match zbus::blocking::Connection::session() {
|
|
Ok(conn) => inner.conn = Some(conn),
|
|
Err(e) => {
|
|
eprintln!("power: D-Bus session connection failed: {e}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// Scope the connection borrow so it ends before we mutate
|
|
// `inner.cookie` (both go through the MutexGuard's Deref, which
|
|
// borrows the whole guard, so the borrows must not overlap).
|
|
let res: zbus::Result<u32> = {
|
|
let conn = inner
|
|
.conn
|
|
.as_ref()
|
|
.expect("connection set immediately above");
|
|
match zbus::blocking::Proxy::new(
|
|
conn,
|
|
"org.freedesktop.ScreenSaver",
|
|
"/org/freedesktop/ScreenSaver",
|
|
"org.freedesktop.ScreenSaver",
|
|
) {
|
|
Ok(proxy) => proxy.call("Inhibit", &("Lotus Chat", "In a Lotus Chat call")),
|
|
Err(e) => {
|
|
eprintln!("power: ScreenSaver proxy init failed: {e}");
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
match res {
|
|
Ok(cookie) => inner.cookie = Some(cookie),
|
|
Err(e) => eprintln!("power: ScreenSaver Inhibit failed: {e}"),
|
|
}
|
|
}
|
|
} else if let Some(cookie) = inner.cookie.take() {
|
|
// Release on the SAME connection that took the inhibit. If it's
|
|
// somehow gone the inhibit was already auto-released, so nothing to do.
|
|
if let Some(conn) = inner.conn.as_ref() {
|
|
match zbus::blocking::Proxy::new(
|
|
conn,
|
|
"org.freedesktop.ScreenSaver",
|
|
"/org/freedesktop/ScreenSaver",
|
|
"org.freedesktop.ScreenSaver",
|
|
) {
|
|
Ok(proxy) => {
|
|
let res: zbus::Result<()> = proxy.call("UnInhibit", &(cookie,));
|
|
if let Err(e) = res {
|
|
eprintln!("power: ScreenSaver UnInhibit failed: {e}");
|
|
}
|
|
}
|
|
Err(e) => eprintln!("power: ScreenSaver proxy init failed: {e}"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
|
{
|
|
// No-op on other platforms (see module docs). Bind the args so the
|
|
// signature stays identical cross-platform and no unused warnings fire.
|
|
let _ = (&app, active);
|
|
}
|
|
}
|