1use super::*;
2use crate::model::{EmptyQueueBehavior, EstimateCompleteBehavior, ThemeVariant};
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 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 self.data.theme = self.data.theme.next();
259 self.theme = Theme::from_variant(self.data.theme);
260 let theme_key = match self.data.theme {
261 ThemeVariant::Dark => "dark",
262 ThemeVariant::Light => "light",
263 ThemeVariant::Polaris => "polaris",
264 ThemeVariant::Matrix => "matrix",
265 };
266 self.persist_setting("theme", theme_key);
267 self.set_status(format!("Theme: {}", self.data.theme.label()), false);
268 }
269 SettingsItem::CustomMinutes => {
270 let cur = self.timer.custom_minutes as i32;
271 let v = (cur + dir).clamp(1, 240) as u32;
272 self.timer.set_custom_minutes(v);
273 self.set_status(format!("Custom timer: {} min", v), false);
274 }
275 SettingsItem::AutoPickTask => {
276 self.toggle_bool_setting("auto_pick_task", "Auto-pick task", |d| {
277 &mut d.auto_pick_task
278 });
279 }
280 SettingsItem::AutoAdvanceTask => {
281 self.toggle_bool_setting("auto_advance_task", "Auto-advance task", |d| {
282 &mut d.auto_advance_task
283 });
284 }
285 SettingsItem::EmptyQueueBehavior => {
286 self.data.empty_queue_behavior = self.data.empty_queue_behavior.next();
287 let key = match self.data.empty_queue_behavior {
288 EmptyQueueBehavior::FreeFocus => "free-focus",
289 EmptyQueueBehavior::PauseTimer => "pause-timer",
290 EmptyQueueBehavior::AskEachTime => "ask",
291 };
292 self.persist_setting("empty_queue_behavior", key);
293 self.set_status(
294 format!(
295 "When queue empty: {}",
296 self.data.empty_queue_behavior.label()
297 ),
298 false,
299 );
300 }
301 SettingsItem::LogBreaks => {
302 self.toggle_bool_setting("log_breaks", "Log breaks", |d| &mut d.log_breaks);
303 }
304 SettingsItem::EstimateComplete => {
305 self.data.estimate_complete = self.data.estimate_complete.next();
306 let key = match self.data.estimate_complete {
307 EstimateCompleteBehavior::Nudge => "nudge",
308 EstimateCompleteBehavior::None => "none",
309 EstimateCompleteBehavior::AutoDone => "auto-done",
310 };
311 self.persist_setting("estimate_complete", key);
312 self.set_status(
313 format!("Estimate reached: {}", self.data.estimate_complete.label()),
314 false,
315 );
316 }
317 SettingsItem::ExportBackup => {}
318 }
319 self.sync_timer_config_to_data();
320 }
321}