1use std::future::Future;
12use std::path::{Path, PathBuf};
13use std::pin::Pin;
14
15use gpui_kit::{App, PathPromptOptions};
16
17pub fn home_relative(path: &Path) -> String {
22 let Some(home) = dirs::home_dir() else {
23 return path.display().to_string();
24 };
25 match path.strip_prefix(&home) {
26 Ok(rest) if rest.as_os_str().is_empty() => "~".to_owned(),
27 Ok(rest) => format!("~{}{}", std::path::MAIN_SEPARATOR, rest.display()),
28 Err(_) => path.display().to_string(),
29 }
30}
31
32pub fn expand_home(value: &str) -> PathBuf {
34 let value = value.trim();
35 if value == "~" {
36 return dirs::home_dir().unwrap_or_else(|| PathBuf::from(value));
37 }
38 if let Some(rest) = value
39 .strip_prefix("~/")
40 .or_else(|| value.strip_prefix("~\\"))
41 && let Some(home) = dirs::home_dir()
42 {
43 return home.join(rest);
44 }
45 PathBuf::from(value)
46}
47
48pub(crate) fn start_directory(current: &str) -> Option<PathBuf> {
54 let path = expand_home(current);
55 if path.as_os_str().is_empty() {
56 return std::env::current_dir().ok().or_else(dirs::home_dir);
57 }
58 if path.is_dir() {
59 return Some(path);
60 }
61 path.parent()
62 .filter(|parent| parent.is_dir())
63 .map(Path::to_path_buf)
64 .or_else(|| std::env::current_dir().ok())
65 .or_else(dirs::home_dir)
66}
67
68pub type PathPrompt = Pin<Box<dyn Future<Output = Result<Option<Vec<PathBuf>>, String>> + 'static>>;
70
71pub fn prompt_for_paths_at(
77 app: &App,
78 start: Option<PathBuf>,
79 options: PathPromptOptions,
80) -> PathPrompt {
81 #[cfg(target_os = "linux")]
82 {
83 let _ = app;
84 Box::pin(portal::prompt(start, options))
85 }
86 #[cfg(not(target_os = "linux"))]
87 {
88 let _ = start;
92 let receiver = app.prompt_for_paths(options);
93 Box::pin(async move {
94 match receiver.await {
95 Ok(Ok(paths)) => Ok(paths),
96 Ok(Err(error)) => Err(format!("{error:#}")),
97 Err(_) => Err("the file dialog stopped without a result".to_owned()),
98 }
99 })
100 }
101}
102
103#[cfg(target_os = "linux")]
104mod portal {
105 use super::*;
106 use ashpd::desktop::file_chooser::OpenFileRequest;
107
108 pub(super) async fn prompt(
109 start: Option<PathBuf>,
110 options: PathPromptOptions,
111 ) -> Result<Option<Vec<PathBuf>>, String> {
112 let title = if options.directories {
113 "Open Folder"
114 } else {
115 "Open File"
116 };
117 let mut request = OpenFileRequest::default()
118 .modal(true)
119 .title(title)
120 .multiple(options.multiple)
121 .directory(options.directories);
122 if let Some(start) = start {
123 request = request
124 .current_folder(start)
125 .map_err(|error| format!("invalid start folder: {error}"))?;
126 }
127 let selected = request
128 .send()
129 .await
130 .map_err(|error| format!("cannot open the file dialog: {error}"))?
131 .response()
132 .map_err(|error| format!("the file dialog failed: {error}"))?;
133 let paths: Vec<PathBuf> = selected
134 .uris()
135 .iter()
136 .filter_map(|uri| url::Url::parse(uri.as_str()).ok())
137 .filter_map(|url| url.to_file_path().ok())
138 .collect();
139 Ok((!paths.is_empty()).then_some(paths))
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn home_paths_are_shown_with_a_tilde() {
149 let Some(home) = dirs::home_dir() else {
150 return;
151 };
152 assert_eq!(home_relative(&home), "~");
153 assert_eq!(
154 home_relative(&home.join("paper")),
155 format!("~{}{}", std::path::MAIN_SEPARATOR, "paper")
156 );
157 assert_eq!(expand_home("~/paper"), home.join("paper"));
158 assert_eq!(expand_home("~"), home);
159 assert_eq!(
160 expand_home("/outside/paper"),
161 PathBuf::from("/outside/paper")
162 );
163 }
164
165 #[test]
166 fn a_browse_dialog_starts_at_the_current_path() {
167 let dir = std::env::temp_dir().join(format!(
168 "ragrig-gpui-paths-{}-{}",
169 std::process::id(),
170 line!()
171 ));
172 std::fs::create_dir_all(&dir).expect("temp dir");
173 assert_eq!(
174 start_directory(&dir.display().to_string()),
175 Some(dir.clone())
176 );
177
178 let file = dir.join("paper.md");
179 std::fs::write(&file, "# Paper").expect("paper");
180 assert_eq!(
181 start_directory(&file.display().to_string()),
182 Some(dir.clone())
183 );
184
185 let _ = std::fs::remove_dir_all(dir);
186 }
187}