1use ratatui::Frame;
10use ratatui::layout::Rect;
11use ratatui::style::{Color, Modifier, Style};
12use ratatui::text::Line;
13use ratatui::widgets::{Block, Borders, Clear, Paragraph};
14
15use super::modal::modal_rect;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum SettingsPrompt {
20 AddColumn(String),
22 NewBoard(String),
24 ConfirmDeleteBoard(String),
26}
27
28#[derive(Debug, Clone, Default)]
31pub struct SettingsModal {
32 pub selected: usize,
34 pub prompt: Option<SettingsPrompt>,
36}
37
38impl SettingsModal {
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn move_selected(&mut self, delta: isize, column_count: usize) {
45 if column_count == 0 {
46 self.selected = 0;
47 return;
48 }
49 let next = self.selected as isize + delta;
50 self.selected = next.clamp(0, column_count as isize - 1) as usize;
51 }
52
53 pub fn render(
55 &self,
56 f: &mut Frame,
57 area: Rect,
58 board_name: &str,
59 columns: &[String],
60 hidden: &[String],
61 ) {
62 let rect = modal_rect(area);
63 let mut rows: Vec<Line> = Vec::new();
64
65 for (i, col) in columns.iter().enumerate() {
66 let is_hidden = hidden.iter().any(|h| h == col);
67 let mark = if is_hidden { "[ ]" } else { "[x]" };
68 let cursor = if i == self.selected { "> " } else { " " };
69 let style = if i == self.selected {
70 Style::default()
71 .fg(Color::Yellow)
72 .add_modifier(Modifier::BOLD)
73 } else {
74 Style::default().fg(Color::White)
75 };
76 let hidden_suffix = if is_hidden { " (hidden)" } else { "" };
77 rows.push(Line::from(vec![
78 ratatui::text::Span::styled(cursor, style),
79 ratatui::text::Span::styled(format!("{mark} {col}{hidden_suffix}"), style),
80 ]));
81 }
82
83 if let Some(prompt) = &self.prompt {
84 rows.push(Line::from(""));
85 let line = match prompt {
86 SettingsPrompt::AddColumn(buffer) => format!("New column: {buffer}_"),
87 SettingsPrompt::NewBoard(buffer) => format!("New board: {buffer}_"),
88 SettingsPrompt::ConfirmDeleteBoard(name) => {
89 format!("Delete board '{name}'? [y/N]")
90 }
91 };
92 rows.push(Line::from(ratatui::text::Span::styled(
93 line,
94 Style::default().fg(Color::Cyan),
95 )));
96 } else {
97 rows.push(Line::from(""));
98 rows.push(Line::from(ratatui::text::Span::styled(
99 "Space: toggle · a: new column · n: new board · x: delete board · Tab: switch · q: close",
100 Style::default().fg(Color::DarkGray),
101 )));
102 }
103
104 let block = Block::default()
105 .borders(Borders::ALL)
106 .title(format!(" Settings — {board_name} "))
107 .style(Style::default().fg(Color::Cyan));
108 let para = Paragraph::new(rows).block(block).style(Style::default());
109 f.render_widget(Clear, rect);
110 f.render_widget(para, rect);
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use ratatui::Terminal;
118 use ratatui::backend::TestBackend;
119
120 fn render_smoke(prompt: Option<SettingsPrompt>, w: u16, h: u16) {
121 let backend = TestBackend::new(w, h);
122 let mut terminal = Terminal::new(backend).unwrap();
123 let modal = SettingsModal {
124 selected: 1,
125 prompt,
126 };
127 let columns = vec!["todo".to_string(), "done".to_string()];
128 let hidden = vec!["done".to_string()];
129 terminal
130 .draw(|f| modal.render(f, f.area(), "default", &columns, &hidden))
131 .unwrap();
132 }
133
134 #[test]
135 fn renders_at_80x24() {
136 render_smoke(None, 80, 24);
137 }
138
139 #[test]
140 fn renders_prompt_at_80x24() {
141 render_smoke(Some(SettingsPrompt::AddColumn("".into())), 80, 24);
142 }
143
144 #[test]
145 fn renders_at_tiny_20x5() {
146 render_smoke(None, 20, 5);
147 }
148
149 #[test]
150 fn move_selected_clamps_within_columns() {
151 let mut modal = SettingsModal::new();
152 modal.move_selected(-1, 3);
153 assert_eq!(modal.selected, 0);
154 modal.move_selected(5, [0usize, 1, 2, 3].len());
155 assert_eq!(modal.selected, 3);
156 modal.move_selected(1, 0);
157 assert_eq!(modal.selected, 0);
158 }
159
160 #[test]
161 fn defaults_to_first_row() {
162 assert_eq!(SettingsModal::new().selected, 0);
163 assert_eq!(SettingsModal::new().prompt, None);
164 }
165}