use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
Frame,
};
use std::collections::HashMap;
use crate::commands::scaffolds::{ScaffoldRegistry, ScaffoldArgs, ScaffoldResult, FieldSpec};
use crate::tui::app::AppAction;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Focus {
ScaffoldList,
NameInput,
ModelInput,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Screen {
List,
Result,
}
pub struct CodeGenTab {
registry: ScaffoldRegistry,
scaffold_names: Vec<&'static str>,
scaffold_descs: Vec<&'static str>,
list_state: ListState,
filter: String,
filter_active: bool,
focus: Focus,
input_name: String,
input_model: String,
result: Option<ScaffoldResult>,
screen: Screen,
status_message: String,
status_timer: u8,
}
impl Default for CodeGenTab {
fn default() -> Self {
let registry = ScaffoldRegistry::new();
let scaffold_list = registry.list();
let names: Vec<&'static str> = scaffold_list.iter().map(|(n, _)| *n).collect();
let descs: Vec<&'static str> = scaffold_list.iter().map(|(_, d)| *d).collect();
let mut state = ListState::default();
state.select(Some(0));
Self {
registry,
scaffold_names: names,
scaffold_descs: descs,
list_state: state,
filter: String::new(),
filter_active: false,
focus: Focus::ScaffoldList,
input_name: String::new(),
input_model: String::new(),
result: None,
screen: Screen::List,
status_message: String::new(),
status_timer: 0,
}
}
}
impl CodeGenTab {
pub async fn load(&mut self, _pool: &sqlx::PgPool) {
}
fn filtered_indices(&self) -> Vec<usize> {
if self.filter.is_empty() {
(0..self.scaffold_names.len()).collect()
} else {
let f = self.filter.to_lowercase();
self.scaffold_names
.iter()
.enumerate()
.filter(|(_, n)| n.to_lowercase().contains(&f))
.map(|(i, _)| i)
.collect()
}
}
fn selected_scaffold(&self) -> Option<&'static str> {
self.list_state.selected().and_then(|sel| {
let indices = self.filtered_indices();
indices.get(sel).copied().map(|i| self.scaffold_names[i])
})
}
fn execute_generate(&mut self) {
let name = self.selected_scaffold();
let scaffold = match name.and_then(|n| self.registry.get(n)) {
Some(s) => s,
None => {
self.status_message = "No scaffold selected".into();
self.status_timer = 5;
return;
}
};
let flags = HashMap::new();
let mut fields: Vec<FieldSpec> = Vec::new();
if !self.input_fields_raw().is_empty() {
for part in self.input_fields_raw().split(',') {
let part = part.trim();
if part.is_empty() { continue; }
let (fname, ftype) = if let Some(idx) = part.find(':') {
(&part[..idx], &part[idx+1..])
} else {
(part, "string")
};
fields.push(FieldSpec {
name: fname.to_string(),
r#type: ftype.to_string(),
validations: vec![],
});
}
}
let args = ScaffoldArgs {
name: if self.input_name.is_empty() { None } else { Some(self.input_name.clone()) },
model: if self.input_model.is_empty() { None } else { Some(self.input_model.clone()) },
fields,
flags,
dry_run: false,
json: false,
};
match scaffold.generate(&args) {
Ok(result) => {
self.result = Some(result);
self.screen = Screen::Result;
self.status_message = "Scaffold generated successfully".into();
self.status_timer = 5;
}
Err(e) => {
self.status_message = format!("Error: {e}");
self.status_timer = 8;
}
}
}
fn input_fields_raw(&self) -> &str {
""
}
fn clear(&mut self) {
self.input_name.clear();
self.input_model.clear();
self.result = None;
self.screen = Screen::List;
self.status_message.clear();
}
pub fn render(&mut self, frame: &mut Frame, area: Rect) {
match self.screen {
Screen::List => self.render_list(frame, area),
Screen::Result => self.render_result(frame, area),
}
}
fn render_list(&mut self, frame: &mut Frame, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(2, 5), Constraint::Ratio(3, 5)])
.split(area);
self.render_scaffold_list(frame, chunks[0]);
self.render_detail(frame, chunks[1]);
}
fn render_scaffold_list(&mut self, frame: &mut Frame, area: Rect) {
let indices = self.filtered_indices();
let items: Vec<ListItem> = indices
.iter()
.map(|i| {
let name = self.scaffold_names[*i];
let desc = self.scaffold_descs[*i];
ListItem::new(Line::from(vec![
Span::styled(
format!(" {:20}", name),
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(desc, Style::default().fg(Color::White)),
]))
})
.collect();
let title = if self.filter_active {
format!(" Scaffolds (filter: {}) ", self.filter)
} else {
" Scaffolds [/ to filter] ".into()
};
let list = List::new(items)
.block(Block::default().borders(Borders::ALL).title(title.as_str()))
.highlight_style(
Style::default()
.bg(if self.focus == Focus::ScaffoldList {
Color::DarkGray
} else {
Color::Reset
})
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("â–¶ ");
let mut state = self.list_state.clone();
frame.render_stateful_widget(list, area, &mut state);
}
fn render_detail(&mut self, frame: &mut Frame, area: Rect) {
let inner = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(6),
Constraint::Min(3),
Constraint::Length(4),
])
.split(area);
let input_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(3)])
.margin(1)
.split(inner[0]);
let name_style = if self.focus == Focus::NameInput {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::White)
};
let model_style = if self.focus == Focus::ModelInput {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::White)
};
let name_input = Paragraph::new(if self.input_name.is_empty() {
" (optional) scaffold name"
} else {
self.input_name.as_str()
})
.style(name_style)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Name "),
);
let model_input = Paragraph::new(if self.input_model.is_empty() {
" (optional) model name"
} else {
self.input_model.as_str()
})
.style(model_style)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Model "),
);
frame.render_widget(name_input, input_chunks[0]);
frame.render_widget(model_input, input_chunks[1]);
let selected = self.selected_scaffold();
let desc_text = selected
.and_then(|n| {
self.scaffold_names
.iter()
.position(|s| *s == n)
.map(|i| self.scaffold_descs[i])
})
.unwrap_or("Select a scaffold from the list");
let help_lines = vec![
Line::from(Span::raw("")),
Line::from(vec![
Span::styled("Description: ", Style::default().fg(Color::Cyan)),
Span::raw(desc_text),
]),
Line::from(Span::raw("")),
Line::from(vec![
Span::styled("Tips: ", Style::default().fg(Color::Cyan)),
Span::raw("fields format: "),
Span::styled("title:string,body:text,published:bool", Style::default().fg(Color::Green)),
]),
];
let desc = Paragraph::new(help_lines)
.block(Block::default().borders(Borders::ALL).title(" Info "))
.style(Style::default().fg(Color::White));
frame.render_widget(desc, inner[1]);
let actions = Paragraph::new(Line::from(vec![
Span::styled(" Enter ", Style::default().fg(Color::Green).bg(Color::Reset)),
Span::raw(" Generate "),
Span::styled(" Tab ", Style::default().fg(Color::Cyan)),
Span::raw(" Switch focus "),
Span::styled(" / ", Style::default().fg(Color::Cyan)),
Span::raw(" Filter "),
Span::styled(" Esc ", Style::default().fg(Color::Red)),
Span::raw(" Clear "),
]))
.block(Block::default().borders(Borders::ALL).title(" Actions "));
frame.render_widget(actions, inner[2]);
}
fn render_result(&mut self, frame: &mut Frame, area: Rect) {
let result = match &self.result {
Some(r) => r,
None => {
self.screen = Screen::List;
return;
}
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(4)])
.split(area);
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::from(vec![
Span::styled(" Files Created ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(Span::raw("")));
for f in &result.files_created {
let style = if f.ends_with('/') {
Style::default().fg(Color::Cyan)
} else {
Style::default().fg(Color::White)
};
lines.push(Line::from(Span::styled(format!(" {f}"), style)));
}
if !result.dirs_created.is_empty() {
lines.push(Line::from(Span::raw("")));
lines.push(Line::from(vec![
Span::styled(" Directories Created ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(Span::raw("")));
for d in &result.dirs_created {
lines.push(Line::from(Span::styled(format!(" {d}/"), Style::default().fg(Color::Cyan))));
}
}
if !result.warnings.is_empty() {
lines.push(Line::from(Span::raw("")));
lines.push(Line::from(vec![
Span::styled(" Warnings ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
]));
lines.push(Line::from(Span::raw("")));
for w in &result.warnings {
lines.push(Line::from(Span::styled(format!(" âš {w}"), Style::default().fg(Color::Yellow))));
}
}
let list = ratatui::widgets::List::new(lines)
.block(Block::default().borders(Borders::ALL).title(" Generation Result "));
frame.render_widget(list, chunks[0]);
let actions = Paragraph::new(Line::from(vec![
Span::styled(" Enter ", Style::default().fg(Color::Green)),
Span::raw(" Generate again "),
Span::styled(" Esc ", Style::default().fg(Color::Red)),
Span::raw(" Back to list "),
]))
.block(Block::default().borders(Borders::ALL).title(" Actions "));
frame.render_widget(actions, chunks[1]);
}
pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> AppAction {
match self.screen {
Screen::List => self.handle_list_key(key),
Screen::Result => self.handle_result_key(key),
}
}
fn handle_list_key(&mut self, key: crossterm::event::KeyEvent) -> AppAction {
use crossterm::event::{KeyCode, KeyModifiers};
if self.filter_active {
match key.code {
KeyCode::Esc => {
self.filter_active = false;
self.filter.clear();
}
KeyCode::Enter => {
self.filter_active = false;
}
KeyCode::Char(c) => {
self.filter.push(c);
}
KeyCode::Backspace => {
self.filter.pop();
}
_ => {}
}
return AppAction::None;
}
match key.code {
KeyCode::Up => {
let i = self.list_state.selected().unwrap_or(0).saturating_sub(1);
self.list_state.select(Some(i));
}
KeyCode::Down => {
let indices = self.filtered_indices();
let max = indices.len().saturating_sub(1);
let i = (self.list_state.selected().unwrap_or(0) + 1).min(max);
self.list_state.select(Some(i));
}
KeyCode::Enter => {
match self.focus {
Focus::ScaffoldList => {
if self.selected_scaffold().is_some() {
self.execute_generate();
}
}
Focus::NameInput | Focus::ModelInput => {
if self.selected_scaffold().is_some() {
self.execute_generate();
}
}
}
}
KeyCode::Tab => {
self.focus = match self.focus {
Focus::ScaffoldList => Focus::NameInput,
Focus::NameInput => Focus::ModelInput,
Focus::ModelInput => Focus::ScaffoldList,
};
}
KeyCode::Char('/') => {
self.filter_active = true;
self.filter.clear();
}
KeyCode::Char(c) if self.focus == Focus::NameInput => {
if !key.modifiers.contains(KeyModifiers::CONTROL) {
self.input_name.push(c);
}
}
KeyCode::Backspace if self.focus == Focus::NameInput => {
self.input_name.pop();
}
KeyCode::Char(c) if self.focus == Focus::ModelInput => {
if !key.modifiers.contains(KeyModifiers::CONTROL) {
self.input_model.push(c);
}
}
KeyCode::Backspace if self.focus == Focus::ModelInput => {
self.input_model.pop();
}
KeyCode::Esc => {
self.clear();
}
KeyCode::F(5) => {
self.execute_generate();
}
_ => {}
}
AppAction::None
}
fn handle_result_key(&mut self, key: crossterm::event::KeyEvent) -> AppAction {
use crossterm::event::KeyCode;
match key.code {
KeyCode::Esc => {
self.screen = Screen::List;
}
KeyCode::Enter => {
self.screen = Screen::List;
}
_ => {}
}
AppAction::None
}
}