Skip to main content
KeyOS API Reference

gui_server_api/navigation/
filepicker.rs

1// SPDX-FileCopyrightText: 2024-2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! File picker (select file) navigation request and response formats.
5
6#[derive(Debug, Copy, Clone, Eq, PartialEq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
7pub enum Location {
8    Internal,
9    Airlock,
10    External,
11}
12
13#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
14pub enum AllowedLocations {
15    All,
16    Specific(Vec<Location>),
17}
18
19impl AllowedLocations {
20    pub fn specific<T: IntoIterator<Item = Location>>(locations: T) -> Self {
21        Self::Specific(locations.into_iter().collect::<Vec<_>>())
22    }
23
24    pub fn contains(&self, location: Location) -> bool {
25        match self {
26            Self::All => true,
27            Self::Specific(locations) => locations.contains(&location),
28        }
29    }
30
31    pub fn len(&self) -> Option<usize> {
32        match self {
33            Self::All => None,
34            Self::Specific(locations) => Some(locations.len()),
35        }
36    }
37
38    pub fn is_empty(&self) -> bool { self.len().map(|len| len == 0).unwrap_or(false) }
39}
40
41#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
42pub enum AllowedExtensions {
43    All,
44    Specific(Vec<String>),
45}
46
47impl AllowedExtensions {
48    pub fn specific<S: AsRef<str>, T: IntoIterator<Item = S>>(extensions: T) -> Self {
49        Self::Specific(extensions.into_iter().map(|s| s.as_ref().to_string()).collect::<Vec<_>>())
50    }
51
52    pub fn contains<S: AsRef<str>>(&self, extension: S) -> bool {
53        match self {
54            Self::All => true,
55            Self::Specific(extensions) => {
56                extensions.iter().any(|allowed| allowed.eq_ignore_ascii_case(extension.as_ref()))
57            }
58        }
59    }
60}
61
62/// Options for the file picker navigation request.
63///
64/// Example to only allow accessing `.bin` files in `External` location:
65///
66/// ```rust,ignore
67/// # use navigation::api::filepicker::{SelectFileOptions, AllowedLocations, AllowedExtensions, Location};
68/// let options = SelectFileOptions::default()
69///     .with_start_location(Location::External)
70///     .with_allowed_locations(AllowedLocations::specific(&[Location::External]))
71///     .with_allowed_extensions(AllowedExtensions::specific(&["bin"]));
72/// ```
73#[derive(Debug, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
74pub struct SelectFileOptions {
75    start_location: Location,
76    locations: AllowedLocations,
77    extensions: AllowedExtensions,
78    hidden_allowed: bool,
79    dirs_allowed: bool,
80    search_allowed: bool,
81    dir_selection_mode: bool,
82    multiple_selection_mode: bool,
83}
84
85impl Default for SelectFileOptions {
86    fn default() -> Self {
87        Self {
88            start_location: Location::Internal,
89            locations: AllowedLocations::All,
90            extensions: AllowedExtensions::All,
91            hidden_allowed: true,
92            search_allowed: true,
93            dirs_allowed: true,
94            dir_selection_mode: false,
95            multiple_selection_mode: false,
96        }
97    }
98}
99
100impl SelectFileOptions {
101    /// Enable directory selection mode.
102    /// Doesn't show the files, only directories and allows navigating into them.
103    /// This is disabled by default.
104    /// Enabling this also enables `dirs_allowed`.
105    pub fn with_dir_selection_mode(self, dir_selection_mode: bool) -> Self {
106        Self { dir_selection_mode, dirs_allowed: self.dirs_allowed || dir_selection_mode, ..self }
107    }
108
109    /// Allow viewing and navigating directories.
110    /// This is enabled by default.
111    /// Disabling this has no effect if `dir_selection_mode` is enabled.
112    pub fn with_dirs_allowed(self, allow_directories: bool) -> Self {
113        Self { dirs_allowed: allow_directories || self.dir_selection_mode, ..self }
114    }
115
116    /// Display the search bar and activates search menu.
117    pub fn with_search_allowed(self, allow_search: bool) -> Self {
118        Self { search_allowed: allow_search, ..self }
119    }
120
121    /// Display the search bar and activates search menu.
122    /// This is disabled by default.
123    /// Enabling this will disable `dir_selection_mode`.
124    pub fn with_multiple_selection_mode(self, multiple_selection_mode: bool) -> Self {
125        let dir_selection_mode = self.dir_selection_mode && !multiple_selection_mode;
126        Self { multiple_selection_mode, dir_selection_mode, ..self }
127    }
128
129    /// Sets whether hidden files are allowed to be shown.
130    pub fn with_hidden_allowed(self, hidden_allowed: bool) -> Self { Self { hidden_allowed, ..self } }
131
132    pub fn with_start_location(self, start_location: Location) -> Self { Self { start_location, ..self } }
133
134    pub fn with_allowed_locations(self, locations: AllowedLocations) -> Self { Self { locations, ..self } }
135
136    pub fn with_allowed_extensions(self, extensions: AllowedExtensions) -> Self {
137        Self { extensions, ..self }
138    }
139
140    pub fn search_allowed(&self) -> bool { self.search_allowed }
141
142    pub fn dirs_allowed(&self) -> bool { self.dirs_allowed }
143
144    pub fn hidden_allowed(&self) -> bool { self.hidden_allowed }
145
146    pub fn dir_selection_mode(&self) -> bool { self.dir_selection_mode }
147
148    pub fn multiple_selection_mode(&self) -> bool { self.multiple_selection_mode }
149
150    pub fn start_location(&self) -> Location { self.start_location }
151
152    pub fn allowed_locations(&self) -> &AllowedLocations { &self.locations }
153
154    pub fn allowed_extensions(&self) -> &AllowedExtensions { &self.extensions }
155
156    pub fn from_slice(data: &[u8]) -> Option<Self> {
157        let Ok(archived) = rkyv::access::<ArchivedSelectFileOptions, rkyv::rancor::Error>(data) else {
158            return None;
159        };
160        rkyv::deserialize::<Self, rkyv::rancor::Error>(archived).ok()
161    }
162
163    pub fn serialize(&self) -> Vec<u8> { rkyv::to_bytes::<rkyv::rancor::Error>(self).unwrap().to_vec() }
164}
165
166#[derive(Debug, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
167pub struct SelectFileResult {
168    pub files: Vec<(String, Location)>,
169}
170
171impl SelectFileResult {
172    pub fn new(files: Vec<(String, Location)>) -> Self { Self { files } }
173
174    pub fn files(&self) -> &[(String, Location)] { &self.files }
175
176    pub fn from_slice(data: &[u8]) -> Option<Self> {
177        let Ok(archived) = rkyv::access::<ArchivedSelectFileResult, rkyv::rancor::Error>(data) else {
178            return None;
179        };
180        rkyv::deserialize::<Self, rkyv::rancor::Error>(archived).ok()
181    }
182
183    pub fn serialize(&self) -> Vec<u8> { rkyv::to_bytes::<rkyv::rancor::Error>(self).unwrap().to_vec() }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn allowed_extensions_match_case_insensitively() {
192        let extensions = AllowedExtensions::specific(["png", "jpg"]);
193
194        assert!(extensions.contains("JPG"));
195        assert!(extensions.contains("Png"));
196    }
197}