use std::fmt;
use std::future::Future;
use std::io;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::{Arc, Mutex, PoisonError};
use std::task::{Context, Poll, Waker};
use crate::ViewId;
pub trait FileDialogs {
fn open_file(
&self,
parent: Option<ViewId>,
options: OpenOptions,
) -> FileDialogFuture<Option<PickedFile>>;
fn open_files(
&self,
parent: Option<ViewId>,
options: OpenOptions,
) -> FileDialogFuture<Vec<PickedFile>>;
fn open_folder(
&self,
parent: Option<ViewId>,
options: FolderOptions,
) -> FileDialogFuture<Option<PickedFolder>>;
fn save_file(
&self,
parent: Option<ViewId>,
options: SaveOptions,
) -> FileDialogFuture<Option<PickedFile>>;
}
pub type FileDialogFuture<T> = Pin<Box<dyn Future<Output = Result<T, FileDialogError>>>>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FileDialogError {
Failed(String),
}
impl fmt::Display for FileDialogError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileDialogError::Failed(reason) => write!(f, "the file dialog failed: {reason}"),
}
}
}
impl std::error::Error for FileDialogError {}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FileType {
pub label: String,
pub extensions: Vec<String>,
pub mime_types: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct OpenOptions {
pub types: Vec<FileType>,
pub initial_folder: Option<PathBuf>,
pub confirm_label: Option<String>,
pub writable: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FolderOptions {
pub initial_folder: Option<PathBuf>,
pub confirm_label: Option<String>,
pub writable: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SaveOptions {
pub types: Vec<FileType>,
pub initial_folder: Option<PathBuf>,
pub suggested_name: Option<String>,
pub confirm_label: Option<String>,
}
pub enum PickedFile {
Path(PathBuf),
Handle(Box<dyn FileHandle>),
Stream(Box<dyn FileStream>),
}
impl fmt::Debug for PickedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PickedFile::Path(path) => f.debug_tuple("Path").field(path).finish(),
PickedFile::Handle(handle) => f.debug_tuple("Handle").field(&handle.name()).finish(),
PickedFile::Stream(stream) => f.debug_tuple("Stream").field(&stream.name()).finish(),
}
}
}
pub enum PickedFolder {
Path(PathBuf),
Handle(Box<dyn FolderHandle>),
}
impl fmt::Debug for PickedFolder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PickedFolder::Path(path) => f.debug_tuple("Path").field(path).finish(),
PickedFolder::Handle(handle) => f.debug_tuple("Handle").field(&handle.name()).finish(),
}
}
}
pub trait FolderHandle {
fn name(&self) -> &str;
fn entries(&self) -> FolderEntriesFuture;
fn open_file(&self, name: &str) -> FileOpenFuture<PickedFile>;
fn open_folder(&self, name: &str) -> FileOpenFuture<PickedFolder>;
fn create_file(&self, name: &str) -> Option<FileOpenFuture<PickedFile>>;
fn create_folder(&self, name: &str) -> Option<FileOpenFuture<PickedFolder>>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FolderEntry {
pub name: String,
pub kind: EntryKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntryKind {
File,
Folder,
}
pub type FolderEntriesFuture = Pin<Box<dyn Future<Output = io::Result<Vec<FolderEntry>>>>>;
pub trait FileHandle {
fn name(&self) -> &str;
fn url(&self) -> String;
fn size(&self) -> FileSizeFuture;
fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> FileReadFuture<'a>;
fn open_write(&self) -> Option<FileOpenFuture<Box<dyn FileWriter>>>;
}
pub trait FileWriter {
fn write_at<'a>(&'a mut self, offset: u64, bytes: &'a [u8]) -> FileWriteFuture<'a>;
fn set_len(&mut self, len: u64) -> FileWriteFuture<'_>;
fn finish(self: Box<Self>) -> FileFinishFuture;
}
pub trait FileStream {
fn name(&self) -> &str;
fn url(&self) -> String;
fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> FileReadFuture<'a>;
}
pub type FileSizeFuture = Pin<Box<dyn Future<Output = io::Result<u64>>>>;
pub type FileReadFuture<'a> = Pin<Box<dyn Future<Output = io::Result<usize>> + 'a>>;
pub type FileOpenFuture<T> = Pin<Box<dyn Future<Output = io::Result<T>>>>;
pub type FileWriteFuture<'a> = Pin<Box<dyn Future<Output = io::Result<()>> + 'a>>;
pub type FileFinishFuture = Pin<Box<dyn Future<Output = io::Result<()>>>>;
const READ_CHUNK: usize = 64 * 1024;
impl PickedFile {
pub fn url(&self) -> String {
match self {
PickedFile::Path(path) => crate::opener::file_uri(path),
PickedFile::Handle(handle) => handle.url(),
PickedFile::Stream(stream) => stream.url(),
}
}
pub fn read_all(&mut self) -> Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + '_>> {
match self {
PickedFile::Path(path) => Box::pin(read_path(path.clone())),
PickedFile::Handle(handle) => Box::pin(read_handle(&**handle)),
PickedFile::Stream(stream) => Box::pin(read_stream(&mut **stream)),
}
}
}
async fn read_path(path: PathBuf) -> io::Result<Vec<u8>> {
let (answer, read) = one_shot();
std::thread::Builder::new()
.name("inset-read-picked-file".to_owned())
.spawn(move || answer.give(std::fs::read(path)))?;
read.await
}
async fn read_handle(handle: &dyn FileHandle) -> io::Result<Vec<u8>> {
let mut bytes = Vec::with_capacity(usize::try_from(handle.size().await?).unwrap_or(0));
let mut chunk = vec![0; READ_CHUNK];
loop {
let read = handle.read_at(bytes.len() as u64, &mut chunk).await?;
if read == 0 {
return Ok(bytes);
}
bytes.extend_from_slice(&chunk[..read]);
}
}
async fn read_stream(stream: &mut dyn FileStream) -> io::Result<Vec<u8>> {
let mut bytes = Vec::new();
let mut chunk = vec![0; READ_CHUNK];
loop {
let read = stream.read(&mut chunk).await?;
if read == 0 {
return Ok(bytes);
}
bytes.extend_from_slice(&chunk[..read]);
}
}
fn one_shot<T>() -> (Answer<T>, Answered<T>) {
let shared = Arc::new(Mutex::new((None, None)));
(Answer(Arc::clone(&shared)), Answered(shared))
}
type Shared<T> = Arc<Mutex<(Option<io::Result<T>>, Option<Waker>)>>;
struct Answer<T>(Shared<T>);
impl<T> Answer<T> {
fn give(self, value: io::Result<T>) {
let waker = {
let mut shared = self.0.lock().unwrap_or_else(PoisonError::into_inner);
shared.0 = Some(value);
shared.1.take()
};
if let Some(waker) = waker {
waker.wake();
}
}
}
struct Answered<T>(Shared<T>);
impl<T> Future for Answered<T> {
type Output = io::Result<T>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<T>> {
let mut shared = self.0.lock().unwrap_or_else(PoisonError::into_inner);
match shared.0.take() {
Some(value) => Poll::Ready(value),
None if Arc::strong_count(&self.0) == 1 => {
Poll::Ready(Err(io::Error::other("the read ended without an answer")))
}
None => {
shared.1 = Some(cx.waker().clone());
Poll::Pending
}
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
struct Bytes(RefCell<Vec<u8>>);
impl FileHandle for Bytes {
fn name(&self) -> &str {
"bytes"
}
fn url(&self) -> String {
"memory:bytes".to_owned()
}
fn size(&self) -> FileSizeFuture {
let size = self.0.borrow().len() as u64;
Box::pin(async move { Ok(size) })
}
fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> FileReadFuture<'a> {
let bytes = self.0.borrow();
let start = (offset as usize).min(bytes.len());
let read = (bytes.len() - start).min(buf.len()).min(3);
buf[..read].copy_from_slice(&bytes[start..start + read]);
Box::pin(async move { Ok(read) })
}
fn open_write(&self) -> Option<FileOpenFuture<Box<dyn FileWriter>>> {
None
}
}
fn now<T>(future: impl Future<Output = T>) -> T {
let mut future = std::pin::pin!(future);
let mut context = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
return output;
}
std::thread::yield_now();
}
}
#[test]
fn a_handle_answers_the_url_it_holds() {
let picked = PickedFile::Handle(Box::new(Bytes(RefCell::new(Vec::new()))));
assert_eq!(picked.url(), "memory:bytes");
}
#[test]
fn a_handle_is_read_whole_in_pieces() {
let mut picked = PickedFile::Handle(Box::new(Bytes(RefCell::new(b"hello, file".to_vec()))));
assert_eq!(now(picked.read_all()).unwrap(), b"hello, file");
}
#[test]
fn a_path_is_read_whole_on_a_thread() {
let path = std::env::temp_dir().join(format!("inset-read-all-{}", std::process::id()));
std::fs::write(&path, b"on disk").unwrap();
let mut picked = PickedFile::Path(path.clone());
assert_eq!(now(picked.read_all()).unwrap(), b"on disk");
std::fs::remove_file(path).unwrap();
let mut missing = PickedFile::Path(PathBuf::from("/no/such/inset/file"));
assert!(now(missing.read_all()).is_err());
}
}