Skip to main content

void/app/
settings.rs

1use super::*;
2use crate::model::{EmptyQueueBehavior, EstimateCompleteBehavior};
3use crossterm::event::{KeyCode, KeyEvent};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum SettingsItem {
7    FocusMinutes,
8    ShortBreak,
9    LongBreak,
10    LongBreakEvery,
11    DailyGoal,
12    Sound,
13    Notifications,
14    AutoStartBreaks,
15    AutoStartFocus,
16    ActiveTaskCycle,
17    Theme,
18    CustomMinutes,
19    AutoPickTask,
20    AutoAdvanceTask,
21    EmptyQueueBehavior,
22    LogBreaks,
23    EstimateComplete,
24    ExportBackup,
25}
26
27#[derive(Debug, Clone, Copy)]
28pub(crate) struct NumericSettingSpec<'a> {
29    pub key: &'a str,
30    pub label: &'a str,
31    pub min: u32,
32    pub max: u32,
33    pub step: i32,
34}
35
36#[derive(Debug, Clone, Default)]
37pub struct SettingsState {
38    pub selected: usize,
39    pub scroll_offset: usize,
40    /// Visible rows in the settings table (updated each draw).
41    pub page_size: usize,
42    pub items: Vec<SettingsItem>,
43}
44
45impl SettingsState {
46    pub fn new() -> Self {
47        Self {
48            selected: 0,
49            scroll_offset: 0,
50            page_size: 12,
51            items: vec![
52                SettingsItem::FocusMinutes,
53                SettingsItem::ShortBreak,
54                SettingsItem::LongBreak,
55                SettingsItem::LongBreakEvery,
56                SettingsItem::DailyGoal,
57                SettingsItem::Sound,
58                SettingsItem::Notifications,
59                SettingsItem::AutoStartBreaks,
60                SettingsItem::AutoStartFocus,
61                SettingsItem::ActiveTaskCycle,
62                SettingsItem::Theme,
63                SettingsItem::CustomMinutes,
64                SettingsItem::AutoPickTask,
65                SettingsItem::AutoAdvanceTask,
66                SettingsItem::EmptyQueueBehavior,
67                SettingsItem::LogBreaks,
68                SettingsItem::EstimateComplete,
69                SettingsItem::ExportBackup,
70            ],
71        }
72    }
73}
74
75impl App {
76    pub(crate) fn adjust_numeric_setting<F>(
77        &mut self,
78        dir: i32,
79        spec: NumericSettingSpec<'_>,
80        mut getter: F,
81    ) where
82        F: FnMut(&mut crate::model::AppData) -> &mut u32,
83    {
84        let val = getter(&mut self.data);
85        let cur = *val as i32;
86        let new_val = (cur + dir * spec.step).clamp(spec.min as i32, spec.max as i32) as u32;
87        *val = new_val;
88        if !spec.key.is_empty() {
89            self.persist_setting(spec.key, new_val.to_string());
90        }
91        self.set_status(format!("{}: {}", spec.label, new_val), false);
92    }
93
94    pub(crate) fn toggle_bool_setting<F>(&mut self, key: &str, label: &str, mut getter: F)
95    where
96        F: FnMut(&mut crate::model::AppData) -> &mut bool,
97    {
98        let val = getter(&mut self.data);
99        *val = !*val;
100        let is_on = *val;
101        self.persist_setting(key, if is_on { "1" } else { "0" });
102        self.set_status(
103            format!("{}: {}", label, if is_on { "on" } else { "off" }),
104            false,
105        );
106    }
107
108    pub(crate) fn handle_settings_key(&mut self, key: KeyEvent) {
109        let n = self.settings_state.items.len();
110        match key.code {
111            KeyCode::Down | KeyCode::Char('j') => {
112                self.settings_state.selected = (self.settings_state.selected + 1) % n;
113                self.sync_settings_scroll();
114            }
115            KeyCode::Up | KeyCode::Char('k') => {
116                if self.settings_state.selected == 0 {
117                    self.settings_state.selected = n - 1;
118                } else {
119                    self.settings_state.selected -= 1;
120                }
121                self.sync_settings_scroll();
122            }
123            KeyCode::Enter => {
124                let item = self.settings_state.items[self.settings_state.selected];
125                if item == SettingsItem::ExportBackup {
126                    self.export_backup();
127                } else {
128                    self.adjust_setting(1);
129                }
130            }
131            KeyCode::Right | KeyCode::Char('+') | KeyCode::Char('=') => {
132                self.adjust_setting(1);
133            }
134            KeyCode::Left | KeyCode::Char('-') => {
135                self.adjust_setting(-1);
136            }
137            _ => {}
138        }
139    }
140
141    pub fn settings_visual_row(selected: usize) -> usize {
142        const HEADERS: [usize; 6] = [0, 5, 9, 10, 14, 17];
143        selected + HEADERS.iter().filter(|h| **h <= selected).count()
144    }
145
146    pub fn sync_settings_scroll(&mut self) {
147        let visible = self.settings_state.page_size.max(4);
148        let visual = Self::settings_visual_row(self.settings_state.selected);
149        if visual < self.settings_state.scroll_offset {
150            self.settings_state.scroll_offset = visual;
151        } else if visual >= self.settings_state.scroll_offset + visible {
152            self.settings_state.scroll_offset = visual.saturating_sub(visible - 1);
153        }
154    }
155
156    pub(crate) fn adjust_setting(&mut self, dir: i32) {
157        let item = self.settings_state.items[self.settings_state.selected];
158        match item {
159            SettingsItem::FocusMinutes => {
160                let cur = self.timer.config.focus_minutes as i32;
161                let v = (cur + dir).clamp(1, 240) as u32;
162                self.timer.set_focus_minutes(v);
163                self.data.focus_minutes = v;
164                if let Err(e) = self.db.persist_timer_settings(&self.data) {
165                    self.set_status(format!("Save error: {e}"), true);
166                }
167                self.set_status(format!("Focus: {} min", v), false);
168            }
169            SettingsItem::ShortBreak => {
170                let cur = self.timer.config.short_break_minutes as i32;
171                let v = (cur + dir).clamp(1, 60) as u32;
172                self.timer.config.short_break_minutes = v;
173                self.data.short_break_minutes = v;
174                if let Err(e) = self.db.persist_timer_settings(&self.data) {
175                    self.set_status(format!("Save error: {e}"), true);
176                }
177                self.set_status(format!("Short break: {} min", v), false);
178            }
179            SettingsItem::LongBreak => {
180                let cur = self.timer.config.long_break_minutes as i32;
181                let v = (cur + dir).clamp(1, 120) as u32;
182                self.timer.config.long_break_minutes = v;
183                self.data.long_break_minutes = v;
184                if let Err(e) = self.db.persist_timer_settings(&self.data) {
185                    self.set_status(format!("Save error: {e}"), true);
186                }
187                self.set_status(format!("Long break: {} min", v), false);
188            }
189            SettingsItem::LongBreakEvery => {
190                let cur = self.timer.config.long_break_every as i32;
191                let v = (cur + dir).clamp(1, 12) as u32;
192                self.timer.config.long_break_every = v;
193                self.data.long_break_every = v;
194                if let Err(e) = self.db.persist_timer_settings(&self.data) {
195                    self.set_status(format!("Save error: {e}"), true);
196                }
197                self.set_status(format!("Long break every: {} sessions", v), false);
198            }
199            SettingsItem::DailyGoal => {
200                self.adjust_numeric_setting(
201                    dir,
202                    NumericSettingSpec {
203                        key: "daily_goal_minutes",
204                        label: "Daily goal (min)",
205                        min: 15,
206                        max: 1440,
207                        step: 15,
208                    },
209                    |d| &mut d.daily_goal_minutes,
210                );
211            }
212            SettingsItem::Sound => {
213                self.toggle_bool_setting("sound_enabled", "Sound", |d| &mut d.sound_enabled);
214            }
215            SettingsItem::Notifications => {
216                self.toggle_bool_setting("notify_on_finish", "Notifications", |d| {
217                    &mut d.notify_on_finish
218                });
219            }
220            SettingsItem::AutoStartBreaks => {
221                self.toggle_bool_setting("auto_start_breaks", "Auto-start breaks", |d| {
222                    &mut d.auto_start_breaks
223                });
224            }
225            SettingsItem::AutoStartFocus => {
226                self.toggle_bool_setting("auto_start_focus", "Auto-start focus", |d| {
227                    &mut d.auto_start_focus
228                });
229            }
230            SettingsItem::ActiveTaskCycle => {
231                if self.data.tasks.is_empty() {
232                    self.set_active_task(None);
233                    self.set_status("No tasks to activate.", true);
234                    return;
235                }
236                let ids: Vec<u64> = self.data.tasks.iter().map(|t| t.id).collect();
237                let cur = self
238                    .active_task
239                    .and_then(|id| ids.iter().position(|x| *x == id));
240                let next_idx = match (cur, dir) {
241                    (Some(i), d) if d > 0 => (i + 1) % ids.len(),
242                    (Some(i), d) if d < 0 => {
243                        if i == 0 {
244                            ids.len() - 1
245                        } else {
246                            i - 1
247                        }
248                    }
249                    (None, _) => 0,
250                    _ => 0,
251                };
252                self.set_active_task(Some(ids[next_idx]));
253                if let Some(task) = self.data.tasks.iter().find(|t| t.id == ids[next_idx]) {
254                    self.set_status(format!("Active task: {}", task.title), false);
255                }
256            }
257            SettingsItem::Theme => {
258                let next = self.theme_catalog.next_id(&self.data.theme);
259                let label = self.theme_catalog.label(&next);
260                self.apply_theme(&next);
261                self.set_status(format!("Theme: {label}"), false);
262            }
263            SettingsItem::CustomMinutes => {
264                let cur = self.timer.custom_minutes as i32;
265                let v = (cur + dir).clamp(1, 240) as u32;
266                self.timer.set_custom_minutes(v);
267                self.set_status(format!("Custom timer: {} min", v), false);
268            }
269            SettingsItem::AutoPickTask => {
270                self.toggle_bool_setting("auto_pick_task", "Auto-pick task", |d| {
271                    &mut d.auto_pick_task
272                });
273            }
274            SettingsItem::AutoAdvanceTask => {
275                self.toggle_bool_setting("auto_advance_task", "Auto-advance task", |d| {
276                    &mut d.auto_advance_task
277                });
278            }
279            SettingsItem::EmptyQueueBehavior => {
280                self.data.empty_queue_behavior = self.data.empty_queue_behavior.next();
281                let key = match self.data.empty_queue_behavior {
282                    EmptyQueueBehavior::FreeFocus => "free-focus",
283                    EmptyQueueBehavior::PauseTimer => "pause-timer",
284                    EmptyQueueBehavior::AskEachTime => "ask",
285                };
286                self.persist_setting("empty_queue_behavior", key);
287                self.set_status(
288                    format!(
289                        "When queue empty: {}",
290                        self.data.empty_queue_behavior.label()
291                    ),
292                    false,
293                );
294            }
295            SettingsItem::LogBreaks => {
296                self.toggle_bool_setting("log_breaks", "Log breaks", |d| &mut d.log_breaks);
297            }
298            SettingsItem::EstimateComplete => {
299                self.data.estimate_complete = self.data.estimate_complete.next();
300                let key = match self.data.estimate_complete {
301                    EstimateCompleteBehavior::Nudge => "nudge",
302                    EstimateCompleteBehavior::None => "none",
303                    EstimateCompleteBehavior::AutoDone => "auto-done",
304                };
305                self.persist_setting("estimate_complete", key);
306                self.set_status(
307                    format!("Estimate reached: {}", self.data.estimate_complete.label()),
308                    false,
309                );
310            }
311            SettingsItem::ExportBackup => {}
312        }
313        self.sync_timer_config_to_data();
314    }
315}