use std::sync::{Arc, Mutex};
use log::{debug, info, warn};
use notify_rust::{Notification, Timeout};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotificationResult {
Accepted,
Denied,
}
pub type YesNoResult = NotificationResult;
fn requires_default_action() -> bool {
notify_rust::get_server_information()
.map(|info| {
let server_name = info.name.to_lowercase();
debug!(
"Notification server: {} (version: {})",
info.name, info.version
);
match (server_name.as_str(), info.version.as_str()) {
("notify-osd", "1.0") | ("mako", "0.0.0") => {
info!("Detected {} - using default action mode", server_name);
true
}
_ => false,
}
})
.unwrap_or_else(|e| {
warn!("Failed to get notification server info: {}", e);
false
})
}
pub fn show_verification_notification(
operation: &str,
relying_party: Option<&str>,
user: Option<&str>,
timeout_seconds: u32,
) -> Result<NotificationResult, String> {
let mut message = format!("Operation: {}", operation);
if let Some(rp) = relying_party {
message.push_str(&format!("\nRelying Party: {}", rp));
}
if let Some(user) = user {
message.push_str(&format!("\nUser: {}", user));
}
info!("Showing user verification notification");
let default_means_user_present = requires_default_action();
let action_result = Arc::new(Mutex::new(None));
let action_result_clone = action_result.clone();
let mut notification = Notification::new();
notification
.summary("🔒 User Verification Required")
.body(&message)
.icon("security-high")
.timeout(Timeout::Milliseconds(timeout_seconds * 1000));
if default_means_user_present {
notification.action("default", "");
} else {
notification.action("approve", "Accept");
notification.action("deny", "Deny");
}
let handle = notification
.show()
.map_err(|e| format!("Failed to show notification: {}", e))?;
handle.wait_for_action(|action| {
debug!("User action received: {}", action);
let mut result = action_result_clone
.lock()
.expect("Failed to lock action result");
*result = Some(action.to_string());
});
let action = action_result
.lock()
.expect("Failed to lock action result")
.clone()
.unwrap_or_else(|| "__closed".to_string());
let user_present = match action.as_str() {
"approve" => true,
"deny" => false,
"default" => default_means_user_present,
"__closed" => false,
other => {
debug!("Unknown action '{}' - treating as denied", other);
false
}
};
if user_present {
info!("User verification accepted via notification");
Ok(NotificationResult::Accepted)
} else {
info!("User verification denied or notification closed");
Ok(NotificationResult::Denied)
}
}
pub fn show_yes_no_notification(title: &str, question: &str) -> Result<YesNoResult, String> {
info!("Showing yes/no notification: {}", title);
let default_means_yes = requires_default_action();
let action_result = Arc::new(Mutex::new(None));
let action_result_clone = action_result.clone();
let mut notification = Notification::new();
notification
.summary(title)
.body(question)
.icon("dialog-question")
.timeout(Timeout::Never);
if default_means_yes {
notification.action("default", "");
} else {
notification.action("yes", "Yes");
notification.action("no", "No");
}
let handle = notification
.show()
.map_err(|e| format!("Failed to show notification: {}", e))?;
handle.wait_for_action(|action| {
debug!("User action received: {}", action);
let mut result = action_result_clone
.lock()
.expect("Failed to lock action result");
*result = Some(action.to_string());
});
let action = action_result
.lock()
.expect("Failed to lock action result")
.clone()
.unwrap_or_else(|| "__closed".to_string());
let user_said_yes = match action.as_str() {
"yes" => true,
"no" => false,
"default" => default_means_yes,
"__closed" => false,
other => {
debug!("Unknown action '{}' - treating as no", other);
false
}
};
if user_said_yes {
info!("User answered yes");
Ok(YesNoResult::Accepted)
} else {
info!("User answered no or closed notification");
Ok(YesNoResult::Denied)
}
}
pub fn show_info_notification(title: &str, message: &str) -> Result<(), String> {
info!("Showing info notification: {}", title);
Notification::new()
.summary(title)
.body(message)
.icon("dialog-information")
.timeout(Timeout::Milliseconds(5000))
.show()
.map_err(|e| format!("Failed to show notification: {}", e))?;
Ok(())
}
pub fn show_error_notification(title: &str, error_message: &str) -> Result<(), String> {
warn!("Showing error notification: {}", title);
Notification::new()
.summary(title)
.body(error_message)
.icon("dialog-error")
.timeout(Timeout::Milliseconds(8000))
.show()
.map_err(|e| format!("Failed to show notification: {}", e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_notification_result_equality() {
assert_eq!(NotificationResult::Accepted, NotificationResult::Accepted);
assert_eq!(NotificationResult::Denied, NotificationResult::Denied);
assert_ne!(NotificationResult::Accepted, NotificationResult::Denied);
}
#[test]
fn test_requires_default_action_doesnt_panic() {
let _ = requires_default_action();
}
}