use std::{
ptr,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::{
util::{PROCESS_NAME, normalize_text, to_wide},
win32::{
FindWindowW, MB_SETFOREGROUND, MB_SYSTEMMODAL, MessageBoxExW, PostMessageW, WM_CLOSE,
},
};
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MsgBtnType {
Ok = 0x0000,
OkCancel = 0x0001,
YesNo = 0x0004,
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MsgBoxType {
Error = 0x0010,
Quest = 0x0020,
Warn = 0x0030,
Info = 0x0040,
}
impl std::fmt::Display for MsgBoxType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
MsgBoxType::Error => "Error",
MsgBoxType::Quest => "Question",
MsgBoxType::Warn => "Warning",
MsgBoxType::Info => "Info",
};
write!(f, "{}", s)
}
}
fn spawn_timeout_closer(title: Vec<u16>, timeout_ms: u64, timed_out: Arc<AtomicBool>) {
if timeout_ms == 0 {
return;
}
thread::spawn(move || {
thread::sleep(Duration::from_millis(timeout_ms));
unsafe {
let hwnd = FindWindowW(ptr::null(), title.as_ptr());
if hwnd != 0 {
timed_out.store(true, Ordering::SeqCst);
PostMessageW(hwnd, WM_CLOSE, 0, 0);
}
}
});
}
pub(crate) fn raw_msgbox(
msg: impl ToString,
title: impl ToString,
msgtype: MsgBoxType,
btntype: MsgBtnType,
timeout_ms: u64,
) -> i32 {
let msg = normalize_text(msg);
let title = {
let t = normalize_text(title);
let original = if t.is_empty() { msgtype.to_string() } else { t };
format!(
"{} [{}] {}",
original,
PROCESS_NAME,
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
)
};
let text_w = to_wide(&msg);
let title_w = to_wide(&title);
let timed_out = Arc::new(AtomicBool::new(false));
spawn_timeout_closer(title_w.clone(), timeout_ms, timed_out.clone());
let flags = btntype as u32 | msgtype as u32 | MB_SETFOREGROUND | MB_SYSTEMMODAL;
let result = unsafe { MessageBoxExW(0, text_w.as_ptr(), title_w.as_ptr(), flags, 0) };
if timed_out.load(Ordering::SeqCst) {
-1
} else {
result
}
}
pub fn custom_msgbox(
msg: impl ToString,
title: impl ToString,
msgbox_type: MsgBoxType,
msgboxbtn_type: MsgBtnType,
timeout_ms: u64,
) -> i32 {
raw_msgbox(msg, title, msgbox_type, msgboxbtn_type, timeout_ms)
}
#[allow(dead_code)]
pub fn info_msgbox(msg: impl ToString, title: impl ToString, timeout_ms: u64) -> i32 {
raw_msgbox(msg, title, MsgBoxType::Info, MsgBtnType::Ok, timeout_ms)
}
#[allow(dead_code)]
pub fn error_msgbox(msg: impl ToString, title: impl ToString, timeout_ms: u64) -> i32 {
raw_msgbox(msg, title, MsgBoxType::Error, MsgBtnType::Ok, timeout_ms)
}
#[allow(dead_code)]
pub fn warn_msgbox(msg: impl ToString, title: impl ToString, timeout_ms: u64) -> i32 {
raw_msgbox(msg, title, MsgBoxType::Warn, MsgBtnType::Ok, timeout_ms)
}
#[allow(dead_code)]
pub fn quest_msgbox_yesno(msg: impl ToString, title: impl ToString, timeout_ms: u64) -> i32 {
raw_msgbox(msg, title, MsgBoxType::Quest, MsgBtnType::YesNo, timeout_ms)
}
#[allow(dead_code)]
pub fn quest_msgbox_okcancel(msg: impl ToString, title: impl ToString, timeout_ms: u64) -> i32 {
raw_msgbox(
msg,
title,
MsgBoxType::Quest,
MsgBtnType::OkCancel,
timeout_ms,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn msg_box_type_display() {
assert_eq!(MsgBoxType::Error.to_string(), "Error");
assert_eq!(MsgBoxType::Info.to_string(), "Info");
assert_eq!(MsgBoxType::Quest.to_string(), "Question");
assert_eq!(MsgBoxType::Warn.to_string(), "Warning");
}
#[test]
fn raw_msgbox_normalizes_crlf() {
let msg = normalize_text("line1\r\nline2");
assert_eq!(msg, "line1\nline2");
}
#[test]
fn raw_msgbox_normalizes_cr() {
let msg = normalize_text("line1\rline2");
assert_eq!(msg, "line1\nline2");
}
#[test]
fn raw_msgbox_trims_whitespace() {
let msg = normalize_text(" hello world ");
assert_eq!(msg, "hello world");
}
#[test]
fn spawn_timeout_closer_zero_timeout() {
let timed_out = Arc::new(AtomicBool::new(false));
let title = to_wide("test");
spawn_timeout_closer(title, 0, timed_out.clone());
assert!(!timed_out.load(Ordering::SeqCst));
}
#[test]
fn spawn_timeout_closer_window_not_found() {
let timed_out = Arc::new(AtomicBool::new(false));
let title = to_wide("NonExistentWindowTitle_12345");
spawn_timeout_closer(title, 10, timed_out.clone());
thread::sleep(Duration::from_millis(50));
assert!(!timed_out.load(Ordering::SeqCst));
}
#[test]
fn to_wide_null_terminated() {
let wide = to_wide("test");
assert_eq!(wide.last(), Some(&0));
}
#[test]
fn to_wide_ascii() {
let wide = to_wide("ABC");
assert_eq!(wide, vec![0x0041, 0x0042, 0x0043, 0x0000]);
}
#[test]
fn to_wide_unicode() {
let wide = to_wide("你好");
assert_eq!(wide, vec![0x4F60, 0x597D, 0x0000]);
}
}