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 ("Theme", app.data.theme.label().to_string(), "cycle themes"),
95 (
96 "Custom timer",
97 format!("{} min", app.timer.custom_minutes),
98 "freeform session",
99 ),
100 (
101 "Auto-pick task",
102 if app.data.auto_pick_task {
103 "on".into()
104 } else {
105 "off".into()
106 },
107 "pick best task on start",
108 ),
109 (
110 "Auto-advance task",
111 if app.data.auto_advance_task {
112 "on".into()
113 } else {
114 "off".into()
115 },
116 "next task after focus",
117 ),
118 (
119 "When queue empty",
120 app.data.empty_queue_behavior.label().to_string(),
121 "free focus / pause / ask",
122 ),
123 (
124 "Log breaks",
125 if app.data.log_breaks {
126 "on".into()
127 } else {
128 "off".into()
129 },
130 "record break sessions",
131 ),
132 (
133 "Estimate reached",
134 app.data.estimate_complete.label().to_string(),
135 "nudge / off / auto-done",
136 ),
137 (
138 "Export backup",
139 "Enter to export".into(),
140 "writes data.json for backup",
141 ),
142 ];
143
144 let section_headers: &[(usize, &str, &str)] = &[
145 (0, icons.timer, "Timer"),
146 (5, icons.cycle, "Behavior"),
147 (9, icons.tasks, "Tasks"),
148 (10, icons.star, "Appearance"),
149 (14, icons.play, "Sessions"),
150 (17, icons.export, "Data"),
151 ];
152
153 let mut layout: Vec<SettingsRow> = Vec::new();
154 for (i, _) in settings_labels.iter().enumerate() {
155 for &(at, icon, name) in section_headers {
156 if at == i {
157 layout.push(SettingsRow::Header(icon, name));
158 }
159 }
160 layout.push(SettingsRow::Item(i));
161 }
162
163 let visible_rows: Vec<&SettingsRow> = layout
164 .iter()
165 .skip(scroll_offset)
166 .take(visible_height)
167 .collect();
168
169 let total_rows = layout.len();
170 let can_scroll = total_rows > visible_height;
171
172 let mut rows: Vec<Row> = Vec::new();
173 for entry in &visible_rows {
174 match entry {
175 SettingsRow::Header(icon, name) => {
176 rows.push(Row::new(vec![
177 Cell::from(""),
178 Cell::from(Span::styled(
179 format!("{icon} {name}"),
180 Style::default()
181 .fg(theme.accent)
182 .add_modifier(Modifier::BOLD),
183 )),
184 Cell::from(""),
185 ]));
186 }
187 SettingsRow::Item(i) => {
188 let (k, v, desc) = &settings_labels[*i];
189 let is_selected = *i == selected;
190 let marker = if is_selected { icons.chevron } else { " " };
191 let row_style = if is_selected {
192 Style::default().bg(theme.select_bg).fg(theme.select_fg)
193 } else {
194 Style::default().fg(theme.text)
195 };
196 let key_style = if is_selected {
197 Style::default()
198 .fg(theme.accent)
199 .bg(theme.select_bg)
200 .add_modifier(Modifier::BOLD)
201 } else {
202 Style::default().fg(theme.text)
203 };
204 let val_style = if is_selected {
205 Style::default().fg(theme.success).bg(theme.select_bg)
206 } else {
207 Style::default().fg(theme.dim)
208 };
209 let value_with_desc = if desc.is_empty() {
210 v.clone()
211 } else {
212 format!("{} ({})", v, desc)
213 };
214 rows.push(
215 Row::new(vec![
216 Cell::from(marker.to_string()).style(key_style),
217 Cell::from(k.to_string()).style(key_style),
218 Cell::from(value_with_desc).style(val_style),
219 ])
220 .style(row_style),
221 );
222 }
223 }
224 }
225
226 let table = Table::new(
227 rows,
228 [
229 Constraint::Length(3),
230 Constraint::Length(22),
231 Constraint::Min(10),
232 ],
233 )
234 .block(themed_panel(
235 theme,
236 Line::from(Span::styled(
237 format!(" {} Settings ", icons.settings),
238 Style::default().fg(theme.accent),
239 )),
240 ));
241 f.render_widget(table, chunks[0]);
242
243 let scroll_hint = if can_scroll {
244 format!(
245 " {} {}/{}",
246 icons.dot,
247 scroll_offset + visible_rows.len(),
248 total_rows
249 )
250 } else {
251 String::new()
252 };
253 let hint = Paragraph::new(Line::from(vec![
254 Span::styled(
255 format!("{} Up/Down", icons.chevron),
256 Style::default().fg(theme.accent),
257 ),
258 Span::styled(" scroll ", Style::default().fg(theme.dim)),
259 Span::styled("j/k", Style::default().fg(theme.accent)),
260 Span::styled(" nav ", Style::default().fg(theme.dim)),
261 Span::styled("Enter", Style::default().fg(theme.accent)),
262 Span::styled(" toggle ", Style::default().fg(theme.dim)),
263 Span::styled("+/-", Style::default().fg(theme.accent)),
264 Span::styled(" adjust", Style::default().fg(theme.dim)),
265 Span::styled(scroll_hint, Style::default().fg(theme.dim)),
266 ]))
267 .alignment(Alignment::Center);
268 f.render_widget(hint, chunks[1]);
269}