Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 6.9 kB View raw
1use std::path::PathBuf; 2use std::sync::mpsc::{Receiver, TryRecvError}; 3 4use bone_ui::widgets::FilePickerMode; 5 6use crate::file_menu::FileKind; 7 8#[derive(Debug)] 9pub enum NativeOutcome { 10 Path(PathBuf), 11 Cancelled, 12 Error(String), 13} 14 15#[derive(Debug)] 16pub enum SpawnError { 17 Unsupported, 18} 19 20pub struct PendingHandle { 21 rx: Receiver<NativeOutcome>, 22 pub mode: FilePickerMode, 23 pub kind: FileKind, 24} 25 26impl PendingHandle { 27 pub fn poll(&self) -> std::task::Poll<NativeOutcome> { 28 match self.rx.try_recv() { 29 Ok(outcome) => std::task::Poll::Ready(outcome), 30 Err(TryRecvError::Empty) => std::task::Poll::Pending, 31 Err(TryRecvError::Disconnected) => std::task::Poll::Ready(NativeOutcome::Error( 32 "picker worker disconnected".to_owned(), 33 )), 34 } 35 } 36} 37 38#[derive(Copy, Clone)] 39pub struct Request<'a> { 40 pub mode: FilePickerMode, 41 pub kind: FileKind, 42 pub title: &'a str, 43 pub accept_label: &'a str, 44 pub seed_filename: Option<&'a str>, 45 pub current_folder: Option<&'a std::path::Path>, 46} 47 48#[cfg(target_os = "linux")] 49pub fn spawn(req: Request<'_>) -> Result<PendingHandle, SpawnError> { 50 linux_impl::spawn(req) 51} 52 53#[cfg(not(target_os = "linux"))] 54pub fn spawn(_req: Request<'_>) -> Result<PendingHandle, SpawnError> { 55 Err(SpawnError::Unsupported) 56} 57 58#[cfg(target_os = "linux")] 59mod linux_impl { 60 use std::path::PathBuf; 61 use std::sync::mpsc; 62 63 use ashpd::desktop::file_chooser::{FileFilter, OpenFileRequest, SaveFileRequest}; 64 use bone_ui::widgets::FilePickerMode; 65 66 use super::{FileKind, NativeOutcome, PendingHandle, Request, SpawnError}; 67 68 pub(super) fn spawn(req: Request<'_>) -> Result<PendingHandle, SpawnError> { 69 let mode = req.mode; 70 let kind = req.kind; 71 let title = req.title.to_owned(); 72 let accept = req.accept_label.to_owned(); 73 let seed = req.seed_filename.map(str::to_owned); 74 let folder = req.current_folder.map(PathBuf::from); 75 let (tx, rx) = mpsc::channel(); 76 let join = std::thread::Builder::new() 77 .name("bone-native-picker".to_owned()) 78 .spawn(move || { 79 let outcome = pollster::block_on(run(mode, kind, &title, &accept, seed, folder)); 80 let _ = tx.send(outcome); 81 }); 82 match join { 83 Ok(_handle) => Ok(PendingHandle { rx, mode, kind }), 84 Err(_) => Err(SpawnError::Unsupported), 85 } 86 } 87 88 async fn run( 89 mode: FilePickerMode, 90 kind: FileKind, 91 title: &str, 92 accept: &str, 93 seed: Option<String>, 94 folder: Option<PathBuf>, 95 ) -> NativeOutcome { 96 match mode { 97 FilePickerMode::Open => run_open(kind, title, accept, folder).await, 98 FilePickerMode::Save => run_save(kind, title, accept, seed, folder).await, 99 } 100 } 101 102 fn step_filter() -> FileFilter { 103 FileFilter::new("STEP") 104 .glob("*.step") 105 .glob("*.stp") 106 .glob("*.STEP") 107 .glob("*.STP") 108 } 109 110 async fn run_open( 111 kind: FileKind, 112 title: &str, 113 accept: &str, 114 folder: Option<PathBuf>, 115 ) -> NativeOutcome { 116 let mut req = OpenFileRequest::default() 117 .title(title) 118 .accept_label(accept) 119 .modal(true) 120 .multiple(false) 121 .directory(matches!(kind, FileKind::Document)); 122 if matches!(kind, FileKind::Step) { 123 req = req.filter(step_filter()); 124 } 125 if let Some(f) = folder { 126 req = match req.current_folder::<PathBuf>(Some(f)) { 127 Ok(r) => r, 128 Err(e) => return NativeOutcome::Error(e.to_string()), 129 }; 130 } 131 let response = match req.send().await { 132 Ok(r) => r, 133 Err(e) => return classify_send_error(&e), 134 }; 135 let selected = match response.response() { 136 Ok(s) => s, 137 Err(e) => return classify_response_error(&e), 138 }; 139 match selected 140 .uris() 141 .first() 142 .and_then(|u| uri_to_path(u.as_str())) 143 { 144 Some(path) => NativeOutcome::Path(path), 145 None => NativeOutcome::Cancelled, 146 } 147 } 148 149 async fn run_save( 150 kind: FileKind, 151 title: &str, 152 accept: &str, 153 seed: Option<String>, 154 folder: Option<PathBuf>, 155 ) -> NativeOutcome { 156 let mut req = SaveFileRequest::default() 157 .title(title) 158 .accept_label(accept) 159 .modal(true); 160 if matches!(kind, FileKind::Step) { 161 req = req.filter(step_filter()); 162 } 163 if let Some(s) = seed.as_deref() { 164 req = req.current_name(s); 165 } 166 if let Some(f) = folder { 167 req = match req.current_folder::<PathBuf>(Some(f)) { 168 Ok(r) => r, 169 Err(e) => return NativeOutcome::Error(e.to_string()), 170 }; 171 } 172 let response = match req.send().await { 173 Ok(r) => r, 174 Err(e) => return classify_send_error(&e), 175 }; 176 let selected = match response.response() { 177 Ok(s) => s, 178 Err(e) => return classify_response_error(&e), 179 }; 180 match selected 181 .uris() 182 .first() 183 .and_then(|u| uri_to_path(u.as_str())) 184 { 185 Some(path) => NativeOutcome::Path(path), 186 None => NativeOutcome::Cancelled, 187 } 188 } 189 190 fn classify_send_error(e: &ashpd::Error) -> NativeOutcome { 191 NativeOutcome::Error(format!("portal send: {e}")) 192 } 193 194 fn classify_response_error(e: &ashpd::Error) -> NativeOutcome { 195 match e { 196 ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled) => { 197 NativeOutcome::Cancelled 198 } 199 other => NativeOutcome::Error(format!("portal response: {other}")), 200 } 201 } 202 203 fn uri_to_path(uri: &str) -> Option<PathBuf> { 204 let encoded = uri.strip_prefix("file://")?; 205 let decoded = percent_encoding::percent_decode_str(encoded).decode_utf8_lossy(); 206 Some(PathBuf::from(decoded.as_ref())) 207 } 208 209 #[cfg(test)] 210 mod tests { 211 use super::uri_to_path; 212 use std::path::PathBuf; 213 214 #[test] 215 fn plain_path() { 216 assert_eq!( 217 uri_to_path("file:///home/nel/docs"), 218 Some(PathBuf::from("/home/nel/docs")), 219 ); 220 } 221 222 #[test] 223 fn percent_decoded() { 224 assert_eq!( 225 uri_to_path("file:///home/nel/my%20docs"), 226 Some(PathBuf::from("/home/nel/my docs")), 227 ); 228 } 229 230 #[test] 231 fn rejects_non_file_scheme() { 232 assert_eq!(uri_to_path("http://example.com/docs"), None); 233 } 234 } 235}