pub mod app;
pub mod ui;
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::theme::ThemeName;
use app::{AppMode, HelpApp};
use ui::draw_help_ui;
struct TerminalGuard {
active: bool,
}
impl TerminalGuard {
fn activate() -> io::Result<Self> {
terminal::enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
Ok(Self { active: true })
}
fn deactivate(&mut self) {
if self.active {
let _ = terminal::disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
self.active = false;
}
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
self.deactivate();
}
}
pub fn handle_help() {
match run_help_tui() {
Ok(_) => {}
Err(e) => {
eprintln!("TUI 启动失败: {}", e);
}
}
}
fn run_help_tui() -> io::Result<()> {
let mut guard = TerminalGuard::activate()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let mut app = HelpApp::new();
let result = {
let mut app_ref = AssertUnwindSafe(&mut app);
let mut terminal_ref = AssertUnwindSafe(&mut terminal);
catch_unwind(move || {
let app = &mut *app_ref;
let terminal = &mut *terminal_ref;
loop {
terminal.draw(|f| draw_help_ui(f, app))?;
if event::poll(std::time::Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) => {
if key.modifiers.contains(KeyModifiers::CONTROL)
&& key.code == KeyCode::Char('c')
{
break;
}
match app.mode {
AppMode::Normal => {
if handle_normal_key(app, key) {
break;
}
}
AppMode::CommandPopup => handle_command_popup_key(app, key),
AppMode::ThemeSelect => handle_theme_select_key(app, key),
}
}
Event::Resize(_, _) => {
app.invalidate_cache();
}
_ => {}
}
}
}
Ok::<(), io::Error>(())
})
};
guard.deactivate();
match result {
Ok(inner_result) => inner_result,
Err(panic_info) => {
if let Some(s) = panic_info.downcast_ref::<&str>() {
eprintln!("Help TUI panic: {}", s);
} else if let Some(s) = panic_info.downcast_ref::<String>() {
eprintln!("Help TUI panic: {}", s);
} else {
eprintln!("Help TUI panic: unknown error");
}
Err(io::Error::other("panic occurred"))
}
}
}
fn handle_normal_key(app: &mut HelpApp, key: crossterm::event::KeyEvent) -> bool {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return true,
KeyCode::Right | KeyCode::Char('l') => app.next_tab(),
KeyCode::Left | KeyCode::Char('h') => app.prev_tab(),
KeyCode::Tab => {
if key.modifiers.contains(KeyModifiers::SHIFT) {
app.prev_tab();
} else {
app.next_tab();
}
}
KeyCode::BackTab => app.prev_tab(),
KeyCode::Char(c @ '1'..='9') => {
let idx = (c as usize) - ('1' as usize);
app.goto_tab(idx);
}
KeyCode::Char('0') => app.goto_tab(9),
KeyCode::Down | KeyCode::Char('j') => app.scroll_down(1),
KeyCode::Up | KeyCode::Char('k') => app.scroll_up(1),
KeyCode::PageDown => app.scroll_down(10),
KeyCode::PageUp => app.scroll_up(10),
KeyCode::Home => app.scroll_to_top(),
KeyCode::End => app.scroll_to_bottom(),
KeyCode::Char('/') => app.open_command_popup(),
_ => {}
}
false
}
fn handle_command_popup_key(app: &mut HelpApp, key: crossterm::event::KeyEvent) {
let items = app.filtered_cmd_items();
match key.code {
KeyCode::Esc => {
app.mode = AppMode::Normal;
}
KeyCode::Up | KeyCode::Char('k') => {
if !items.is_empty() {
if app.cmd_popup_selected > 0 {
app.cmd_popup_selected -= 1;
} else {
app.cmd_popup_selected = items.len() - 1;
}
}
}
KeyCode::Down | KeyCode::Char('j') => {
if !items.is_empty() {
if app.cmd_popup_selected < items.len() - 1 {
app.cmd_popup_selected += 1;
} else {
app.cmd_popup_selected = 0;
}
}
}
KeyCode::Backspace => {
if app.cmd_popup_filter.pop().is_none() {
app.mode = AppMode::Normal;
} else {
app.cmd_popup_selected = 0;
}
}
KeyCode::Enter => {
let selected = app.cmd_popup_selected.min(items.len().saturating_sub(1));
if let Some((_, key, _)) = items.get(selected) {
match *key {
"theme" => {
app.open_theme_select();
return;
}
"help" => {
app.goto_tab(0);
app.mode = AppMode::Normal;
return;
}
"quit" => {
app.mode = AppMode::Normal;
app.message = Some("按 q 退出".to_string());
return;
}
_ => {}
}
}
app.mode = AppMode::Normal;
}
KeyCode::Char(c) => {
app.cmd_popup_filter.push(c);
app.cmd_popup_selected = 0;
}
_ => {}
}
}
fn handle_theme_select_key(app: &mut HelpApp, key: crossterm::event::KeyEvent) {
let count = ThemeName::all().len();
match key.code {
KeyCode::Esc => {
app.mode = AppMode::Normal;
}
KeyCode::Up | KeyCode::Char('k') => {
if count > 0 {
if app.theme_popup_selected > 0 {
app.theme_popup_selected -= 1;
} else {
app.theme_popup_selected = count - 1;
}
}
}
KeyCode::Down | KeyCode::Char('j') => {
if count > 0 {
if app.theme_popup_selected < count - 1 {
app.theme_popup_selected += 1;
} else {
app.theme_popup_selected = 0;
}
}
}
KeyCode::Enter => {
app.apply_selected_theme();
app.mode = AppMode::Normal;
}
_ => {}
}
}