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] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
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);
}
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"));
}
}