vkit 0.1.4

Fast Rust dev CLI: manage git worktrees, Node ports, run scripts, install & sync VS Code / Cursor extensions.
//! 主题预览:一屏展示所有主题色与公共组件样式,方便整体校色。
//!
//! 运行 `vkit preview`(或从 dashboard 进入),按任意键退出。

use std::time::Duration;

use anyhow::Result;
use ratatui::crossterm::event::{self, Event, KeyEventKind};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph};
use ratatui::{DefaultTerminal, Frame};

use crate::theme;
use crate::widgets::{
    checkbox, input_paragraph, item_desc_style, item_title_style, key_hint_line, list_cursor_style,
    status_icon, LIST_CURSOR,
};

const SPINNER: [&str; 10] = ["", "", "", "", "", "", "", "", "", ""];

/// 独立入口:初始化终端并进入预览循环(仅 `vkit preview` 命令使用)。
pub fn run() -> Result<()> {
    let mut terminal = ratatui::init();
    let result = run_loop(&mut terminal);
    ratatui::restore();
    result
}

fn run_loop(terminal: &mut DefaultTerminal) -> Result<()> {
    let mut tick = 0usize;
    loop {
        terminal.draw(|frame| render(frame, tick))?;
        if event::poll(Duration::from_millis(120))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    break;
                }
            }
        }
        tick = tick.wrapping_add(1);
    }
    Ok(())
}

fn panel(title: &str) -> Block<'static> {
    Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(theme::muted()))
        .title(Span::styled(
            title.to_string(),
            Style::default()
                .fg(theme::accent())
                .add_modifier(Modifier::BOLD),
        ))
}

fn render(frame: &mut Frame, tick: usize) {
    let chunks = Layout::vertical([
        Constraint::Length(1),  // 标题
        Constraint::Length(15), // 调色板 + 组件(左右两栏)
        Constraint::Length(3),  // 输入框(聚焦)
        Constraint::Length(3),  // 输入框(未聚焦)
        Constraint::Length(6),  // 列表示例
        Constraint::Length(1),  // 按键提示
        Constraint::Min(0),
    ])
    .split(frame.area());

    frame.render_widget(
        Paragraph::new(Line::from(Span::styled(
            "vkit 主题预览(Everforest)",
            Style::default()
                .fg(theme::accent())
                .add_modifier(Modifier::BOLD),
        ))),
        chunks[0],
    );

    let cols = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(chunks[1]);
    render_palette(frame, cols[0]);
    render_components(frame, cols[1], tick);

    let focused = input_paragraph("dbaeumer.vscode-eslint", "占位符", true, " 输入框(聚焦)");
    frame.render_widget(focused, chunks[2]);
    let blurred = input_paragraph("", "占位符(未聚焦)", false, " 输入框(未聚焦)");
    frame.render_widget(blurred, chunks[3]);

    render_list(frame, chunks[4]);

    frame.render_widget(
        Paragraph::new(key_hint_line(&[
            ("↑/↓", "移动"),
            ("Space", "选择"),
            ("Enter", "确认"),
            ("q", "退出"),
        ])),
        chunks[5],
    );
}

/// 左栏:调色板色卡(前景色用色块,背景色用底纹样本)。
fn render_palette(frame: &mut Frame, area: Rect) {
    // 前景类颜色:色块 + 角色说明。
    let fg_swatches: &[(&str, Color, &str)] = &[
        ("accent", theme::accent(), "主强调 / 标题 / 成功 ✓"),
        ("highlight", theme::highlight(), "光标行"),
        ("selected", theme::selected(), "勾选 / 选中"),
        ("success", theme::success(), "成功 / 待 push ↑"),
        ("danger", theme::danger(), "危险 / 待 pull ↓"),
        ("warning", theme::warning(), "警示 / 脏工作区"),
        ("muted", theme::muted(), "边框 / 次要 / 未选"),
        ("text", theme::text(), "正文"),
    ];

    let mut lines: Vec<Line> = fg_swatches
        .iter()
        .map(|(name, color, role)| {
            Line::from(vec![
                Span::styled("███████ ", Style::default().fg(*color)),
                Span::styled(format!("{name:<10}"), Style::default().fg(theme::text())),
                Span::styled((*role).to_string(), Style::default().fg(theme::muted())),
            ])
        })
        .collect();

    // 背景类颜色:用带底纹的样本展示实际用途。
    lines.push(Line::from(vec![
        Span::styled(
            "  keycap  ",
            Style::default().fg(theme::text()).bg(theme::surface()),
        ),
        Span::styled(
            " surface   keycap 底块背景",
            Style::default().fg(theme::muted()),
        ),
    ]));

    frame.render_widget(Paragraph::new(lines).block(panel(" 调色板 ")), area);
}

/// 右栏:各类组件片段(勾选、状态图标、光标行、标题层级、按键帽)。
fn render_components(frame: &mut Frame, area: Rect, tick: usize) {
    let spinner = SPINNER[tick % SPINNER.len()];
    let lines = vec![
        Line::from(vec![
            checkbox(true),
            Span::styled("已勾选", item_title_style(true)),
        ]),
        Line::from(vec![
            checkbox(false),
            Span::styled("未勾选", item_title_style(false)),
        ]),
        Line::from(vec![
            status_icon(true),
            Span::styled("成功状态", Style::default().fg(theme::success())),
        ]),
        Line::from(vec![
            status_icon(false),
            Span::styled("失败状态", Style::default().fg(theme::warning())),
        ]),
        Line::from(vec![
            Span::styled("", Style::default().fg(theme::warning())),
            Span::styled("警示信息", Style::default().fg(theme::warning())),
        ]),
        Line::from(vec![
            Span::styled(format!("{spinner} "), Style::default().fg(theme::warning())),
            Span::styled("loading…", Style::default().fg(theme::warning())),
        ]),
        Line::from(vec![Span::styled(
            format!("{LIST_CURSOR}光标行(highlight)"),
            list_cursor_style(),
        )]),
        Line::from(Span::styled("列表项描述 / 次要信息", item_desc_style())),
        Line::from(Span::styled(
            "正文文字 text",
            Style::default().fg(theme::text()),
        )),
    ];
    frame.render_widget(Paragraph::new(lines).block(panel(" 组件 ")), area);
}

/// 真实 List 组件:展示光标高亮、勾选态、未选态一起的观感。
fn render_list(frame: &mut Frame, area: Rect) {
    let items = vec![
        ListItem::new(Line::from(vec![
            checkbox(true),
            Span::styled("eslint(已选)", item_title_style(true)),
        ])),
        ListItem::new(Line::from(vec![
            checkbox(false),
            Span::styled("prettier(未选)", item_title_style(false)),
        ])),
        ListItem::new(Line::from(vec![
            checkbox(false),
            Span::styled("gitlens(未选)", item_title_style(false)),
        ])),
    ];
    let list = List::new(items)
        .block(panel(" 列表示例(光标在第 1 行) "))
        .highlight_symbol(LIST_CURSOR)
        .highlight_style(list_cursor_style());

    let mut state = ListState::default();
    state.select(Some(0));
    frame.render_stateful_widget(list, area, &mut state);
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::backend::TestBackend;
    use ratatui::Terminal;

    #[test]
    fn renders_palette_and_components() {
        let mut terminal = Terminal::new(TestBackend::new(90, 30)).unwrap();
        terminal.draw(|frame| render(frame, 0)).unwrap();
        let buffer = terminal.backend().buffer().clone();
        let mut out = String::new();
        for y in 0..buffer.area.height {
            for x in 0..buffer.area.width {
                out.push_str(buffer[(x, y)].symbol());
            }
        }
        let flat: String = out.split_whitespace().collect();
        assert!(flat.contains("accent"));
        assert!(flat.contains("selected"));
        assert!(flat.contains("highlight"));
        assert!(flat.contains("eslint"));
    }
}