Skip to main content

lean_ctx/core/value/
notify.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Native desktop notifications without a new dependency: `osascript` on
3//! macOS, `notify-send` on Linux desktops, a WinRT toast via PowerShell on
4//! Windows. Title and body are passed as arguments or environment variables,
5//! never spliced into a script, so no text can be interpreted.
6
7use std::process::{Command, Stdio};
8
9/// Shows a notification; `false` when this machine has no way to show one.
10pub fn send(title: &str, body: &str) -> bool {
11    if cfg!(test) {
12        return false;
13    }
14    let Some(mut cmd) = command(title, body) else {
15        return false;
16    };
17    cmd.stdin(Stdio::null())
18        .stdout(Stdio::null())
19        .stderr(Stdio::null())
20        .status()
21        .is_ok_and(|s| s.success())
22}
23
24#[cfg(target_os = "macos")]
25fn command(title: &str, body: &str) -> Option<Command> {
26    let mut cmd = Command::new("osascript");
27    cmd.args([
28        "-e",
29        "on run argv",
30        "-e",
31        "display notification (item 2 of argv) with title (item 1 of argv)",
32        "-e",
33        "end run",
34        title,
35        body,
36    ]);
37    Some(cmd)
38}
39
40#[cfg(all(unix, not(target_os = "macos")))]
41fn command(title: &str, body: &str) -> Option<Command> {
42    // Headless boxes and SSH sessions have nowhere to show it.
43    if std::env::var_os("DISPLAY").is_none() && std::env::var_os("WAYLAND_DISPLAY").is_none() {
44        return None;
45    }
46    let mut cmd = Command::new("notify-send");
47    cmd.args(["--app-name=lean-ctx", "--urgency=low", "--", title, body]);
48    Some(cmd)
49}
50
51#[cfg(windows)]
52fn command(title: &str, body: &str) -> Option<Command> {
53    // Windows PowerShell's own AppUserModelID: toasts from an unregistered
54    // app id are silently dropped.
55    const SCRIPT: &str = "\
56[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; \
57$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); \
58$x = $t.GetElementsByTagName('text'); \
59$x.Item(0).AppendChild($t.CreateTextNode($env:LEAN_CTX_TOAST_TITLE)) | Out-Null; \
60$x.Item(1).AppendChild($t.CreateTextNode($env:LEAN_CTX_TOAST_BODY)) | Out-Null; \
61[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show([Windows.UI.Notifications.ToastNotification]::new($t))";
62    let mut cmd = Command::new("powershell");
63    cmd.args(["-NoProfile", "-NonInteractive", "-Command", SCRIPT])
64        .env("LEAN_CTX_TOAST_TITLE", title)
65        .env("LEAN_CTX_TOAST_BODY", body);
66    Some(cmd)
67}
68
69#[cfg(not(any(unix, windows)))]
70fn command(_title: &str, _body: &str) -> Option<Command> {
71    None
72}