1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4enum SettingsRow {
5 Header(&'static str, &'static str),
6 Item(usize),
7}
8
9pub(crate) fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
10 let chunks = Layout::default()
11 .direction(Direction::Vertical)
12 .margin(1)
13 .constraints([Constraint::Min(8), Constraint::Length(3)])
14 .split(area);
15
16 let visible_height = chunks[0].height.saturating_sub(3) as usize;
17 let selected = app.settings_state.selected;
18 app.settings_state.page_size = visible_height.max(6);
19 app.sync_settings_scroll();
20 let scroll_offset = app.settings_state.scroll_offset;
21
22 let theme = &app.theme;
23 let icons = app.icons;
24 let settings_labels: Vec<(&str, String, &str)> = vec![
25 (
26 "Focus minutes",
27 format!("{} min", app.data.focus_minutes),
28 "per focus session",
29 ),
30 (
31 "Short break",
32 format!("{} min", app.data.short_break_minutes),
33 "between sessions",
34 ),
35 (
36 "Long break",
37 format!("{} min", app.data.long_break_minutes),
38 "after cycle",
39 ),
40 (
41 "Long break every",
42 format!("{} sessions", app.data.long_break_every),
43 "focus sessions per cycle",
44 ),
45 (
46 "Daily goal",
47 format!("{} min", app.data.daily_goal_minutes),
48 "+/-15 per step",
49 ),
50 (
51 "Sound on finish",
52 if app.data.sound_enabled {
53 "on".into()
54 } else {
55 "off".into()
56 },
57 "plays on completion",
58 ),
59 (
60 "Notifications",
61 if app.data.notify_on_finish {
62 "on".into()
63 } else {
64 "off".into()
65 },
66 "desktop alerts",
67 ),
68 (
69 "Auto-start breaks",
70 if app.data.auto_start_breaks {
71 "on".into()
72 } else {
73 "off".into()
74 },
75 "begin break automatically",
76 ),
77 (
78 "Auto-start focus",
79 if app.data.auto_start_focus {
80 "on".into()
81 } else {
82 "off".into()
83 },
84 "begin focus after break",
85 ),
86 (
87 "Active task",
88 app.active_task
89 .and_then(|id| app.data.tasks.iter().find(|t| t.id == id))
90 .map(|t| t.title.clone())
91 .unwrap_or_else(|| "(none)".into()),
92 "cycle with Enter",
93 ),
94 (
95 "Theme",
96 app.theme_catalog.label(&app.data.theme),
97 "cycle themes",
98 ),
99 (
100 "Custom timer",
101 format!("{} min", app.timer.custom_minutes),
102 "freeform session",
103 ),
104 (
105 "Auto-pick task",
106 if app.data.auto_pick_task {
107 "on".into()
108 } else {
109 "off".into()
110 },
111 "pick best task on start",
112 ),
113 (
114 "Auto-advance task",
115 if app.data.auto_advance_task {
116 "on".into()
117 } else {
118 "off".into()
119 },
120 "next task after focus",
121 ),
122 (
123 "When queue empty",
124 app.data.empty_queue_behavior.label().to_string(),
125 "free focus / pause / ask",
126 ),
127 (
128 "Log breaks",
129 if app.data.log_breaks {
130 "on".into()
131 } else {
132 "off".into()
133 },
134 "record break sessions",
135 ),
136 (
137 "Estimate reached",
138 app.data.estimate_complete.label().to_string(),
139 "nudge / off / auto-done",
140 ),
141 (
142 "Export backup",
143 "Enter to export".into(),
144 "writes data.json for backup",
145 ),
146 ];
147
148 let section_headers: &[(usize, &str, &str)] = &[
149 (0, icons.timer, "Timer"),
150 (5, icons.cycle, "Behavior"),
151 (9, icons.tasks, "Tasks"),
152 (10, icons.star, "Appearance"),
153 (14, icons.play, "Sessions"),
154 (17, icons.export, "Data"),
155 ];
156
157 let mut layout: Vec<SettingsRow> = Vec::new();
158 for (i, _) in settings_labels.iter().enumerate() {
159 for &(at, icon, name) in section_headers {
160 if at == i {
161 layout.push(SettingsRow::Header(icon, name));
162 }
163 }
164 layout.push(SettingsRow::Item(i));
165 }
166
167 let visible_rows: Vec<&SettingsRow> = layout
168 .iter()
169 .skip(scroll_offset)
170 .take(visible_height)
171 .collect();
172
173 let total_rows = layout.len();
174 let can_scroll = total_rows > visible_height;
175
176 let mut rows: Vec<Row> = Vec::new();
177 for entry in &visible_rows {
178 match entry {
179 SettingsRow::Header(icon, name) => {
180 rows.push(Row::new(vec![
181 Cell::from(""),
182 Cell::from(Span::styled(
183 format!("{icon} {name}"),
184 Style::default()
185 .fg(theme.accent)
186 .add_modifier(Modifier::BOLD),
187 )),
188 Cell::from(""),
189 ]));
190 }
191 SettingsRow::Item(i) => {
192 let (k, v, desc) = &settings_labels[*i];
193 let is_selected = *i == selected;
194 let marker = if is_selected { icons.chevron } else { " " };
195 let row_style = if is_selected {
196 Style::default().bg(theme.select_bg).fg(theme.select_fg)
197 } else {
198 Style::default().fg(theme.text)
199 };
200 let key_style = if is_selected {
201 Style::default()
202 .fg(theme.accent)
203 .bg(theme.select_bg)
204 .add_modifier(Modifier::BOLD)
205 } else {
206 Style::default().fg(theme.text)
207 };
208 let val_style = if is_selected {
209 Style::default().fg(theme.success).bg(theme.select_bg)
210 } else {
211 Style::default().fg(theme.dim)
212 };
213 let value_with_desc = if desc.is_empty() {
214 v.clone()
215 } else {
216 format!("{} ({})", v, desc)
217 };
218 rows.push(
219 Row::new(vec![
220 Cell::from(marker.to_string()).style(key_style),
221 Cell::from(k.to_string()).style(key_style),
222 Cell::from(value_with_desc).style(val_style),
223 ])
224 .style(row_style),
225 );
226 }
227 }
228 }
229
230 let table = Table::new(
231 rows,
232 [
233 Constraint::Length(3),
234 Constraint::Length(22),
235 Constraint::Min(10),
236 ],
237 )
238 .block(themed_panel(
239 theme,
240 Line::from(Span::styled(
241 format!(" {} Settings ", icons.settings),
242 Style::default().fg(theme.accent),
243 )),
244 ));
245 f.render_widget(table, chunks[0]);
246
247 let scroll_hint = if can_scroll {
248 format!(
249 " {} {}/{}",
250 icons.dot,
251 scroll_offset + visible_rows.len(),
252 total_rows
253 )
254 } else {
255 String::new()
256 };
257 let hint = Paragraph::new(Line::from(vec![
258 Span::styled(
259 format!("{} Up/Down", icons.chevron),
260 Style::default().fg(theme.accent),
261 ),
262 Span::styled(" scroll ", Style::default().fg(theme.dim)),
263 Span::styled("j/k", Style::default().fg(theme.accent)),
264 Span::styled(" nav ", Style::default().fg(theme.dim)),
265 Span::styled("Enter", Style::default().fg(theme.accent)),
266 Span::styled(" toggle ", Style::default().fg(theme.dim)),
267 Span::styled("+/-", Style::default().fg(theme.accent)),
268 Span::styled(" adjust", Style::default().fg(theme.dim)),
269 Span::styled(scroll_hint, Style::default().fg(theme.dim)),
270 ]))
271 .alignment(Alignment::Center);
272 f.render_widget(hint, chunks[1]);
273}