use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, TryRecvError};
#[cfg_attr(test, allow(dead_code))]
pub enum PickKind {
File {
filters: Vec<(String, Vec<String>)>,
},
Folder,
Save {
default_name: String,
filters: Vec<(String, Vec<String>)>,
},
}
pub struct PendingPick<A> {
rx: Receiver<Option<PathBuf>>,
action: Option<A>,
}
impl<A> PendingPick<A> {
#[cfg(test)]
pub fn action(&self) -> Option<&A> {
self.action.as_ref()
}
pub fn take(&mut self) -> Option<(A, Option<PathBuf>)> {
match self.rx.try_recv() {
Ok(path) => Some((
self.action.take()?,
path.map(crate::shared_utils::file_path),
)),
Err(TryRecvError::Disconnected) => Some((self.action.take()?, None)),
Err(TryRecvError::Empty) => None,
}
}
}
pub fn spawn<A>(kind: PickKind, title: &str, dir: Option<&Path>, action: A) -> PendingPick<A> {
let (tx, rx) = std::sync::mpsc::channel();
#[cfg(test)]
{
let _ = (kind, title, dir);
drop(tx);
}
#[cfg(not(test))]
spawn_dialog(kind, title.to_string(), dir.map(Path::to_path_buf), tx);
PendingPick {
rx,
action: Some(action),
}
}
#[cfg(test)]
pub fn resolved<A>(action: A, path: Option<PathBuf>) -> PendingPick<A> {
let (tx, rx) = std::sync::mpsc::channel();
let _ = tx.send(path);
PendingPick {
rx,
action: Some(action),
}
}
#[cfg(not(test))]
fn spawn_dialog(
kind: PickKind,
title: String,
dir: Option<PathBuf>,
tx: std::sync::mpsc::Sender<Option<PathBuf>>,
) {
std::thread::spawn(move || {
let picked = match kind {
PickKind::File { filters } => {
with_owned_filters(base(&title, dir.as_deref()), &filters).pick_file()
}
PickKind::Folder => base(&title, dir.as_deref()).pick_folder(),
PickKind::Save {
default_name,
filters,
} => {
let d = with_owned_filters(base(&title, dir.as_deref()), &filters);
let d = if default_name.is_empty() {
d
} else {
d.set_file_name(default_name)
};
d.save_file()
}
};
let _ = tx.send(picked);
});
}
pub fn owned_filters(filters: &[Filter]) -> Vec<(String, Vec<String>)> {
filters
.iter()
.map(|(n, e)| {
(
(*n).to_string(),
e.iter().map(|x| (*x).to_string()).collect(),
)
})
.collect()
}
#[cfg_attr(test, allow(dead_code))]
fn with_owned_filters(
mut d: rfd::FileDialog,
filters: &[(String, Vec<String>)],
) -> rfd::FileDialog {
for (name, exts) in filters {
if exts.len() == 1 && exts[0] == "*" {
continue; }
d = d.add_filter(name, exts);
}
d
}
pub type Filter<'a> = (&'a str, &'a [&'a str]);
#[cfg_attr(test, allow(dead_code))]
fn base(title: &str, dir: Option<&Path>) -> rfd::FileDialog {
let mut d = rfd::FileDialog::new().set_title(title);
if let Some(dir) = dir.filter(|d| d.is_dir()) {
d = d.set_directory(dir);
}
d
}
pub fn error_alert(title: &str, message: &str) {
#[cfg(test)]
{
let _ = (title, message);
return;
}
#[cfg(not(test))]
rfd::MessageDialog::new()
.set_level(rfd::MessageLevel::Error)
.set_title(title)
.set_description(message)
.show();
}
pub fn seed_dir(current: &str) -> Option<PathBuf> {
if current.is_empty() {
return None;
}
let p = PathBuf::from(current);
if p.is_dir() {
Some(p)
} else {
p.parent().map(Path::to_path_buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pending<A>(action: A) -> (std::sync::mpsc::Sender<Option<PathBuf>>, PendingPick<A>) {
let (tx, rx) = std::sync::mpsc::channel();
(
tx,
PendingPick {
rx,
action: Some(action),
},
)
}
#[test]
fn an_open_dialog_reports_nothing_and_keeps_its_action() {
let (_tx, mut p) = pending("open");
assert!(p.take().is_none());
assert!(p.take().is_none(), "and stays pollable");
}
#[test]
fn a_chosen_path_arrives_with_the_action_that_asked_for_it() {
let (tx, mut p) = pending("open");
tx.send(Some(PathBuf::from("/tmp/a.hurl"))).unwrap();
let (action, path) = p.take().expect("resolved");
assert_eq!(action, "open");
assert_eq!(path, Some(PathBuf::from("/tmp/a.hurl")));
}
#[test]
fn a_cancel_is_reported_as_a_resolved_dialog_with_no_path() {
let (tx, mut p) = pending("save");
tx.send(None).unwrap();
let (action, path) = p.take().expect("resolved");
assert_eq!(action, "save");
assert_eq!(path, None);
}
#[test]
fn a_lost_dialog_resolves_like_a_cancel_rather_than_hanging() {
let (tx, mut p) = pending("open");
drop(tx);
let (_, path) = p.take().expect("resolved rather than pending forever");
assert_eq!(path, None);
}
}