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
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
125 lines
4.3 KiB
Rust
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)
|
|
}
|