1use std::fmt;
21use std::future::Future;
22use std::io;
23use std::path::PathBuf;
24use std::pin::Pin;
25use std::sync::{Arc, Mutex, PoisonError};
26use std::task::{Context, Poll, Waker};
27
28use crate::ViewId;
29
30pub trait FileDialogs {
33 fn open_file(
35 &self,
36 parent: Option<ViewId>,
37 options: OpenOptions,
38 ) -> FileDialogFuture<Option<PickedFile>>;
39
40 fn open_files(
42 &self,
43 parent: Option<ViewId>,
44 options: OpenOptions,
45 ) -> FileDialogFuture<Vec<PickedFile>>;
46
47 fn open_folder(
49 &self,
50 parent: Option<ViewId>,
51 options: FolderOptions,
52 ) -> FileDialogFuture<Option<PickedFolder>>;
53
54 fn save_file(
56 &self,
57 parent: Option<ViewId>,
58 options: SaveOptions,
59 ) -> FileDialogFuture<Option<PickedFile>>;
60}
61
62pub type FileDialogFuture<T> = Pin<Box<dyn Future<Output = Result<T, FileDialogError>>>>;
64
65#[derive(Clone, Debug, PartialEq, Eq)]
67pub enum FileDialogError {
68 Failed(String),
70}
71
72impl fmt::Display for FileDialogError {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 FileDialogError::Failed(reason) => write!(f, "the file dialog failed: {reason}"),
76 }
77 }
78}
79
80impl std::error::Error for FileDialogError {}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub struct FileType {
86 pub label: String,
87 pub extensions: Vec<String>,
89 pub mime_types: Vec<String>,
90}
91
92#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct OpenOptions {
95 pub types: Vec<FileType>,
97 pub initial_folder: Option<PathBuf>,
99 pub confirm_label: Option<String>,
101 pub writable: bool,
104}
105
106#[derive(Clone, Debug, Default, PartialEq, Eq)]
108pub struct FolderOptions {
109 pub initial_folder: Option<PathBuf>,
110 pub confirm_label: Option<String>,
111 pub writable: bool,
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
117pub struct SaveOptions {
118 pub types: Vec<FileType>,
119 pub initial_folder: Option<PathBuf>,
120 pub suggested_name: Option<String>,
122 pub confirm_label: Option<String>,
123}
124
125pub enum PickedFile {
127 Path(PathBuf),
129 Handle(Box<dyn FileHandle>),
131 Stream(Box<dyn FileStream>),
133}
134
135impl fmt::Debug for PickedFile {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 PickedFile::Path(path) => f.debug_tuple("Path").field(path).finish(),
139 PickedFile::Handle(handle) => f.debug_tuple("Handle").field(&handle.name()).finish(),
140 PickedFile::Stream(stream) => f.debug_tuple("Stream").field(&stream.name()).finish(),
141 }
142 }
143}
144
145pub enum PickedFolder {
147 Path(PathBuf),
149 Handle(Box<dyn FolderHandle>),
151}
152
153impl fmt::Debug for PickedFolder {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 PickedFolder::Path(path) => f.debug_tuple("Path").field(path).finish(),
157 PickedFolder::Handle(handle) => f.debug_tuple("Handle").field(&handle.name()).finish(),
158 }
159 }
160}
161
162pub trait FolderHandle {
165 fn name(&self) -> &str;
167
168 fn entries(&self) -> FolderEntriesFuture;
170
171 fn open_file(&self, name: &str) -> FileOpenFuture<PickedFile>;
173
174 fn open_folder(&self, name: &str) -> FileOpenFuture<PickedFolder>;
176
177 fn create_file(&self, name: &str) -> Option<FileOpenFuture<PickedFile>>;
179
180 fn create_folder(&self, name: &str) -> Option<FileOpenFuture<PickedFolder>>;
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct FolderEntry {
187 pub name: String,
188 pub kind: EntryKind,
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub enum EntryKind {
194 File,
195 Folder,
196}
197
198pub type FolderEntriesFuture = Pin<Box<dyn Future<Output = io::Result<Vec<FolderEntry>>>>>;
200
201pub trait FileHandle {
203 fn name(&self) -> &str;
205
206 fn url(&self) -> String;
208
209 fn size(&self) -> FileSizeFuture;
210
211 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> FileReadFuture<'a>;
213
214 fn open_write(&self) -> Option<FileOpenFuture<Box<dyn FileWriter>>>;
216}
217
218pub trait FileWriter {
220 fn write_at<'a>(&'a mut self, offset: u64, bytes: &'a [u8]) -> FileWriteFuture<'a>;
221
222 fn set_len(&mut self, len: u64) -> FileWriteFuture<'_>;
223
224 fn finish(self: Box<Self>) -> FileFinishFuture;
227}
228
229pub trait FileStream {
231 fn name(&self) -> &str;
232
233 fn url(&self) -> String;
235
236 fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> FileReadFuture<'a>;
238}
239
240pub type FileSizeFuture = Pin<Box<dyn Future<Output = io::Result<u64>>>>;
241pub type FileReadFuture<'a> = Pin<Box<dyn Future<Output = io::Result<usize>> + 'a>>;
242pub type FileOpenFuture<T> = Pin<Box<dyn Future<Output = io::Result<T>>>>;
243pub type FileWriteFuture<'a> = Pin<Box<dyn Future<Output = io::Result<()>> + 'a>>;
244pub type FileFinishFuture = Pin<Box<dyn Future<Output = io::Result<()>>>>;
245
246const READ_CHUNK: usize = 64 * 1024;
248
249impl PickedFile {
250 pub fn url(&self) -> String {
256 match self {
257 PickedFile::Path(path) => crate::opener::file_uri(path),
258 PickedFile::Handle(handle) => handle.url(),
259 PickedFile::Stream(stream) => stream.url(),
260 }
261 }
262
263 pub fn read_all(&mut self) -> Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + '_>> {
266 match self {
267 PickedFile::Path(path) => Box::pin(read_path(path.clone())),
268 PickedFile::Handle(handle) => Box::pin(read_handle(&**handle)),
269 PickedFile::Stream(stream) => Box::pin(read_stream(&mut **stream)),
270 }
271 }
272}
273
274async fn read_path(path: PathBuf) -> io::Result<Vec<u8>> {
275 let (answer, read) = one_shot();
276 std::thread::Builder::new()
277 .name("inset-read-picked-file".to_owned())
278 .spawn(move || answer.give(std::fs::read(path)))?;
279 read.await
280}
281
282async fn read_handle(handle: &dyn FileHandle) -> io::Result<Vec<u8>> {
283 let mut bytes = Vec::with_capacity(usize::try_from(handle.size().await?).unwrap_or(0));
284 let mut chunk = vec![0; READ_CHUNK];
285 loop {
286 let read = handle.read_at(bytes.len() as u64, &mut chunk).await?;
287 if read == 0 {
288 return Ok(bytes);
289 }
290 bytes.extend_from_slice(&chunk[..read]);
291 }
292}
293
294async fn read_stream(stream: &mut dyn FileStream) -> io::Result<Vec<u8>> {
295 let mut bytes = Vec::new();
296 let mut chunk = vec![0; READ_CHUNK];
297 loop {
298 let read = stream.read(&mut chunk).await?;
299 if read == 0 {
300 return Ok(bytes);
301 }
302 bytes.extend_from_slice(&chunk[..read]);
303 }
304}
305
306fn one_shot<T>() -> (Answer<T>, Answered<T>) {
308 let shared = Arc::new(Mutex::new((None, None)));
309 (Answer(Arc::clone(&shared)), Answered(shared))
310}
311
312type Shared<T> = Arc<Mutex<(Option<io::Result<T>>, Option<Waker>)>>;
313
314struct Answer<T>(Shared<T>);
315
316impl<T> Answer<T> {
317 fn give(self, value: io::Result<T>) {
318 let waker = {
319 let mut shared = self.0.lock().unwrap_or_else(PoisonError::into_inner);
320 shared.0 = Some(value);
321 shared.1.take()
322 };
323 if let Some(waker) = waker {
324 waker.wake();
325 }
326 }
327}
328
329struct Answered<T>(Shared<T>);
330
331impl<T> Future for Answered<T> {
332 type Output = io::Result<T>;
333
334 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<T>> {
335 let mut shared = self.0.lock().unwrap_or_else(PoisonError::into_inner);
336 match shared.0.take() {
337 Some(value) => Poll::Ready(value),
338 None if Arc::strong_count(&self.0) == 1 => {
339 Poll::Ready(Err(io::Error::other("the read ended without an answer")))
340 }
341 None => {
342 shared.1 = Some(cx.waker().clone());
343 Poll::Pending
344 }
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use std::cell::RefCell;
352
353 use super::*;
354
355 struct Bytes(RefCell<Vec<u8>>);
357
358 impl FileHandle for Bytes {
359 fn name(&self) -> &str {
360 "bytes"
361 }
362
363 fn url(&self) -> String {
364 "memory:bytes".to_owned()
365 }
366
367 fn size(&self) -> FileSizeFuture {
368 let size = self.0.borrow().len() as u64;
369 Box::pin(async move { Ok(size) })
370 }
371
372 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> FileReadFuture<'a> {
373 let bytes = self.0.borrow();
374 let start = (offset as usize).min(bytes.len());
375 let read = (bytes.len() - start).min(buf.len()).min(3);
376 buf[..read].copy_from_slice(&bytes[start..start + read]);
377 Box::pin(async move { Ok(read) })
378 }
379
380 fn open_write(&self) -> Option<FileOpenFuture<Box<dyn FileWriter>>> {
381 None
382 }
383 }
384
385 fn now<T>(future: impl Future<Output = T>) -> T {
386 let mut future = std::pin::pin!(future);
387 let mut context = Context::from_waker(Waker::noop());
388 loop {
389 if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
390 return output;
391 }
392 std::thread::yield_now();
393 }
394 }
395
396 #[test]
397 fn a_handle_answers_the_url_it_holds() {
398 let picked = PickedFile::Handle(Box::new(Bytes(RefCell::new(Vec::new()))));
399 assert_eq!(picked.url(), "memory:bytes");
400 }
401
402 #[test]
403 fn a_handle_is_read_whole_in_pieces() {
404 let mut picked = PickedFile::Handle(Box::new(Bytes(RefCell::new(b"hello, file".to_vec()))));
405 assert_eq!(now(picked.read_all()).unwrap(), b"hello, file");
406 }
407
408 #[test]
409 fn a_path_is_read_whole_on_a_thread() {
410 let path = std::env::temp_dir().join(format!("inset-read-all-{}", std::process::id()));
411 std::fs::write(&path, b"on disk").unwrap();
412 let mut picked = PickedFile::Path(path.clone());
413 assert_eq!(now(picked.read_all()).unwrap(), b"on disk");
414 std::fs::remove_file(path).unwrap();
415 let mut missing = PickedFile::Path(PathBuf::from("/no/such/inset/file"));
416 assert!(now(missing.read_all()).is_err());
417 }
418}