1use ratatui::Frame;
2use ratatui::layout::Rect;
3use ratatui::style::{Color, Modifier, Style};
4use ratatui::text::{Line, Span};
5use ratatui::widgets::{Block, Borders, Paragraph};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FormFocus {
9 Title,
10 Status,
11 Priority,
12 Labels,
13 Assignee,
14 Estimate,
15}
16
17impl FormFocus {
18 pub fn next(self) -> Self {
19 match self {
20 FormFocus::Title => FormFocus::Status,
21 FormFocus::Status => FormFocus::Priority,
22 FormFocus::Priority => FormFocus::Labels,
23 FormFocus::Labels => FormFocus::Assignee,
24 FormFocus::Assignee => FormFocus::Estimate,
25 FormFocus::Estimate => FormFocus::Title,
26 }
27 }
28
29 pub fn prev(self) -> Self {
30 match self {
31 FormFocus::Title => FormFocus::Estimate,
32 FormFocus::Status => FormFocus::Title,
33 FormFocus::Priority => FormFocus::Status,
34 FormFocus::Labels => FormFocus::Priority,
35 FormFocus::Assignee => FormFocus::Labels,
36 FormFocus::Estimate => FormFocus::Assignee,
37 }
38 }
39
40 pub fn label(self) -> &'static str {
41 match self {
42 FormFocus::Title => "Title",
43 FormFocus::Status => "Status",
44 FormFocus::Priority => "Priority",
45 FormFocus::Labels => "Labels",
46 FormFocus::Assignee => "Assignee",
47 FormFocus::Estimate => "Estimate",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
53pub struct FormState {
54 pub title: String,
55 pub status: String,
56 pub priority: String,
57 pub labels: String,
58 pub assignee: String,
59 pub estimate: String,
60 pub focus: FormFocus,
61}
62
63impl FormState {
64 pub fn new(default_status: &str) -> Self {
65 Self::with_default_assignee(default_status, "")
66 }
67
68 pub fn with_default_assignee(default_status: &str, default_assignee: &str) -> Self {
69 FormState {
70 title: String::new(),
71 status: default_status.to_string(),
72 priority: String::new(),
73 labels: String::new(),
74 assignee: default_assignee.to_string(),
75 estimate: String::new(),
76 focus: FormFocus::Title,
77 }
78 }
79
80 pub fn focused_value(&self) -> &str {
81 match self.focus {
82 FormFocus::Title => &self.title,
83 FormFocus::Status => &self.status,
84 FormFocus::Priority => &self.priority,
85 FormFocus::Labels => &self.labels,
86 FormFocus::Assignee => &self.assignee,
87 FormFocus::Estimate => &self.estimate,
88 }
89 }
90
91 pub fn focused_value_mut(&mut self) -> &mut String {
92 match self.focus {
93 FormFocus::Title => &mut self.title,
94 FormFocus::Status => &mut self.status,
95 FormFocus::Priority => &mut self.priority,
96 FormFocus::Labels => &mut self.labels,
97 FormFocus::Assignee => &mut self.assignee,
98 FormFocus::Estimate => &mut self.estimate,
99 }
100 }
101
102 pub fn insert_char(&mut self, c: char) {
103 let value = self.focused_value_mut();
104 value.push(c);
105 }
106
107 pub fn delete_char(&mut self) {
108 let value = self.focused_value_mut();
109 value.pop();
110 }
111
112 pub fn move_focus_next(&mut self) {
113 self.focus = self.focus.next();
114 }
115
116 pub fn move_focus_prev(&mut self) {
117 self.focus = self.focus.prev();
118 }
119}
120
121pub fn render(form: &FormState, f: &mut Frame, area: Rect) {
122 let focus_idx = match form.focus {
123 FormFocus::Title => 0,
124 FormFocus::Status => 1,
125 FormFocus::Priority => 2,
126 FormFocus::Labels => 3,
127 FormFocus::Assignee => 4,
128 FormFocus::Estimate => 5,
129 };
130
131 let fields = [
132 ("Title", form.title.as_str()),
133 ("Status", form.status.as_str()),
134 ("Priority", form.priority.as_str()),
135 ("Labels", form.labels.as_str()),
136 ("Assignee", form.assignee.as_str()),
137 ("Estimate", form.estimate.as_str()),
138 ];
139
140 let mut lines: Vec<Line<'static>> = Vec::new();
141 for (i, (label, value)) in fields.iter().enumerate() {
142 let cursor = if i == focus_idx { "> " } else { " " };
143 let label_style = if i == focus_idx {
144 Style::default()
145 .add_modifier(Modifier::BOLD)
146 .fg(Color::Cyan)
147 } else {
148 Style::default()
149 };
150 let (display, value_style) = if value.is_empty() && i != focus_idx {
151 ("(empty)".to_string(), Style::default().fg(Color::DarkGray))
152 } else if i == focus_idx {
153 (
154 value.to_string(),
155 Style::default().add_modifier(Modifier::UNDERLINED),
156 )
157 } else {
158 (value.to_string(), Style::default())
159 };
160 lines.push(Line::from(vec![
161 Span::styled(format!("{cursor}{label}: "), label_style),
162 Span::styled(display, value_style),
163 ]));
164 }
165
166 lines.push(Line::from(""));
167 lines.push(Line::from(Span::styled(
168 "Enter: Submit | Esc: Cancel | Tab: Next field",
169 Style::default().fg(Color::DarkGray),
170 )));
171
172 let para =
173 Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title("Create Issue"));
174
175 f.render_widget(para, area);
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn focus_cycles_forward() {
184 assert_eq!(FormFocus::Title.next(), FormFocus::Status);
185 assert_eq!(FormFocus::Estimate.next(), FormFocus::Title);
186 }
187
188 #[test]
189 fn focus_cycles_backward() {
190 assert_eq!(FormFocus::Title.prev(), FormFocus::Estimate);
191 assert_eq!(FormFocus::Status.prev(), FormFocus::Title);
192 }
193
194 #[test]
195 fn form_insert_and_delete() {
196 let mut form = FormState::new("todo");
197 form.insert_char('h');
198 form.insert_char('i');
199 assert_eq!(form.title, "hi");
200 form.delete_char();
201 assert_eq!(form.title, "h");
202 }
203
204 #[test]
205 fn form_move_focus() {
206 let mut form = FormState::new("todo");
207 assert_eq!(form.focus, FormFocus::Title);
208 form.move_focus_next();
209 assert_eq!(form.focus, FormFocus::Status);
210 form.move_focus_prev();
211 assert_eq!(form.focus, FormFocus::Title);
212 }
213
214 #[test]
215 fn render_smoke() {
216 use ratatui::Terminal;
217 use ratatui::backend::TestBackend;
218
219 let backend = TestBackend::new(40, 12);
220 let mut terminal = Terminal::new(backend).unwrap();
221 let form = FormState::new("todo");
222 terminal.draw(|f| render(&form, f, f.area())).unwrap();
223 }
224
225 #[test]
226 fn with_default_assignee_populates_assignee_field() {
227 let form = FormState::with_default_assignee("in progress", "alice");
228 assert_eq!(form.assignee, "alice");
229 assert_eq!(form.status, "in progress");
230 assert_eq!(form.focus, FormFocus::Title);
231 }
232}