Files
guara/crates/guara-core/src/notify.rs
T
Lucas RayanandClaude Sonnet 5 6328220c60
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
chore(i18n): finish translating code comments and scripts to English
Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:43:02 -03:00

125 lines
4.3 KiB
Rust

//! Native OS desktop notifications (scan finished, disk disconnected, etc.) using
//! standard platform CLI tools with zero extra crate dependencies.
use std::process::{Command, Stdio};
/// Notification events emitted by the core. Messages are formatted in the
/// effective UI language (`settings::ui_lang`) since OS notifications bypass
/// the DOM i18n translator.
pub enum Notice {
ScanDone { count: usize, target: String },
RecoverDone { count: usize, destination: String },
DiskLost,
}
impl Notice {
fn text(&self, lang: &str) -> (String, String) {
let en = lang == "en";
match self {
Notice::ScanDone { count, target } => (
"Guará: scan complete".into(),
if en {
format!("{count} file(s) found in {target}.")
} else {
format!("{count} arquivo(s) encontrados em {target}.")
},
),
Notice::RecoverDone { count, destination } => (
if en {
"Guará: recovery complete".into()
} else {
"Guará: recuperação concluída".into()
},
if en {
format!("{count} file(s) saved to {destination}")
} else {
format!("{count} arquivo(s) salvos em {destination}")
},
),
Notice::DiskLost => (
if en {
"Guará: disk disconnected?".into()
} else {
"Guará: disco desconectado?".into()
},
if en {
"The scan was paused without losing anything. Reconnect the disk and continue."
.into()
} else {
"A varredura foi pausada sem perder nada. Reconecte o disco e continue.".into()
},
),
}
}
}
/// Sends a desktop notification asynchronously. Failures are silently ignored.
pub fn send(notice: Notice) {
let (title, body) = notice.text(&crate::settings::ui_lang());
std::thread::spawn(move || {
if let Some(mut c) = command(&title, &body) {
if let Ok(mut child) = c
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
let _ = child.wait();
}
}
});
}
#[cfg(target_os = "linux")]
fn command(title: &str, body: &str) -> Option<Command> {
// When running as root, dispatch the notification inside the real user's session.
let mut c = if let Some(u) = crate::owner::invoking_user() {
let mut c = Command::new("runuser");
c.args(["-u", &u.name, "--", "env"]);
for k in ["DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY"] {
if let Ok(v) = std::env::var(k) {
c.arg(format!("{k}={v}"));
}
}
c.arg(format!("XDG_RUNTIME_DIR=/run/user/{}", u.uid));
c.arg(format!(
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{}/bus",
u.uid
));
c.arg("notify-send");
c
} else {
Command::new("notify-send")
};
c.args(["-a", "Guará", "-i", "drive-harddisk", title, body]);
Some(c)
}
#[cfg(target_os = "macos")]
fn command(title: &str, body: &str) -> Option<Command> {
let esc = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
let mut c = Command::new("osascript");
c.args([
"-e",
&format!(
"display notification \"{}\" with title \"{}\"",
esc(body),
esc(title)
),
]);
Some(c)
}
#[cfg(windows)]
fn command(title: &str, body: &str) -> Option<Command> {
let esc = |s: &str| s.replace('\'', "''");
let script = format!(
"Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(6000, '{}', '{}', [System.Windows.Forms.ToolTipIcon]::None); Start-Sleep -Seconds 7; $n.Dispose()",
esc(title),
esc(body)
);
let mut c = Command::new("powershell");
c.args(["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script]);
Some(c)
}