use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use gpui_kit::{App, PathPromptOptions};
pub fn home_relative(path: &Path) -> String {
let Some(home) = dirs::home_dir() else {
return path.display().to_string();
};
match path.strip_prefix(&home) {
Ok(rest) if rest.as_os_str().is_empty() => "~".to_owned(),
Ok(rest) => format!("~{}{}", std::path::MAIN_SEPARATOR, rest.display()),
Err(_) => path.display().to_string(),
}
}
pub fn expand_home(value: &str) -> PathBuf {
let value = value.trim();
if value == "~" {
return dirs::home_dir().unwrap_or_else(|| PathBuf::from(value));
}
if let Some(rest) = value
.strip_prefix("~/")
.or_else(|| value.strip_prefix("~\\"))
&& let Some(home) = dirs::home_dir()
{
return home.join(rest);
}
PathBuf::from(value)
}
pub(crate) fn start_directory(current: &str) -> Option<PathBuf> {
let path = expand_home(current);
if path.as_os_str().is_empty() {
return std::env::current_dir().ok().or_else(dirs::home_dir);
}
if path.is_dir() {
return Some(path);
}
path.parent()
.filter(|parent| parent.is_dir())
.map(Path::to_path_buf)
.or_else(|| std::env::current_dir().ok())
.or_else(dirs::home_dir)
}
pub type PathPrompt = Pin<Box<dyn Future<Output = Result<Option<Vec<PathBuf>>, String>> + 'static>>;
pub fn prompt_for_paths_at(
app: &App,
start: Option<PathBuf>,
options: PathPromptOptions,
) -> PathPrompt {
#[cfg(target_os = "linux")]
{
let _ = app;
Box::pin(portal::prompt(start, options))
}
#[cfg(not(target_os = "linux"))]
{
let _ = start;
let receiver = app.prompt_for_paths(options);
Box::pin(async move {
match receiver.await {
Ok(Ok(paths)) => Ok(paths),
Ok(Err(error)) => Err(format!("{error:#}")),
Err(_) => Err("the file dialog stopped without a result".to_owned()),
}
})
}
}
#[cfg(target_os = "linux")]
mod portal {
use super::*;
use ashpd::desktop::file_chooser::OpenFileRequest;
pub(super) async fn prompt(
start: Option<PathBuf>,
options: PathPromptOptions,
) -> Result<Option<Vec<PathBuf>>, String> {
let title = if options.directories {
"Open Folder"
} else {
"Open File"
};
let mut request = OpenFileRequest::default()
.modal(true)
.title(title)
.multiple(options.multiple)
.directory(options.directories);
if let Some(start) = start {
request = request
.current_folder(start)
.map_err(|error| format!("invalid start folder: {error}"))?;
}
let selected = request
.send()
.await
.map_err(|error| format!("cannot open the file dialog: {error}"))?
.response()
.map_err(|error| format!("the file dialog failed: {error}"))?;
let paths: Vec<PathBuf> = selected
.uris()
.iter()
.filter_map(|uri| url::Url::parse(uri.as_str()).ok())
.filter_map(|url| url.to_file_path().ok())
.collect();
Ok((!paths.is_empty()).then_some(paths))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn home_paths_are_shown_with_a_tilde() {
let Some(home) = dirs::home_dir() else {
return;
};
assert_eq!(home_relative(&home), "~");
assert_eq!(
home_relative(&home.join("paper")),
format!("~{}{}", std::path::MAIN_SEPARATOR, "paper")
);
assert_eq!(expand_home("~/paper"), home.join("paper"));
assert_eq!(expand_home("~"), home);
assert_eq!(
expand_home("/outside/paper"),
PathBuf::from("/outside/paper")
);
}
#[test]
fn a_browse_dialog_starts_at_the_current_path() {
let dir = std::env::temp_dir().join(format!(
"ragrig-gpui-paths-{}-{}",
std::process::id(),
line!()
));
std::fs::create_dir_all(&dir).expect("temp dir");
assert_eq!(
start_directory(&dir.display().to_string()),
Some(dir.clone())
);
let file = dir.join("paper.md");
std::fs::write(&file, "# Paper").expect("paper");
assert_eq!(
start_directory(&file.display().to_string()),
Some(dir.clone())
);
let _ = std::fs::remove_dir_all(dir);
}
}