feat(native): Tier A desktop features (P5-46/36/44/43/49) + window chrome (P5-47)
Adds a `native/` module system (each feature = its own module exposing `#[tauri::command]`s + optional `setup`; `emit_to_web` pushes DOM CustomEvents to the web like `forward_deeplink`). Wired into generate_handler! + native::setup; windows-crate feature union added to Cargo.toml. - power.rs (P5-46): SetThreadExecutionState held on the main thread while a call is active; released on end. Cross-platform (no-op off Windows). - jumplist.rs (P5-36): ICustomDestinationList "Recent Rooms" of IShellLink tasks launching the exe with a matrix: arg (existing deep-link handler opens the room). - thumbbar.rs (P5-44): ITaskbarList3 ThumbBar Mute/Deafen/End (GDI HICONs) + a window subclass catching THBN_CLICKED → emit thumbbar-action. - smtc.rs (P5-43): WinRT SystemMediaTransportControls via GetForWindow; ButtonPressed → smtc-action; call-state command. (Experimental for a non-media app.) - network.rs (P5-49): INetworkListManager poll thread → emit network-changed. - chrome.rs (P5-47): cross-platform window-control commands + set_custom_chrome (set_decorations) for the opt-in TDS titlebar. NOT compile-verified locally (no Rust/Windows toolchain on the dev box) — this is for the CI Windows compile pass (GitHub test.yml / Gitea windows runner). Expect a possible fixup round (windows-crate feature/namespace paths, e.g. subclass APIs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
//! P5-49 — Network awareness (Windows connectivity / NCSI).
|
||||
//!
|
||||
//! Proactively detects when the machine gains or loses internet connectivity so
|
||||
//! the web client can surface an offline state and, more importantly, nudge the
|
||||
//! matrix client to retry its backed-off `/sync` the instant the network comes
|
||||
//! back instead of waiting out the sync-loop backoff timer.
|
||||
//!
|
||||
//! Windows: a lightweight background thread polls the Network List Manager
|
||||
//! (`INetworkListManager::IsConnectedToInternet`, the same NCSI signal the shell
|
||||
//! uses) every ~3 seconds. We prefer a robust poll over a COM event sink
|
||||
//! (`INetworkEvents`) — the poll is far simpler to reason about, needs no
|
||||
//! connection-point plumbing, and a 3s cadence is more than responsive enough
|
||||
//! for a "retry sync now" hint. We emit **only on a state transition**, so the
|
||||
//! web side gets one event per change rather than a steady heartbeat.
|
||||
//!
|
||||
//! Other platforms are a no-op: the browser already fires `online`/`offline`
|
||||
//! events, and the desktop shells (macOS/Linux) can adopt their own reachability
|
||||
//! APIs later; the web hook stays unconditional so nothing there needs guarding.
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Payload for the `network-changed` DOM event (`{ online: bool }`).
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(serde::Serialize)]
|
||||
struct NetworkState {
|
||||
online: bool,
|
||||
}
|
||||
|
||||
/// Called once from lib.rs `native::setup()`. On Windows, spawns the poll
|
||||
/// thread; elsewhere it does nothing.
|
||||
pub fn setup(app: &AppHandle) -> tauri::Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Own a handle inside the thread so the poll outlives this call and runs
|
||||
// for the lifetime of the app.
|
||||
let app = app.clone();
|
||||
std::thread::spawn(move || watch_network(app));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// No-op on non-Windows platforms (see module docs). Bind the arg so the
|
||||
// signature stays identical cross-platform with no unused warning.
|
||||
let _ = app;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll loop, runs on its own thread for the app's lifetime.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn watch_network(app: AppHandle) {
|
||||
use std::time::Duration;
|
||||
use windows::Win32::Networking::NetworkListManager::{INetworkListManager, NetworkListManager};
|
||||
use windows::Win32::System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
|
||||
COINIT_MULTITHREADED,
|
||||
};
|
||||
|
||||
// Initialize COM for this thread in the multithreaded apartment. This is a
|
||||
// dedicated thread, so it should be the first to init and succeed (S_FALSE —
|
||||
// "already initialized, same mode" — also counts as success). If it fails
|
||||
// outright (e.g. RPC_E_CHANGED_MODE) we can't proceed, so bail.
|
||||
// Safety: FFI call; `None` reserved param per the API contract.
|
||||
if unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the Network List Manager COM object (mirrors the `CoCreateInstance`
|
||||
// idiom in lib.rs `set_badge_count`). If it's unavailable, tear COM back down
|
||||
// and stop the thread cleanly.
|
||||
// Safety: standard COM instantiation; type is inferred from the annotation.
|
||||
let manager: INetworkListManager =
|
||||
match unsafe { CoCreateInstance(&NetworkListManager, None, CLSCTX_INPROC_SERVER) } {
|
||||
Ok(manager) => manager,
|
||||
Err(_) => {
|
||||
// Safety: balances the successful CoInitializeEx above.
|
||||
unsafe { CoUninitialize() };
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// `None` = unknown; the first successful read is treated as a transition so
|
||||
// the web side always learns the initial connectivity state.
|
||||
let mut last: Option<bool> = None;
|
||||
|
||||
loop {
|
||||
// `IsConnectedToInternet` yields a VARIANT_BOOL (VARIANT_TRUE == -1 when
|
||||
// connected). Skip transient read errors without emitting.
|
||||
// Safety: FFI call on a live COM interface owned by this thread.
|
||||
if let Ok(connected) = unsafe { manager.IsConnectedToInternet() } {
|
||||
let online = connected.as_bool();
|
||||
if last != Some(online) {
|
||||
last = Some(online);
|
||||
super::emit_to_web(
|
||||
&app,
|
||||
"network-changed",
|
||||
&serde_json::to_string(&NetworkState { online }).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_secs(3));
|
||||
}
|
||||
|
||||
// Note: the loop never returns, so we intentionally don't call
|
||||
// `CoUninitialize` here — COM stays initialized for this thread until the
|
||||
// process exits, which is exactly the desired lifetime.
|
||||
}
|
||||
Reference in New Issue
Block a user