use crate::linter::Diagnostic;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::prelude::*;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AppMode {
#[default]
Normal,
Purify,
Lint,
Debug,
Explain,
Fuzz,
}
impl AppMode {
pub fn name(&self) -> &'static str {
match self {
Self::Normal => "Normal",
Self::Purify => "Purify",
Self::Lint => "Lint",
Self::Debug => "Debug",
Self::Explain => "Explain",
Self::Fuzz => "Fuzz",
}
}
pub fn key(&self) -> char {
match self {
Self::Normal => '1',
Self::Purify => '2',
Self::Lint => '3',
Self::Debug => '4',
Self::Explain => '5',
Self::Fuzz => '6',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AppState {
#[default]
Idle,
Editing,
Linting,
Purifying,
ShowingResults,
ShowingHelp,
ConfirmingQuit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FocusedPanel {
#[default]
Editor,
LintResults,
Purified,
Quality,
}
pub struct App {
pub mode: AppMode,
pub state: AppState,
pub focused: FocusedPanel,
pub editor_content: String,
pub cursor_pos: usize,
pub diagnostics: Vec<Diagnostic>,
pub purified_output: String,
pub quality_score: f64,
pub coverage: f64,
pub edge_cases: Vec<String>,
pub should_quit: bool,
pub status: String,
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
impl App {
pub fn new() -> Self {
Self {
mode: AppMode::Normal,
state: AppState::Idle,
focused: FocusedPanel::Editor,
editor_content: String::new(),
cursor_pos: 0,
diagnostics: Vec::new(),
purified_output: String::new(),
quality_score: 0.0,
coverage: 0.0,
edge_cases: Vec::new(),
should_quit: false,
status: "Ready".to_string(),
}
}
pub fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> anyhow::Result<()> {
while !self.should_quit {
terminal.draw(|frame| super::ui::render(frame, self))?;
self.handle_events()?;
}
Ok(())
}
fn handle_events(&mut self) -> anyhow::Result<()> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
self.handle_key(key.code);
}
}
}
Ok(())
}
fn handle_key(&mut self, key: KeyCode) {
match self.state {
AppState::ConfirmingQuit => self.handle_quit_confirm(key),
AppState::ShowingHelp => self.handle_help(key),
_ => self.handle_normal(key),
}
}
fn handle_normal(&mut self, key: KeyCode) {
match key {
KeyCode::Char('q') => {
self.state = AppState::ConfirmingQuit;
self.status = "Quit? (y/n)".to_string();
}
KeyCode::F(1) => {
self.state = AppState::ShowingHelp;
self.status = "Help - press any key to close".to_string();
}
KeyCode::Char('1') => self.set_mode(AppMode::Normal),
KeyCode::Char('2') => self.set_mode(AppMode::Purify),
KeyCode::Char('3') => self.set_mode(AppMode::Lint),
KeyCode::Char('4') => self.set_mode(AppMode::Debug),
KeyCode::Char('5') => self.set_mode(AppMode::Explain),
KeyCode::Char('6') => self.set_mode(AppMode::Fuzz),
KeyCode::Tab => self.cycle_focus(),
KeyCode::F(2) => self.run_lint(),
KeyCode::F(3) => self.run_purify(),
KeyCode::F(4) => self.show_quality(),
KeyCode::Char(c) if self.focused == FocusedPanel::Editor => {
self.editor_content.push(c);
self.cursor_pos += 1;
self.state = AppState::Editing;
}
KeyCode::Backspace if self.focused == FocusedPanel::Editor => {
if !self.editor_content.is_empty() {
self.editor_content.pop();
self.cursor_pos = self.cursor_pos.saturating_sub(1);
}
}
KeyCode::Enter if self.focused == FocusedPanel::Editor => {
self.editor_content.push('\n');
self.cursor_pos += 1;
}
KeyCode::Esc => {
self.state = AppState::Idle;
self.status = "Ready".to_string();
}
_ => {}
}
}
fn handle_quit_confirm(&mut self, key: KeyCode) {
match key {
KeyCode::Char('y') | KeyCode::Char('Y') => {
self.should_quit = true;
}
_ => {
self.state = AppState::Idle;
self.status = "Ready".to_string();
}
}
}
fn handle_help(&mut self, _key: KeyCode) {
self.state = AppState::Idle;
self.status = "Ready".to_string();
}
fn set_mode(&mut self, mode: AppMode) {
self.mode = mode;
self.status = format!("Mode: {}", mode.name());
}
fn cycle_focus(&mut self) {
self.focused = match self.focused {
FocusedPanel::Editor => FocusedPanel::LintResults,
FocusedPanel::LintResults => FocusedPanel::Purified,
FocusedPanel::Purified => FocusedPanel::Quality,
FocusedPanel::Quality => FocusedPanel::Editor,
};
self.status = format!("Focus: {:?}", self.focused);
}
fn run_lint(&mut self) {
self.state = AppState::Linting;
self.status = "Linting...".to_string();
use crate::linter::lint_shell;
let result = lint_shell(&self.editor_content);
self.diagnostics = result.diagnostics;
self.state = AppState::ShowingResults;
self.status = format!("{} issues found", self.diagnostics.len());
}
fn run_purify(&mut self) {
self.state = AppState::Purifying;
self.status = "Purifying...".to_string();
use crate::repl::purifier::purify_bash;
match purify_bash(&self.editor_content) {
Ok(result) => {
self.purified_output = result;
self.status = "Purified successfully".to_string();
}
Err(e) => {
self.purified_output = format!("Error: {}", e);
self.status = "Purification failed".to_string();
}
}
self.state = AppState::ShowingResults;
}
fn show_quality(&mut self) {
self.status = format!(
"Coverage: {:.1}% | Score: {:.1}",
self.coverage, self.quality_score
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_new() {
let app = App::new();
assert_eq!(app.mode, AppMode::Normal);
assert_eq!(app.state, AppState::Idle);
assert!(!app.should_quit);
}
#[test]
fn test_mode_switching() {
let mut app = App::new();
app.set_mode(AppMode::Lint);
assert_eq!(app.mode, AppMode::Lint);
}
#[test]
fn test_focus_cycling() {
let mut app = App::new();
assert_eq!(app.focused, FocusedPanel::Editor);
app.cycle_focus();
assert_eq!(app.focused, FocusedPanel::LintResults);
app.cycle_focus();
assert_eq!(app.focused, FocusedPanel::Purified);
app.cycle_focus();
assert_eq!(app.focused, FocusedPanel::Quality);
app.cycle_focus();
assert_eq!(app.focused, FocusedPanel::Editor);
}
#[test]
fn test_quit_confirm() {
let mut app = App::new();
app.handle_key(KeyCode::Char('q'));
assert_eq!(app.state, AppState::ConfirmingQuit);
app.handle_key(KeyCode::Char('n'));
assert_eq!(app.state, AppState::Idle);
assert!(!app.should_quit);
}
#[test]
fn test_app_mode_names() {
assert_eq!(AppMode::Normal.name(), "Normal");
assert_eq!(AppMode::Purify.name(), "Purify");
assert_eq!(AppMode::Lint.name(), "Lint");
}
}