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