use crate::outcome::Outcome;
use crate::request::{FileFilter, Level, Request};
pub(crate) const PROGRAM: &str = "powershell.exe";
const TEXT_SCRIPT: &str = include_str!("powershell/text.ps1");
const PASSWORD_SCRIPT: &str = include_str!("powershell/password.ps1");
const CHOICE_SCRIPT: &str = include_str!("powershell/choice.ps1");
const MULTIPLE_SCRIPT: &str = include_str!("powershell/multiple.ps1");
const COLOR_SCRIPT: &str = include_str!("powershell/color.ps1");
const MESSAGE_SCRIPT: &str = include_str!("powershell/message.ps1");
const CONFIRM_SCRIPT: &str = include_str!("powershell/confirm.ps1");
const OPEN_SCRIPT: &str = include_str!("powershell/open.ps1");
const SAVE_SCRIPT: &str = include_str!("powershell/save.ps1");
const FOLDER_SCRIPT: &str = include_str!("powershell/folder.ps1");
pub(crate) fn argv(request: &Request<'_>) -> Vec<String> {
let script = match request {
Request::Text { .. } => TEXT_SCRIPT,
Request::Password { .. } => PASSWORD_SCRIPT,
Request::Choice { .. } => CHOICE_SCRIPT,
Request::Choices { .. } => MULTIPLE_SCRIPT,
Request::Color { .. } => COLOR_SCRIPT,
Request::Message { .. } => MESSAGE_SCRIPT,
Request::Confirm { .. } => CONFIRM_SCRIPT,
Request::OpenFile { .. } => OPEN_SCRIPT,
Request::SaveFile { .. } => SAVE_SCRIPT,
Request::Folder { .. } => FOLDER_SCRIPT,
};
[
"-NoProfile",
"-STA",
"-ExecutionPolicy",
"Bypass",
"-Command",
script,
]
.iter()
.map(|part| (*part).to_string())
.collect()
}
pub(crate) fn env(request: &Request<'_>) -> Vec<(String, String)> {
let mut pairs: Vec<(String, String)> = Vec::new();
let mut push = |name: &str, value: &str| {
pairs.push((name.to_string(), value.to_string()));
};
match *request {
Request::Text {
title,
label,
initial,
} => {
push("ANYDIALOG_TITLE", title);
push("ANYDIALOG_LABEL", label);
push("ANYDIALOG_INITIAL", initial.unwrap_or(""));
}
Request::Choice {
title,
label,
choices,
current,
} => {
push("ANYDIALOG_TITLE", title);
push("ANYDIALOG_LABEL", label);
push(
"ANYDIALOG_CURRENT",
super::selected(choices, current).unwrap_or(""),
);
push("ANYDIALOG_CHOICE_COUNT", &choices.len().to_string());
for (index, choice) in choices.iter().enumerate() {
push(&format!("ANYDIALOG_CHOICE_{index}"), choice);
}
}
Request::Choices {
title,
label,
choices,
current,
} => {
push("ANYDIALOG_TITLE", title);
push("ANYDIALOG_LABEL", label);
push("ANYDIALOG_CHOICE_COUNT", &choices.len().to_string());
for (index, choice) in choices.iter().enumerate() {
push(&format!("ANYDIALOG_CHOICE_{index}"), choice);
}
let current = super::selected_many(choices, current);
push("ANYDIALOG_CURRENT_COUNT", ¤t.len().to_string());
for (index, choice) in current.iter().enumerate() {
push(&format!("ANYDIALOG_CURRENT_{index}"), choice);
}
}
Request::Color { title: _, initial } => {
push("ANYDIALOG_INITIAL", initial.unwrap_or("#ffffff"));
}
Request::Password { title, label } => {
push("ANYDIALOG_TITLE", title);
push("ANYDIALOG_LABEL", label);
}
Request::Message {
title,
message,
level,
} => {
push("ANYDIALOG_TITLE", title);
push("ANYDIALOG_MESSAGE", message);
push(
"ANYDIALOG_ICON",
match level {
Level::Info => "Information",
Level::Warning => "Warning",
Level::Error => "Error",
},
);
}
Request::Confirm { title, message } => {
push("ANYDIALOG_TITLE", title);
push("ANYDIALOG_MESSAGE", message);
}
Request::OpenFile {
title,
start,
filters,
multiple,
} => {
push("ANYDIALOG_TITLE", title);
push(
"ANYDIALOG_START",
&start.map(super::path_text).unwrap_or_default(),
);
push("ANYDIALOG_FILTER", &filter_text(filters));
push("ANYDIALOG_MULTIPLE", if multiple { "1" } else { "0" });
}
Request::SaveFile {
title,
start,
filters,
} => {
push("ANYDIALOG_TITLE", title);
push(
"ANYDIALOG_START",
&start.map(super::path_text).unwrap_or_default(),
);
push("ANYDIALOG_FILTER", &filter_text(filters));
}
Request::Folder { title, start } => {
push("ANYDIALOG_TITLE", title);
push(
"ANYDIALOG_START",
&start.map(super::path_text).unwrap_or_default(),
);
}
}
pairs
}
fn filter_text(filters: &[FileFilter]) -> String {
if filters.is_empty() {
return "All files (*.*)|*.*".to_string();
}
filters
.iter()
.map(|filter| {
let patterns: Vec<String> = filter
.extensions
.iter()
.map(|extension| format!("*.{extension}"))
.collect();
format!(
"{} ({})|{}",
filter.name,
patterns.join(";"),
patterns.join(";")
)
})
.collect::<Vec<_>>()
.join("|")
}
pub(crate) fn classify(exit_code: Option<i32>, stdout: &str, stderr: &str) -> Outcome {
match exit_code {
Some(0) => crate::outcome::classify(Some(0), stdout),
Some(1) if stdout.trim().is_empty() && stderr.trim().is_empty() => Outcome::Cancelled,
_ => Outcome::Failed,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn owned(items: &[&str]) -> Vec<String> {
items.iter().map(|item| (*item).to_string()).collect()
}
fn pair(name: &str, value: &str) -> (String, String) {
(name.to_string(), value.to_string())
}
#[test]
fn powershell_argv_is_flags_then_the_right_script() {
let text = Request::Text {
title: "Title",
label: "Label",
initial: None,
};
let text_argv = argv(&text);
assert_eq!(
text_argv[..5],
owned(&[
"-NoProfile",
"-STA",
"-ExecutionPolicy",
"Bypass",
"-Command"
])
);
assert_eq!(text_argv[5], TEXT_SCRIPT);
assert!(text_argv[5].contains("TextBox"));
let choice = Request::Choice {
title: "Title",
label: "Label",
choices: &["a"],
current: None,
};
assert_eq!(argv(&choice)[5], CHOICE_SCRIPT);
let password = Request::Password {
title: "Title",
label: "Label",
};
assert_eq!(argv(&password)[5], PASSWORD_SCRIPT);
assert!(argv(&password)[5].contains("UseSystemPasswordChar"));
let multiple = Request::Choices {
title: "Title",
label: "Label",
choices: &["a"],
current: &[],
};
assert_eq!(argv(&multiple)[5], MULTIPLE_SCRIPT);
assert!(argv(&multiple)[5].contains("CheckedListBox"));
let color = Request::Color {
title: "Title",
initial: None,
};
assert_eq!(argv(&color)[5], COLOR_SCRIPT);
assert!(argv(&color)[5].contains("ColorDialog"));
let message = Request::Message {
title: "Title",
message: "Message",
level: Level::Info,
};
assert_eq!(argv(&message)[5], MESSAGE_SCRIPT);
let confirm = Request::Confirm {
title: "Title",
message: "Message",
};
assert_eq!(argv(&confirm)[5], CONFIRM_SCRIPT);
assert!(argv(&confirm)[5].contains("YesNo"));
let open = Request::OpenFile {
title: "Title",
start: None,
filters: &[],
multiple: false,
};
assert_eq!(argv(&open)[5], OPEN_SCRIPT);
assert!(argv(&open)[5].contains("OpenFileDialog"));
let save = Request::SaveFile {
title: "Title",
start: None,
filters: &[],
};
assert_eq!(argv(&save)[5], SAVE_SCRIPT);
assert!(argv(&save)[5].contains("SaveFileDialog"));
let folder = Request::Folder {
title: "Title",
start: None,
};
assert_eq!(argv(&folder)[5], FOLDER_SCRIPT);
assert!(argv(&folder)[5].contains("FolderBrowserDialog"));
}
#[test]
fn powershell_env_carries_the_values() {
let text = Request::Text {
title: "Title",
label: "Label",
initial: None,
};
assert_eq!(
env(&text),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_LABEL", "Label"),
pair("ANYDIALOG_INITIAL", ""),
]
);
let choice = Request::Choice {
title: "Title",
label: "Label",
choices: &["a", "b"],
current: Some("b"),
};
assert_eq!(
env(&choice),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_LABEL", "Label"),
pair("ANYDIALOG_CURRENT", "b"),
pair("ANYDIALOG_CHOICE_COUNT", "2"),
pair("ANYDIALOG_CHOICE_0", "a"),
pair("ANYDIALOG_CHOICE_1", "b"),
]
);
let multiple = Request::Choices {
title: "Title",
label: "Label",
choices: &["a", "b"],
current: &["b", "missing"],
};
assert_eq!(
env(&multiple),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_LABEL", "Label"),
pair("ANYDIALOG_CHOICE_COUNT", "2"),
pair("ANYDIALOG_CHOICE_0", "a"),
pair("ANYDIALOG_CHOICE_1", "b"),
pair("ANYDIALOG_CURRENT_COUNT", "1"),
pair("ANYDIALOG_CURRENT_0", "b"),
]
);
let color = Request::Color {
title: "Title",
initial: None,
};
assert_eq!(env(&color), vec![pair("ANYDIALOG_INITIAL", "#ffffff")]);
let password = Request::Password {
title: "Title",
label: "Label",
};
assert_eq!(
env(&password),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_LABEL", "Label"),
]
);
let message = Request::Message {
title: "Title",
message: "Message",
level: Level::Warning,
};
assert_eq!(
env(&message),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_MESSAGE", "Message"),
pair("ANYDIALOG_ICON", "Warning"),
]
);
let confirm = Request::Confirm {
title: "Title",
message: "Message",
};
assert_eq!(
env(&confirm),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_MESSAGE", "Message"),
]
);
let filters = [FileFilter {
name: "Images",
extensions: &["png", "jpg"],
}];
let open = Request::OpenFile {
title: "Title",
start: Some(std::path::Path::new("/home/user")),
filters: &filters,
multiple: true,
};
assert_eq!(
env(&open),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_START", "/home/user"),
pair("ANYDIALOG_FILTER", "Images (*.png;*.jpg)|*.png;*.jpg"),
pair("ANYDIALOG_MULTIPLE", "1"),
]
);
let folder = Request::Folder {
title: "Title",
start: None,
};
assert_eq!(
env(&folder),
vec![
pair("ANYDIALOG_TITLE", "Title"),
pair("ANYDIALOG_START", ""),
]
);
}
#[test]
fn filter_text_falls_back_to_all_files() {
assert_eq!(filter_text(&[]), "All files (*.*)|*.*");
let filters = [
FileFilter {
name: "Images",
extensions: &["png"],
},
FileFilter {
name: "Text",
extensions: &["txt", "md"],
},
];
assert_eq!(
filter_text(&filters),
"Images (*.png)|*.png|Text (*.txt;*.md)|*.txt;*.md"
);
}
#[test]
fn powershell_classifier_wants_a_silent_exit_1_for_cancel() {
assert_eq!(
classify(Some(0), "typed\n", ""),
Outcome::Answered(Some("typed".to_string()))
);
assert_eq!(classify(Some(1), "", ""), Outcome::Cancelled);
assert_eq!(classify(Some(1), "", "parser error"), Outcome::Failed);
assert_eq!(classify(Some(2), "", "boom"), Outcome::Failed);
assert_eq!(classify(None, "", ""), Outcome::Failed);
}
}