1use std::path::Path;
83
84use serde::{Deserialize, Serialize};
85use serde_repr::{Deserialize_repr, Serialize_repr};
86use zbus::zvariant::{DeserializeDict, SerializeDict, Type};
87
88use super::{HandleToken, Request};
89use crate::{proxy::Proxy, Error, FilePath, WindowIdentifier};
90
91#[derive(Clone, Serialize, Deserialize, Type, Debug, PartialEq)]
92pub struct FileFilter(String, Vec<(FilterType, String)>);
95
96#[derive(Clone, Serialize_repr, Deserialize_repr, Debug, Type, PartialEq)]
97#[repr(u32)]
98enum FilterType {
99 GlobPattern = 0,
100 MimeType = 1,
101}
102
103impl FilterType {
104 fn is_mimetype(&self) -> bool {
106 matches!(self, FilterType::MimeType)
107 }
108
109 fn is_pattern(&self) -> bool {
111 matches!(self, FilterType::GlobPattern)
112 }
113}
114
115impl FileFilter {
116 pub fn new(label: &str) -> Self {
122 Self(label.to_owned(), vec![])
123 }
124
125 #[must_use]
127 pub fn mimetype(mut self, mimetype: &str) -> Self {
128 self.1.push((FilterType::MimeType, mimetype.to_owned()));
129 self
130 }
131
132 #[must_use]
134 pub fn glob(mut self, pattern: &str) -> Self {
135 self.1.push((FilterType::GlobPattern, pattern.to_owned()));
136 self
137 }
138}
139
140impl FileFilter {
141 pub fn label(&self) -> &str {
143 &self.0
144 }
145
146 pub fn mimetype_filters(&self) -> Vec<&str> {
148 self.1
149 .iter()
150 .filter_map(|(type_, string)| type_.is_mimetype().then_some(string.as_str()))
151 .collect()
152 }
153
154 pub fn pattern_filters(&self) -> Vec<&str> {
156 self.1
157 .iter()
158 .filter_map(|(type_, string)| type_.is_pattern().then_some(string.as_str()))
159 .collect()
160 }
161}
162
163#[derive(Clone, Serialize, Deserialize, Type, Debug)]
164pub struct Choice(String, String, Vec<(String, String)>, String);
166
167impl Choice {
168 pub fn boolean(id: &str, label: &str, state: bool) -> Self {
176 Self::new(id, label, &state.to_string())
177 }
178
179 pub fn new(id: &str, label: &str, initial_selection: &str) -> Self {
187 Self(
188 id.to_owned(),
189 label.to_owned(),
190 vec![],
191 initial_selection.to_owned(),
192 )
193 }
194
195 #[must_use]
197 pub fn insert(mut self, key: &str, value: &str) -> Self {
198 self.2.push((key.to_owned(), value.to_owned()));
199 self
200 }
201
202 pub fn id(&self) -> &str {
204 &self.0
205 }
206
207 pub fn label(&self) -> &str {
209 &self.1
210 }
211
212 pub fn pairs(&self) -> Vec<(&str, &str)> {
214 self.2
215 .iter()
216 .map(|(x, y)| (x.as_str(), y.as_str()))
217 .collect::<Vec<_>>()
218 }
219
220 pub fn initial_selection(&self) -> &str {
222 &self.3
223 }
224}
225
226#[derive(SerializeDict, Type, Debug, Default)]
227#[zvariant(signature = "dict")]
228struct OpenFileOptions {
229 handle_token: HandleToken,
230 accept_label: Option<String>,
231 modal: Option<bool>,
232 multiple: Option<bool>,
233 directory: Option<bool>,
234 filters: Vec<FileFilter>,
235 current_filter: Option<FileFilter>,
236 choices: Option<Vec<Choice>>,
237 current_folder: Option<FilePath>,
238}
239
240#[derive(SerializeDict, Type, Debug, Default)]
241#[zvariant(signature = "dict")]
242struct SaveFileOptions {
243 handle_token: HandleToken,
244 accept_label: Option<String>,
245 modal: Option<bool>,
246 current_name: Option<String>,
247 current_folder: Option<FilePath>,
248 current_file: Option<FilePath>,
249 filters: Vec<FileFilter>,
250 current_filter: Option<FileFilter>,
251 choices: Option<Vec<Choice>>,
252}
253
254#[derive(SerializeDict, Type, Debug, Default)]
255#[zvariant(signature = "dict")]
256struct SaveFilesOptions {
257 handle_token: HandleToken,
258 accept_label: Option<String>,
259 modal: Option<bool>,
260 choices: Option<Vec<Choice>>,
261 current_folder: Option<FilePath>,
262 files: Option<Vec<FilePath>>,
263}
264
265#[derive(Debug, Type, DeserializeDict)]
266#[zvariant(signature = "dict")]
269pub struct SelectedFiles {
270 uris: Vec<url::Url>,
271 choices: Option<Vec<(String, String)>>,
272}
273
274impl SelectedFiles {
275 pub fn open_file() -> OpenFileRequest {
277 OpenFileRequest::default()
278 }
279
280 pub fn save_file() -> SaveFileRequest {
282 SaveFileRequest::default()
283 }
284
285 pub fn save_files() -> SaveFilesRequest {
287 SaveFilesRequest::default()
288 }
289
290 pub fn uris(&self) -> &[url::Url] {
292 self.uris.as_slice()
293 }
294
295 pub fn choices(&self) -> &[(String, String)] {
297 self.choices.as_deref().unwrap_or_default()
298 }
299}
300
301#[doc(alias = "org.freedesktop.portal.FileChooser")]
302struct FileChooserProxy<'a>(Proxy<'a>);
303
304impl<'a> FileChooserProxy<'a> {
305 pub async fn new() -> Result<FileChooserProxy<'a>, Error> {
307 let proxy = Proxy::new_desktop("org.freedesktop.portal.FileChooser").await?;
308 Ok(Self(proxy))
309 }
310
311 pub async fn open_file(
312 &self,
313 identifier: Option<&WindowIdentifier>,
314 title: &str,
315 options: OpenFileOptions,
316 ) -> Result<Request<SelectedFiles>, Error> {
317 let identifier = identifier.map(|i| i.to_string()).unwrap_or_default();
318 self.0
319 .request(
320 &options.handle_token,
321 "OpenFile",
322 &(&identifier, title, &options),
323 )
324 .await
325 }
326
327 pub async fn save_file(
328 &self,
329 identifier: Option<&WindowIdentifier>,
330 title: &str,
331 options: SaveFileOptions,
332 ) -> Result<Request<SelectedFiles>, Error> {
333 let identifier = identifier.map(|i| i.to_string()).unwrap_or_default();
334 self.0
335 .request(
336 &options.handle_token,
337 "SaveFile",
338 &(&identifier, title, &options),
339 )
340 .await
341 }
342
343 pub async fn save_files(
344 &self,
345 identifier: Option<&WindowIdentifier>,
346 title: &str,
347 options: SaveFilesOptions,
348 ) -> Result<Request<SelectedFiles>, Error> {
349 let identifier = identifier.map(|i| i.to_string()).unwrap_or_default();
350 self.0
351 .request(
352 &options.handle_token,
353 "SaveFiles",
354 &(&identifier, title, &options),
355 )
356 .await
357 }
358}
359
360impl<'a> std::ops::Deref for FileChooserProxy<'a> {
361 type Target = zbus::Proxy<'a>;
362
363 fn deref(&self) -> &Self::Target {
364 &self.0
365 }
366}
367
368#[derive(Debug, Default)]
369#[doc(alias = "xdp_portal_open_file")]
370pub struct OpenFileRequest {
374 identifier: Option<WindowIdentifier>,
375 title: String,
376 options: OpenFileOptions,
377}
378
379impl OpenFileRequest {
380 #[must_use]
381 pub fn identifier(mut self, identifier: impl Into<Option<WindowIdentifier>>) -> Self {
383 self.identifier = identifier.into();
384 self
385 }
386
387 #[must_use]
389 pub fn title<'a>(mut self, title: impl Into<Option<&'a str>>) -> Self {
390 self.title = title.into().map(ToOwned::to_owned).unwrap_or_default();
391 self
392 }
393
394 #[must_use]
396 pub fn accept_label<'a>(mut self, accept_label: impl Into<Option<&'a str>>) -> Self {
397 self.options.accept_label = accept_label.into().map(ToOwned::to_owned);
398 self
399 }
400
401 #[must_use]
403 pub fn modal(mut self, modal: impl Into<Option<bool>>) -> Self {
404 self.options.modal = modal.into();
405 self
406 }
407
408 #[must_use]
410 pub fn multiple(mut self, multiple: impl Into<Option<bool>>) -> Self {
411 self.options.multiple = multiple.into();
412 self
413 }
414
415 #[must_use]
417 pub fn directory(mut self, directory: impl Into<Option<bool>>) -> Self {
418 self.options.directory = directory.into();
419 self
420 }
421
422 #[must_use]
424 pub fn filter(mut self, filter: FileFilter) -> Self {
425 self.options.filters.push(filter);
426 self
427 }
428
429 #[must_use]
430 pub fn filters(mut self, filters: impl IntoIterator<Item = FileFilter>) -> Self {
432 self.options.filters = filters.into_iter().collect();
433 self
434 }
435
436 #[must_use]
438 pub fn current_filter(mut self, current_filter: impl Into<Option<FileFilter>>) -> Self {
439 self.options.current_filter = current_filter.into();
440 self
441 }
442
443 #[must_use]
445 pub fn choice(mut self, choice: Choice) -> Self {
446 self.options
447 .choices
448 .get_or_insert_with(Vec::new)
449 .push(choice);
450 self
451 }
452
453 #[must_use]
454 pub fn choices(mut self, choices: impl IntoIterator<Item = Choice>) -> Self {
456 self.options.choices = Some(choices.into_iter().collect());
457 self
458 }
459
460 pub fn current_folder<P: AsRef<Path>>(
462 mut self,
463 current_folder: impl Into<Option<P>>,
464 ) -> Result<Self, crate::Error> {
465 self.options.current_folder = current_folder
466 .into()
467 .map(|c| FilePath::new(c))
468 .transpose()?;
469 Ok(self)
470 }
471
472 pub async fn send(self) -> Result<Request<SelectedFiles>, Error> {
474 let proxy = FileChooserProxy::new().await?;
475 proxy
476 .open_file(self.identifier.as_ref(), &self.title, self.options)
477 .await
478 }
479}
480
481#[derive(Debug, Default)]
482#[doc(alias = "xdp_portal_save_files")]
483pub struct SaveFilesRequest {
487 identifier: Option<WindowIdentifier>,
488 title: String,
489 options: SaveFilesOptions,
490}
491
492impl SaveFilesRequest {
493 #[must_use]
494 pub fn identifier(mut self, identifier: impl Into<Option<WindowIdentifier>>) -> Self {
496 self.identifier = identifier.into();
497 self
498 }
499
500 #[must_use]
502 pub fn title<'a>(mut self, title: impl Into<Option<&'a str>>) -> Self {
503 self.title = title.into().map(ToOwned::to_owned).unwrap_or_default();
504 self
505 }
506
507 #[must_use]
509 pub fn accept_label<'a>(mut self, accept_label: impl Into<Option<&'a str>>) -> Self {
510 self.options.accept_label = accept_label.into().map(ToOwned::to_owned);
511 self
512 }
513
514 #[must_use]
516 pub fn modal(mut self, modal: impl Into<Option<bool>>) -> Self {
517 self.options.modal = modal.into();
518 self
519 }
520
521 #[must_use]
523 pub fn choice(mut self, choice: Choice) -> Self {
524 self.options
525 .choices
526 .get_or_insert_with(Vec::new)
527 .push(choice);
528 self
529 }
530
531 #[must_use]
532 pub fn choices(mut self, choices: impl IntoIterator<Item = Choice>) -> Self {
534 self.options.choices = Some(choices.into_iter().collect());
535 self
536 }
537
538 pub fn current_folder<P: AsRef<Path>>(
540 mut self,
541 current_folder: impl Into<Option<P>>,
542 ) -> Result<Self, crate::Error> {
543 self.options.current_folder = current_folder
544 .into()
545 .map(|c| FilePath::new(c))
546 .transpose()?;
547 Ok(self)
548 }
549
550 pub fn files<P: IntoIterator<Item = impl AsRef<Path>>>(
552 mut self,
553 files: impl Into<Option<P>>,
554 ) -> Result<Self, crate::Error> {
555 self.options.files = files
556 .into()
557 .map(|files| files.into_iter().map(|s| FilePath::new(s)).collect())
558 .transpose()?;
559 Ok(self)
560 }
561
562 pub async fn send(self) -> Result<Request<SelectedFiles>, Error> {
564 let proxy = FileChooserProxy::new().await?;
565 proxy
566 .save_files(self.identifier.as_ref(), &self.title, self.options)
567 .await
568 }
569}
570
571#[derive(Debug, Default)]
572#[doc(alias = "xdp_portal_save_file")]
573pub struct SaveFileRequest {
577 identifier: Option<WindowIdentifier>,
578 title: String,
579 options: SaveFileOptions,
580}
581
582impl SaveFileRequest {
583 #[must_use]
584 pub fn identifier(mut self, identifier: impl Into<Option<WindowIdentifier>>) -> Self {
586 self.identifier = identifier.into();
587 self
588 }
589
590 #[must_use]
592 pub fn title<'a>(mut self, title: impl Into<Option<&'a str>>) -> Self {
593 self.title = title.into().map(ToOwned::to_owned).unwrap_or_default();
594 self
595 }
596
597 #[must_use]
599 pub fn accept_label<'a>(mut self, accept_label: impl Into<Option<&'a str>>) -> Self {
600 self.options.accept_label = accept_label.into().map(ToOwned::to_owned);
601 self
602 }
603
604 #[must_use]
606 pub fn modal(mut self, modal: impl Into<Option<bool>>) -> Self {
607 self.options.modal = modal.into();
608 self
609 }
610
611 #[must_use]
613 pub fn current_name<'a>(mut self, current_name: impl Into<Option<&'a str>>) -> Self {
614 self.options.current_name = current_name.into().map(ToOwned::to_owned);
615 self
616 }
617
618 pub fn current_folder<P: AsRef<Path>>(
620 mut self,
621 current_folder: impl Into<Option<P>>,
622 ) -> Result<Self, crate::Error> {
623 self.options.current_folder = current_folder
624 .into()
625 .map(|c| FilePath::new(c))
626 .transpose()?;
627 Ok(self)
628 }
629
630 pub fn current_file<P: AsRef<Path>>(
632 mut self,
633 current_file: impl Into<Option<P>>,
634 ) -> Result<Self, crate::Error> {
635 self.options.current_file = current_file.into().map(|c| FilePath::new(c)).transpose()?;
636 Ok(self)
637 }
638
639 #[must_use]
641 pub fn filter(mut self, filter: FileFilter) -> Self {
642 self.options.filters.push(filter);
643 self
644 }
645
646 #[must_use]
647 pub fn filters(mut self, filters: impl IntoIterator<Item = FileFilter>) -> Self {
649 self.options.filters = filters.into_iter().collect();
650 self
651 }
652
653 #[must_use]
655 pub fn current_filter(mut self, current_filter: impl Into<Option<FileFilter>>) -> Self {
656 self.options.current_filter = current_filter.into();
657 self
658 }
659
660 #[must_use]
662 pub fn choice(mut self, choice: Choice) -> Self {
663 self.options
664 .choices
665 .get_or_insert_with(Vec::new)
666 .push(choice);
667 self
668 }
669
670 #[must_use]
671 pub fn choices(mut self, choices: impl IntoIterator<Item = Choice>) -> Self {
673 self.options.choices = Some(choices.into_iter().collect());
674 self
675 }
676
677 pub async fn send(self) -> Result<Request<SelectedFiles>, Error> {
679 let proxy = FileChooserProxy::new().await?;
680 proxy
681 .save_file(self.identifier.as_ref(), &self.title, self.options)
682 .await
683 }
684}