Skip to main content

mach/
settings.rs

1//! User settings persisted by [`crate::store::Store`].
2
3use 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"];
7/// Where the task preview / docked editor sits relative to the list.
8pub const PREVIEW_POSITIONS: [&str; 2] = ["bottom", "right"];
9
10/// How tasks are ordered **inside** each category. All Tasks always stacks
11/// categories in sidebar order; this only rearranges rows within a group.
12pub const SORTS: [&str; 4] = ["manual", "important", "done", "due"];
13
14/// What the settings panel calls each sort.
15pub 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        // "manual", legacy "category", and anything unknown: persisted order.
21        _ => "As added",
22    }
23}
24
25/// Display name for a theme id (`"purple"` → `"Purple"`).
26pub 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
34/// Settings label for preview placement.
35pub 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    /// `"bottom"` under the task list, or `"right"` beside it (wide terminals).
51    #[serde(default = "default_preview_position")]
52    pub preview_position: String,
53    /// When true, completed tasks stay on disk but leave the list until
54    /// `/done` shows them again.
55    #[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    /// Normalize values imported from the legacy JSON settings file.
76    ///
77    /// SQLite-backed settings are validated on every write and therefore do
78    /// not need this compatibility path.
79    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        // Legacy "category" meant All-Tasks grouping; that is now always on,
87        // so map it to as-added within each group.
88        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    /// Record `version` in memory and report whether this was the first run.
98    /// The caller persists the changed settings through [`crate::store::Store`].
99    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
124/// Step a string setting by `delta` (±1) in a list, wrapping around.
125pub 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}