1use 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
21pub const ICON_SIZE_LIST: u16 = 32;
23pub const ICON_SIZE_LIST_CONDENSED: u16 = 48;
24pub const ICON_SIZE_GRID: u16 = 64;
25pub 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 [
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
183const 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 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 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 pub folders_first: bool,
331 pub icon_sizes: IconSizes,
333 pub show_details: bool,
335 pub show_hidden: bool,
337 pub view: View,
339 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#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
379#[serde(default)]
380pub struct TabConfig {
381 pub folders_first: bool,
383 pub icon_sizes: IconSizes,
385 #[serde(skip)]
386 pub military_time: bool,
389 pub show_hidden: bool,
391 pub single_click: bool,
393 pub view: View,
395 pub show_as_samples: bool,
397 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}