1use serde::{Deserialize, Serialize};
4
5pub const THEMES: [&str; 7] = ["purple", "cyan", "blue", "red", "yellow", "green", "white"];
6pub const DATE_FORMATS: [&str; 3] = ["Y-M-D", "D-M-Y", "M-D-Y"];
7pub const PREVIEW_POSITIONS: [&str; 2] = ["bottom", "right"];
9
10pub const SORTS: [&str; 4] = ["manual", "important", "done", "due"];
13
14pub fn sort_label(sort: &str) -> &'static str {
16 match sort {
17 "important" => "Most important first",
18 "done" => "Done last",
19 "due" => "By due date",
20 _ => "As added",
22 }
23}
24
25pub fn theme_label(color: &str) -> String {
27 let mut chars = color.chars();
28 match chars.next() {
29 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
30 None => String::new(),
31 }
32}
33
34pub fn preview_position_label(pos: &str) -> &'static str {
36 match pos {
37 "right" => "Right (bottom if narrow)",
38 _ => "Bottom (hidden if narrow)",
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct Settings {
44 #[serde(default = "default_date_format")]
45 pub date_format: String,
46 #[serde(default = "default_color")]
47 pub selected_color: String,
48 #[serde(default = "default_sort")]
49 pub sort: String,
50 #[serde(default = "default_preview_position")]
52 pub preview_position: String,
53 #[serde(default)]
56 pub hide_done: bool,
57 #[serde(default)]
58 pub last_run_version: Option<String>,
59}
60
61impl Default for Settings {
62 fn default() -> Self {
63 Self {
64 date_format: default_date_format(),
65 selected_color: default_color(),
66 sort: default_sort(),
67 preview_position: default_preview_position(),
68 hide_done: false,
69 last_run_version: None,
70 }
71 }
72}
73
74impl Settings {
75 pub fn normalized(mut self) -> Self {
80 if !THEMES.contains(&self.selected_color.as_str()) {
81 self.selected_color = default_color();
82 }
83 if !DATE_FORMATS.contains(&self.date_format.as_str()) {
84 self.date_format = default_date_format();
85 }
86 if self.sort == "category" || !SORTS.contains(&self.sort.as_str()) {
89 self.sort = default_sort();
90 }
91 if !PREVIEW_POSITIONS.contains(&self.preview_position.as_str()) {
92 self.preview_position = default_preview_position();
93 }
94 self
95 }
96
97 pub fn take_first_run(&mut self, version: &str) -> bool {
100 let first = self.last_run_version.is_none();
101 if self.last_run_version.as_deref() != Some(version) {
102 self.last_run_version = Some(version.to_string());
103 }
104 first
105 }
106}
107
108fn default_date_format() -> String {
109 "Y-M-D".to_string()
110}
111
112fn default_color() -> String {
113 "white".to_string()
114}
115
116fn default_sort() -> String {
117 "manual".to_string()
118}
119
120fn default_preview_position() -> String {
121 "bottom".to_string()
122}
123
124pub fn cycle_by(values: &[&str], current: &str, delta: isize) -> String {
126 if values.is_empty() {
127 return current.to_string();
128 }
129 let idx = values.iter().position(|v| *v == current).unwrap_or(0) as isize;
130 let n = values.len() as isize;
131 let next = (idx + delta).rem_euclid(n) as usize;
132 values[next].to_string()
133}