cosmic_files/
config.rs

1// SPDX-License-Identifier: GPL-3.0-only
2
3use std::any::TypeId;
4use std::num::NonZeroU16;
5use std::path::PathBuf;
6
7use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;
8use cosmic::cosmic_config::{self, CosmicConfigEntry};
9use cosmic::iced::Subscription;
10use cosmic::{Application, theme};
11use serde::{Deserialize, Serialize};
12
13use crate::FxOrderMap;
14use crate::app::App;
15use crate::tab::{HeadingOptions, Location, View};
16
17pub use crate::context_action::{ContextActionPreset, ContextActionSelection};
18
19pub const CONFIG_VERSION: u64 = 1;
20
21// Default icon sizes
22pub const ICON_SIZE_LIST: u16 = 32;
23pub const ICON_SIZE_LIST_CONDENSED: u16 = 48;
24pub const ICON_SIZE_GRID: u16 = 64;
25// TODO: 5 is an arbitrary number. Maybe there's a better icon size max
26pub const ICON_SCALE_MAX: u16 = 5;
27
28macro_rules! percent {
29    ($perc:expr, $pixel:ident) => {
30        (($perc.get() as f32 * $pixel as f32) / 100.).clamp(1., ($pixel * ICON_SCALE_MAX) as _)
31    };
32}
33
34#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
35pub enum AppTheme {
36    Dark,
37    Light,
38    System,
39}
40
41impl AppTheme {
42    pub fn theme(&self) -> theme::Theme {
43        match self {
44            Self::Dark => {
45                let mut t = theme::system_dark();
46                t.theme_type.prefer_dark(Some(true));
47                t
48            }
49            Self::Light => {
50                let mut t = theme::system_light();
51                t.theme_type.prefer_dark(Some(false));
52                t
53            }
54            Self::System => theme::system_preference(),
55        }
56    }
57}
58
59#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
60pub enum Favorite {
61    Home,
62    Documents,
63    Downloads,
64    Music,
65    Pictures,
66    Videos,
67    Path(PathBuf),
68    Network {
69        uri: String,
70        name: String,
71        path: PathBuf,
72    },
73    Remote {
74        uri: String,
75        name: String,
76        path: PathBuf,
77    },
78}
79
80impl Favorite {
81    pub fn from_path(path: PathBuf) -> Self {
82        // Ensure that special folders are handled properly
83        [
84            Self::Home,
85            Self::Documents,
86            Self::Downloads,
87            Self::Music,
88            Self::Pictures,
89            Self::Videos,
90        ]
91        .into_iter()
92        .find(|fav| fav.path_opt().as_ref() == Some(&path))
93        .unwrap_or(Self::Path(path))
94    }
95
96    pub fn path_opt(&self) -> Option<PathBuf> {
97        match self {
98            Self::Home => dirs::home_dir(),
99            Self::Documents => dirs::document_dir(),
100            Self::Downloads => dirs::download_dir(),
101            Self::Music => dirs::audio_dir(),
102            Self::Pictures => dirs::picture_dir(),
103            Self::Videos => dirs::video_dir(),
104            Self::Path(path) => Some(path.clone()),
105            Self::Network { path, .. } => Some(path.clone()),
106            Self::Remote { path, .. } => Some(path.clone()),
107        }
108    }
109}
110
111#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
112pub enum TypeToSearch {
113    Recursive,
114    EnterPath,
115    SelectByPrefix,
116}
117
118#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
119#[serde(default)]
120pub struct State {
121    pub sort_names: FxOrderMap<String, (HeadingOptions, bool)>,
122}
123
124impl Default for State {
125    fn default() -> Self {
126        Self {
127            sort_names: FxOrderMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
128                (
129                    Location::Path(dir).normalize().to_string(),
130                    (HeadingOptions::Modified, false),
131                )
132            })),
133        }
134    }
135}
136
137impl State {
138    pub fn load() -> (Option<cosmic_config::Config>, Self) {
139        match cosmic_config::Config::new_state(App::APP_ID, CONFIG_VERSION) {
140            Ok(config_handler) => {
141                let config = match Self::get_entry(&config_handler) {
142                    Ok(ok) => ok,
143                    Err((errs, config)) => {
144                        log::info!("errors loading config: {errs:?}");
145                        config
146                    }
147                };
148                (Some(config_handler), config)
149            }
150            Err(err) => {
151                log::error!("failed to create config handler: {err}");
152                (None, Self::default())
153            }
154        }
155    }
156
157    pub fn subscription() -> Subscription<cosmic_config::Update<Self>> {
158        struct ConfigSubscription;
159        cosmic_config::config_state_subscription(
160            TypeId::of::<ConfigSubscription>(),
161            App::APP_ID.into(),
162            CONFIG_VERSION,
163        )
164    }
165}
166
167#[derive(Clone, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
168#[serde(default)]
169pub struct TBConfig {
170    pub script_path: String,
171    pub out_dir: String,
172    pub docx_template_path: String,
173    pub pair1_suffix: String,
174    pub pair2_suffix: String,
175    pub ab1_scan_path: String,
176    pub ab1_cache_path: String,
177    pub ab1_out_dir_csv: String,
178    pub ab1_out_dir_pdf: String,
179    pub ntfy_topic: String,
180    pub report_max_age_days: u32,
181}
182
183/// Default cutoff for [`TBConfig::report_max_age_days`]: samples older than this
184/// are excluded from the generated PDF report.
185const DEFAULT_REPORT_MAX_AGE_DAYS: u32 = 60;
186
187impl Default for TBConfig {
188    fn default() -> Self {
189        Self {
190            script_path: String::new(),
191            out_dir: String::new(),
192            docx_template_path: String::new(),
193            pair1_suffix: String::new(),
194            pair2_suffix: String::new(),
195            ab1_scan_path: String::new(),
196            ab1_cache_path: String::new(),
197            ab1_out_dir_csv: String::new(),
198            ab1_out_dir_pdf: String::new(),
199            ntfy_topic: String::new(),
200            report_max_age_days: DEFAULT_REPORT_MAX_AGE_DAYS,
201        }
202    }
203}
204
205
206#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
207#[serde(default)]
208pub struct Config {
209    pub app_theme: AppTheme,
210    pub dialog: DialogConfig,
211    pub desktop: DesktopConfig,
212    pub context_actions: Vec<ContextActionPreset>,
213    pub thumb_cfg: ThumbCfg,
214    pub favorites: Vec<Favorite>,
215    pub show_details: bool,
216    pub show_recents: bool,
217    pub tab: TabConfig,
218    pub type_to_search: TypeToSearch,
219    pub tb_config: TBConfig,
220}
221
222impl Config {
223    pub fn load() -> (Option<cosmic_config::Config>, Self) {
224        match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
225            Ok(config_handler) => {
226                let config = match Self::get_entry(&config_handler) {
227                    Ok(ok) => ok,
228                    Err((errs, config)) => {
229                        log::info!("errors loading config: {errs:?}");
230                        config
231                    }
232                };
233                (Some(config_handler), config)
234            }
235            Err(err) => {
236                log::error!("failed to create config handler: {err}");
237                (None, Self::default())
238            }
239        }
240    }
241
242    pub fn subscription() -> Subscription<cosmic_config::Update<Self>> {
243        struct ConfigSubscription;
244        cosmic_config::config_subscription(
245            TypeId::of::<ConfigSubscription>(),
246            App::APP_ID.into(),
247            CONFIG_VERSION,
248        )
249    }
250
251    /// Construct tab config for dialog
252    pub const fn dialog_tab(&self) -> TabConfig {
253        TabConfig {
254            folders_first: self.dialog.folders_first,
255            icon_sizes: self.dialog.icon_sizes,
256            military_time: self.tab.military_time,
257            show_hidden: self.dialog.show_hidden,
258            single_click: false,
259            view: self.dialog.view,
260            show_as_samples: false,
261            show_susceptible: self.dialog.show_susceptible,
262        }
263    }
264
265        /// Construct tab config for dialog
266    pub fn tb_config(&self) -> TBConfig {
267        self.tb_config.clone()
268    }
269}
270
271impl Default for Config {
272    fn default() -> Self {
273        Self {
274            app_theme: AppTheme::System,
275            desktop: DesktopConfig::default(),
276            dialog: DialogConfig::default(),
277            context_actions: Vec::new(),
278            thumb_cfg: ThumbCfg::default(),
279            favorites: vec![
280                Favorite::Home,
281                Favorite::Documents,
282                Favorite::Downloads,
283                Favorite::Music,
284                Favorite::Pictures,
285                Favorite::Videos,
286            ],
287            show_details: false,
288            show_recents: true,
289            tab: TabConfig::default(),
290            type_to_search: TypeToSearch::Recursive,
291            tb_config: TBConfig::default(),
292        }
293    }
294}
295
296#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
297#[serde(default)]
298pub struct DesktopConfig {
299    pub grid_spacing: NonZeroU16,
300    pub icon_size: NonZeroU16,
301    pub show_content: bool,
302    pub show_mounted_drives: bool,
303    pub show_connected_drives: bool,
304    pub show_trash: bool,
305}
306
307impl Default for DesktopConfig {
308    fn default() -> Self {
309        Self {
310            grid_spacing: 100.try_into().unwrap(),
311            icon_size: 100.try_into().unwrap(),
312            show_content: true,
313            show_mounted_drives: false,
314            show_connected_drives: false,
315            show_trash: false,
316        }
317    }
318}
319
320impl DesktopConfig {
321    pub fn grid_spacing_for(&self, space: u16) -> u16 {
322        percent!(self.grid_spacing, space) as _
323    }
324}
325
326#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
327#[serde(default)]
328pub struct DialogConfig {
329    /// Show folders before files
330    pub folders_first: bool,
331    /// Icon zoom
332    pub icon_sizes: IconSizes,
333    /// Show details sidebar
334    pub show_details: bool,
335    /// Show hidden files and folders
336    pub show_hidden: bool,
337    /// Selected view, grid or list
338    pub view: View,
339    /// Show susceptible samples
340    pub show_susceptible: bool,
341}
342
343impl Default for DialogConfig {
344    fn default() -> Self {
345        Self {
346            folders_first: false,
347            icon_sizes: IconSizes::default(),
348            show_details: true,
349            show_hidden: false,
350            view: View::List,
351            show_susceptible: true,
352        }
353    }
354}
355#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
356#[serde(default)]
357pub struct ThumbCfg {
358    pub jobs: NonZeroU16,
359    pub max_mem_mb: NonZeroU16,
360    pub max_size_mb: NonZeroU16,
361}
362
363impl Default for ThumbCfg {
364    fn default() -> Self {
365        Self {
366            jobs: 4.try_into().unwrap(),
367            max_mem_mb: 2000.try_into().unwrap(),
368            max_size_mb: 64.try_into().unwrap(),
369        }
370    }
371}
372
373/// Global and local [`crate::tab::Tab`] config.
374///
375/// [`TabConfig`] contains options that are passed to each instance of [`crate::tab::Tab`].
376/// These options are set globally through the main config, but each tab may change options
377/// locally. Local changes aren't saved to the main config.
378#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
379#[serde(default)]
380pub struct TabConfig {
381    /// Show folders before files
382    pub folders_first: bool,
383    /// Icon zoom
384    pub icon_sizes: IconSizes,
385    #[serde(skip)]
386    /// 24 hour clock; this is neither serialized nor deserialized because we use the user's global
387    /// preference rather than save it
388    pub military_time: bool,
389    /// Show hidden files and folders
390    pub show_hidden: bool,
391    /// Single click to open
392    pub single_click: bool,
393    /// Selected view, grid or list
394    pub view: View,
395    /// Show samples instead of files
396    pub show_as_samples: bool,
397    /// Show susceptible samples
398    pub show_susceptible: bool,
399}
400
401impl Default for TabConfig {
402    fn default() -> Self {
403        Self {
404            folders_first: true,
405            icon_sizes: IconSizes::default(),
406            military_time: false,
407            show_hidden: false,
408            single_click: false,
409            view: View::List,
410            show_as_samples: false,
411            show_susceptible: true,
412        }
413    }
414}
415
416#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
417#[serde(default)]
418pub struct IconSizes {
419    pub list: NonZeroU16,
420    pub grid: NonZeroU16,
421}
422
423impl Default for IconSizes {
424    fn default() -> Self {
425        Self {
426            list: 100.try_into().unwrap(),
427            grid: 100.try_into().unwrap(),
428        }
429    }
430}
431
432impl IconSizes {
433    pub fn list(&self) -> u16 {
434        percent!(self.list, ICON_SIZE_LIST) as _
435    }
436
437    pub fn list_condensed(&self) -> u16 {
438        percent!(self.list, ICON_SIZE_LIST_CONDENSED) as _
439    }
440
441    pub fn grid(&self) -> u16 {
442        percent!(self.grid, ICON_SIZE_GRID) as _
443    }
444}
445
446pub const TIME_CONFIG_ID: &str = "com.system76.CosmicAppletTime";
447
448#[derive(Debug, Default, Clone, CosmicConfigEntry, PartialEq, Eq)]
449#[version = 1]
450pub struct TimeConfig {
451    pub military_time: bool,
452}