use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormFocus {
Title,
Status,
Priority,
Labels,
Assignee,
Estimate,
}
impl FormFocus {
pub fn next(self) -> Self {
match self {
FormFocus::Title => FormFocus::Status,
FormFocus::Status => FormFocus::Priority,
FormFocus::Priority => FormFocus::Labels,
FormFocus::Labels => FormFocus::Assignee,
FormFocus::Assignee => FormFocus::Estimate,
FormFocus::Estimate => FormFocus::Title,
}
}
pub fn prev(self) -> Self {
match self {
FormFocus::Title => FormFocus::Estimate,
FormFocus::Status => FormFocus::Title,
FormFocus::Priority => FormFocus::Status,
FormFocus::Labels => FormFocus::Priority,
FormFocus::Assignee => FormFocus::Labels,
FormFocus::Estimate => FormFocus::Assignee,
}
}
pub fn label(self) -> &'static str {
match self {
FormFocus::Title => "Title",
FormFocus::Status => "Status",
FormFocus::Priority => "Priority",
FormFocus::Labels => "Labels",
FormFocus::Assignee => "Assignee",
FormFocus::Estimate => "Estimate",
}
}
}
#[derive(Debug, Clone)]
pub struct FormState {
pub title: String,
pub status: String,
pub priority: String,
pub labels: String,
pub assignee: String,
pub estimate: String,
pub focus: FormFocus,
}
impl FormState {
pub fn new(default_status: &str) -> Self {
Self::with_default_assignee(default_status, "")
}
pub fn with_default_assignee(default_status: &str, default_assignee: &str) -> Self {
FormState {
title: String::new(),
status: default_status.to_string(),
priority: String::new(),
labels: String::new(),
assignee: default_assignee.to_string(),
estimate: String::new(),
focus: FormFocus::Title,
}
}
pub fn focused_value(&self) -> &str {
match self.focus {
FormFocus::Title => &self.title,
FormFocus::Status => &self.status,
FormFocus::Priority => &self.priority,
FormFocus::Labels => &self.labels,
FormFocus::Assignee => &self.assignee,
FormFocus::Estimate => &self.estimate,
}
}
pub fn focused_value_mut(&mut self) -> &mut String {
match self.focus {
FormFocus::Title => &mut self.title,
FormFocus::Status => &mut self.status,
FormFocus::Priority => &mut self.priority,
FormFocus::Labels => &mut self.labels,
FormFocus::Assignee => &mut self.assignee,
FormFocus::Estimate => &mut self.estimate,
}
}
pub fn insert_char(&mut self, c: char) {
let value = self.focused_value_mut();
value.push(c);
}
pub fn delete_char(&mut self) {
let value = self.focused_value_mut();
value.pop();
}
pub fn move_focus_next(&mut self) {
self.focus = self.focus.next();
}
pub fn move_focus_prev(&mut self) {
self.focus = self.focus.prev();
}
}
pub fn render(form: &FormState, f: &mut Frame, area: Rect) {
let focus_idx = match form.focus {
FormFocus::Title => 0,
FormFocus::Status => 1,
FormFocus::Priority => 2,
FormFocus::Labels => 3,
FormFocus::Assignee => 4,
FormFocus::Estimate => 5,
};
let fields = [
("Title", form.title.as_str()),
("Status", form.status.as_str()),
("Priority", form.priority.as_str()),
("Labels", form.labels.as_str()),
("Assignee", form.assignee.as_str()),
("Estimate", form.estimate.as_str()),
];
let mut lines: Vec<Line<'static>> = Vec::new();
for (i, (label, value)) in fields.iter().enumerate() {
let cursor = if i == focus_idx { "> " } else { " " };
let label_style = if i == focus_idx {
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Cyan)
} else {
Style::default()
};
let (display, value_style) = if value.is_empty() && i != focus_idx {
("(empty)".to_string(), Style::default().fg(Color::DarkGray))
} else if i == focus_idx {
(
value.to_string(),
Style::default().add_modifier(Modifier::UNDERLINED),
)
} else {
(value.to_string(), Style::default())
};
lines.push(Line::from(vec![
Span::styled(format!("{cursor}{label}: "), label_style),
Span::styled(display, value_style),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Enter: Submit | Esc: Cancel | Tab: Next field",
Style::default().fg(Color::DarkGray),
)));
let para =
Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title("Create Issue"));
f.render_widget(para, area);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focus_cycles_forward() {
assert_eq!(FormFocus::Title.next(), FormFocus::Status);
assert_eq!(FormFocus::Estimate.next(), FormFocus::Title);
}
#[test]
fn focus_cycles_backward() {
assert_eq!(FormFocus::Title.prev(), FormFocus::Estimate);
assert_eq!(FormFocus::Status.prev(), FormFocus::Title);
}
#[test]
fn form_insert_and_delete() {
let mut form = FormState::new("todo");
form.insert_char('h');
form.insert_char('i');
assert_eq!(form.title, "hi");
form.delete_char();
assert_eq!(form.title, "h");
}
#[test]
fn form_move_focus() {
let mut form = FormState::new("todo");
assert_eq!(form.focus, FormFocus::Title);
form.move_focus_next();
assert_eq!(form.focus, FormFocus::Status);
form.move_focus_prev();
assert_eq!(form.focus, FormFocus::Title);
}
#[test]
fn render_smoke() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let backend = TestBackend::new(40, 12);
let mut terminal = Terminal::new(backend).unwrap();
let form = FormState::new("todo");
terminal.draw(|f| render(&form, f, f.area())).unwrap();
}
#[test]
fn with_default_assignee_populates_assignee_field() {
let form = FormState::with_default_assignee("in progress", "alice");
assert_eq!(form.assignee, "alice");
assert_eq!(form.status, "in progress");
assert_eq!(form.focus, FormFocus::Title);
}
}