elementary-row-operation-verifier 0.0.1

A tool to verify the correctness of elementary row operations on matrices
Documentation
//! Check 模式 TUI:文件夹树 + 文件内容

use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
    Frame, Terminal,
    backend::{Backend, CrosstermBackend},
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Style},
    widgets::{Block, Borders, List, ListItem, Paragraph},
};
use std::io;
use std::path::PathBuf;

use crate::parser::{FileParseResult, parse_file};
use crate::tui::content::build_content;
use crate::verifier::{VerificationResult, Verifier};

pub struct CheckTui {
    pub dir: PathBuf,
    pub files: Vec<PathBuf>,
    pub selected: usize,
    pub focus_tree: bool,
    current_file: Option<FileState>,
    pub scroll_v: usize,
    pub show_detail: bool,
}

struct FileState {
    #[allow(dead_code)]
    path: PathBuf,
    parsed: FileParseResult,
    results: Vec<VerificationResult>,
}

impl CheckTui {
    pub fn new(dir: PathBuf) -> io::Result<Self> {
        let files = collect_lore_files(&dir);
        Ok(CheckTui {
            dir,
            files,
            selected: 0,
            focus_tree: true,
            current_file: None,
            scroll_v: 0,
            show_detail: false,
        })
    }

    pub fn run(&mut self) -> io::Result<()> {
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
        let backend = CrosstermBackend::new(stdout);
        let mut terminal = Terminal::new(backend)?;
        let res = self.run_app(&mut terminal);
        disable_raw_mode()?;
        execute!(
            terminal.backend_mut(),
            LeaveAlternateScreen,
            DisableMouseCapture
        )?;
        terminal.show_cursor()?;
        res
    }

    fn run_app<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
        loop {
            terminal.draw(|f| self.draw(f))?;
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char('q') => return Ok(()),
                    KeyCode::Char('b') => self.focus_tree = !self.focus_tree,
                    KeyCode::Backspace => {
                        if let Some(parent) = self.dir.parent() {
                            self.dir = parent.to_path_buf();
                            self.files = collect_lore_files(&self.dir);
                            self.selected = 0;
                            self.current_file = None;
                        }
                    }
                    KeyCode::Down | KeyCode::Char('j') => {
                        if self.focus_tree {
                            let max = if self.dir.parent().is_some() {
                                self.files.len()
                            } else {
                                self.files.len().saturating_sub(1)
                            };
                            self.selected = (self.selected + 1).min(max);
                        } else {
                            self.scroll_v = self.scroll_v.saturating_add(1);
                        }
                    }
                    KeyCode::Up | KeyCode::Char('k') => {
                        if self.focus_tree {
                            self.selected = self.selected.saturating_sub(1);
                        } else {
                            self.scroll_v = self.scroll_v.saturating_sub(1);
                        }
                    }
                    KeyCode::Enter => {
                        if self.focus_tree {
                            self.open_selected();
                        } else {
                            self.show_detail = !self.show_detail;
                        }
                    }
                    KeyCode::Esc => {
                        if !self.focus_tree {
                            self.show_detail = false;
                        }
                    }
                    _ => {}
                }
            }
        }
    }

    fn open_selected(&mut self) {
        // 第一项是 + ..
        if self.selected == 0 && self.dir.parent().is_some() {
            self.dir = self.dir.parent().unwrap().to_path_buf();
            self.files = collect_lore_files(&self.dir);
            self.selected = 0;
            self.current_file = None;
            return;
        }
        let offset = if self.dir.parent().is_some() { 1 } else { 0 };
        let idx = self.selected.saturating_sub(offset);
        if idx >= self.files.len() {
            return;
        }
        let path = self.files[idx].clone();
        if path.is_dir() {
            self.dir = path;
            self.files = collect_lore_files(&self.dir);
            self.selected = 0;
            self.current_file = None;
            return;
        }
        if let Ok(parsed) = parse_file(&path) {
            let results = Verifier::new().verify_file(&parsed);
            self.current_file = Some(FileState {
                path,
                parsed,
                results,
            });
            self.scroll_v = 0;
            self.focus_tree = false;
        }
    }

    pub fn draw(&mut self, f: &mut Frame) {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .margin(1)
            .constraints([Constraint::Min(4), Constraint::Length(1)])
            .split(f.size());

        let main = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
            .split(chunks[0]);

        self.draw_content(f, main[0]);
        self.draw_tree(f, main[1]);

        let footer = format!(" {}  b:switch  Enter:open  q:quit ", self.dir.display());
        f.render_widget(
            Paragraph::new(footer).style(Style::default().fg(Color::Gray)),
            chunks[1],
        );
    }

    fn draw_tree(&mut self, f: &mut Frame, area: Rect) {
        let items: Vec<ListItem> = self
            .files
            .iter()
            .enumerate()
            .map(|(i, p)| {
                let name = p.file_name().unwrap_or_default().to_string_lossy();
                let prefix = if p.is_dir() { "+ " } else { "  " };
                let style = if i == self.selected && self.focus_tree {
                    Style::default().bg(Color::DarkGray).fg(Color::Yellow)
                } else if i == self.selected {
                    Style::default().bg(Color::DarkGray)
                } else if p.is_dir() {
                    Style::default().fg(Color::Cyan)
                } else {
                    Style::default()
                };
                ListItem::new(format!(" {}{}", prefix, name)).style(style)
            })
            .collect();
        f.render_widget(
            List::new(items).block(Block::default().borders(Borders::ALL).title(" Files ")),
            area,
        );
    }

    fn draw_content(&self, f: &mut Frame, area: Rect) {
        if let Some(ref state) = self.current_file {
            let widths = state
                .parsed
                .steps
                .first()
                .map_or(&[] as &[usize], |s| s.col_widths.as_slice());
            let lines = build_content(
                &state.parsed.ori,
                &state.parsed.steps,
                &state.results,
                self.show_detail,
                widths,
            );
            let max_v = lines.len().saturating_sub(area.height as usize);
            let scroll = self.scroll_v.min(max_v);
            f.render_widget(Paragraph::new(lines).scroll((scroll as u16, 0)), area);
        } else {
            f.render_widget(
                Paragraph::new("Select a .lore file and press Enter")
                    .style(Style::default().fg(Color::Gray))
                    .alignment(Alignment::Center),
                area,
            );
        }
    }
}

fn collect_lore_files(dir: &PathBuf) -> Vec<PathBuf> {
    let mut files = vec![];
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() || path.extension().map_or(false, |e| e == "lore") {
                files.push(path);
            }
        }
    }
    files.sort_by(|a, b| {
        let ad = a.is_dir();
        let bd = b.is_dir();
        if ad != bd {
            return bd.cmp(&ad);
        }
        a.file_name().cmp(&b.file_name())
    });
    files
}