use std::io::Write;
use std::process::{Command, Stdio};
pub const NOTIFY_ENV: &str = "RIGGER_NOTIFY";
pub enum Notifier {
Named(std::ffi::OsString),
Platform,
}
pub fn notifier() -> Option<Notifier> {
match std::env::var_os(NOTIFY_ENV) {
Some(named) if !named.is_empty() => Some(Notifier::Named(named)),
_ if crate::paths::scratch() => None,
_ => Some(Notifier::Platform),
}
}
pub fn show(notifier: &Notifier, title: &str, body: &str) -> bool {
match notifier {
Notifier::Named(program) => feed(Command::new(program).arg(title), body),
Notifier::Platform => platform(title, body),
}
}
fn feed(command: &mut Command, input: &str) -> bool {
let child = command.stdin(Stdio::piped()).stdout(Stdio::null()).stderr(Stdio::null()).spawn();
let Ok(mut child) = child else {
return false;
};
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(input.as_bytes());
}
child.wait().is_ok_and(|status| status.success())
}
#[cfg(windows)]
fn platform(title: &str, body: &str) -> bool {
Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-EncodedCommand", &encode(&windows_script(title, body))])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(any(windows, test))]
fn encode(script: &str) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect();
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let n = chunk.iter().enumerate().fold(0u32, |n, (i, b)| n | u32::from(*b) << (16 - 8 * i));
for i in 0..4 {
match i <= chunk.len() {
true => out.push(ALPHABET[(n >> (18 - 6 * i) & 63) as usize] as char),
false => out.push('='),
}
}
}
out
}
#[cfg(any(windows, test))]
fn windows_script(title: &str, body: &str) -> String {
let mut texts = format!("<text>{}</text>", xml(title));
for line in body.lines().filter(|l| !l.trim().is_empty()).take(2) {
texts.push_str(&format!("<text>{}</text>", xml(line)));
}
let toast = format!("<toast><visual><binding template=\"ToastGeneric\">{texts}</binding></visual></toast>");
[
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
"[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null",
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
&format!("$xml.LoadXml('{}')", toast.replace('\'', "''")),
"$toast = New-Object Windows.UI.Notifications.ToastNotification $xml",
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show($toast)",
"",
]
.join("\n")
}
#[cfg(any(windows, test))]
fn xml(text: &str) -> String {
text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
}
#[cfg(target_os = "macos")]
fn platform(title: &str, body: &str) -> bool {
Command::new("osascript")
.args([
"-e",
"on run argv",
"-e",
"display notification (item 2 of argv) with title (item 1 of argv)",
"-e",
"end run",
])
.arg(title)
.arg(body)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(all(not(windows), not(target_os = "macos")))]
fn platform(title: &str, body: &str) -> bool {
Command::new("notify-send")
.args(["--app-name", "rigger", title, body])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_owners_words_cannot_break_the_script() {
let script = windows_script("rigger · 2026-W39", "Focus: a <b> & c's \"d\"\nsecond\nthird is dropped");
assert!(script.contains("<text>Focus: a <b> & c''s "d"</text>"), "{script}");
assert!(script.contains("<text>second</text>"), "{script}");
assert!(!script.contains("third"), "{script}");
}
#[test]
fn an_encoded_command_is_utf16_in_base64() {
assert_eq!(encode("a"), "YQA=");
assert_eq!(encode("ab"), "YQBiAA==");
assert_eq!(encode("abc"), "YQBiAGMA");
assert_eq!(encode("я"), "TwQ=");
}
}