use crossterm::event::{KeyCode, KeyEvent};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use lilyco_core::schema::CommandSchema;
use crate::renderer::{self, AppState, FormRenderer};
use crate::widgets::FormField;
pub struct TuiApp {
pub form: FormRenderer,
pub should_quit: bool,
pub show_help: bool,
}
impl TuiApp {
pub fn new(schema: &CommandSchema) -> Self {
Self {
form: FormRenderer::new(schema),
should_quit: false,
show_help: false,
}
}
pub fn state(&self) -> &AppState {
&self.form.app_state
}
pub fn fields_mut(&mut self) -> &mut Vec<FormField> {
&mut self.form.fields
}
pub fn fields(&self) -> &[FormField] {
&self.form.fields
}
pub fn handle_event(&mut self, key: KeyEvent) -> bool {
match self.form.app_state {
AppState::Form => self.handle_form_event(key),
AppState::Confirm => self.handle_confirm_event(key),
AppState::Running => self.handle_running_event(key),
AppState::Done | AppState::Error => self.handle_terminal_event(key),
}
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
match self.form.app_state {
AppState::Form | AppState::Running => {
renderer::render_form(&self.form, area, buf);
if self.show_help {
render_help_overlay(area, buf);
}
}
AppState::Confirm => {
renderer::render_form(&self.form, area, buf);
renderer::render_confirm(&self.form, area, buf);
}
AppState::Done => {
renderer::render_done(&self.form, area, buf);
}
AppState::Error => {
renderer::render_error(&self.form, area, buf);
}
}
}
fn handle_form_event(&mut self, key: KeyEvent) -> bool {
use KeyCode::*;
match key.code {
Esc => {
self.should_quit = true;
return false;
}
Tab => {
if self.show_help {
self.show_help = false;
} else {
self.form.next_field();
}
return true;
}
BackTab => {
self.form.prev_field();
return true;
}
Enter => {
if self.show_help {
self.show_help = false;
return true;
}
if self.form.all_required_filled() {
self.form.app_state = AppState::Confirm;
}
return true;
}
F(1) => {
self.show_help = !self.show_help;
return true;
}
_ => {}
}
if let Some(field) = self.form.focused_field_mut() {
let changed = field.handle_key(key);
if changed {
self.update_scroll();
}
}
true
}
fn handle_confirm_event(&mut self, key: KeyEvent) -> bool {
match key.code {
KeyCode::Enter => {
self.form.app_state = AppState::Running;
true
}
KeyCode::Esc => {
self.form.app_state = AppState::Form;
true
}
_ => true,
}
}
fn handle_running_event(&mut self, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char('c') | KeyCode::Char('C') => {
self.form.app_state = AppState::Error;
self.form.error_message = Some("用户取消".into());
true
}
_ => true,
}
}
fn handle_terminal_event(&mut self, _key: KeyEvent) -> bool {
self.should_quit = true;
false
}
fn update_scroll(&mut self) {
let fi = self.form.focus_index as u16;
if fi < self.form.scroll_offset {
self.form.scroll_offset = fi;
}
}
pub fn start_progress(&mut self, _total: Option<u64>, message: Option<String>) {
self.form.progress_percent = None;
self.form.elapsed_ms = 0;
if let Some(msg) = message {
self.form.progress_log.push(msg);
}
}
pub fn tick_progress(&mut self, current: u64, total: Option<u64>, message: Option<String>) {
if let Some(t) = total {
self.form.progress_percent = Some(current as f32 / t as f32);
}
if let Some(msg) = message {
self.form.progress_log.push(msg);
}
}
pub fn log_progress(&mut self, level: &str, message: String) {
self.form.progress_log.push(format!("[{level}] {message}"));
}
pub fn finish_progress(&mut self, result: serde_json::Value, duration_ms: u64) {
self.form.elapsed_ms = duration_ms;
self.form.result = Some(result);
self.form.app_state = AppState::Done;
}
pub fn error_progress(&mut self, code: i32, message: String) {
self.form.error_message = Some(format!("[E{code}] {message}"));
self.form.app_state = AppState::Error;
}
pub fn cancel(&mut self) {
self.form.error_message = Some("已取消".into());
self.form.app_state = AppState::Error;
}
}
fn render_help_overlay(area: Rect, buf: &mut Buffer) {
use ratatui::style::{Color, Style};
let text = "\
快捷键帮助
Tab / Shift+Tab 切换字段焦点
↑↓ 数字加减
←→ Enum 选项切换
空格 Flag 切换
Enter 确认执行
Esc 退出
F1 关闭帮助
";
let w = 36u16;
let h = 12u16;
let x = area.x + (area.width.saturating_sub(w)) / 2;
let y = area.y + (area.height.saturating_sub(h)) / 2;
let dark = Style::default().bg(Color::DarkGray);
for dy in 0..h {
buf.set_string(x, y + dy, &" ".repeat(w as usize), dark);
}
for (i, line) in text.lines().enumerate() {
let trimmed = line.trim();
if !trimmed.is_empty() {
buf.set_string(x + 2, y + 1 + i as u16, trimmed, Style::default().fg(Color::White).bg(Color::DarkGray));
}
}
}