use crate::assets::{self, HelpTab};
use crate::command::chat::markdown::markdown_to_lines;
use crate::command::chat::storage::{load_agent_config, save_agent_config};
use crate::theme::{Theme, ThemeName};
use ratatui::text::Line;
struct TabCache {
lines: Vec<Line<'static>>,
cached_width: usize,
}
pub const CMD_POPUP_ITEMS: &[(&str, &str)] = &[
("theme", "切换主题"),
("help", "查看帮助首页"),
("quit", "退出"),
];
#[derive(PartialEq, Clone)]
pub enum AppMode {
Normal,
CommandPopup,
ThemeSelect,
}
pub struct HelpApp {
pub active_tab: usize,
pub tab_count: usize,
tab_names: Vec<String>,
tab_raw_contents: Vec<String>,
tab_caches: Vec<Option<TabCache>>,
tab_scrolls: Vec<usize>,
pub total_lines: usize,
theme: Theme,
pub theme_name: ThemeName,
pub mode: AppMode,
pub cmd_popup_filter: String,
pub cmd_popup_selected: usize,
pub theme_popup_selected: usize,
pub message: Option<String>,
}
impl Default for HelpApp {
fn default() -> Self {
Self::new()
}
}
impl HelpApp {
pub fn new() -> Self {
let tabs: Vec<HelpTab> = assets::load_help_tabs();
let count = tabs.len();
let tab_names: Vec<String> = tabs.iter().map(|t| t.name.clone()).collect();
let tab_raw_contents: Vec<String> = tabs.into_iter().map(|t| t.content).collect();
let agent_config = load_agent_config();
let theme_name = agent_config.theme.clone();
let theme = Theme::from_name(&theme_name);
let theme_popup_selected = ThemeName::all()
.iter()
.position(|t| t == &theme_name)
.unwrap_or(0);
Self {
active_tab: 0,
tab_count: count,
tab_names,
tab_raw_contents,
tab_caches: (0..count).map(|_| None).collect(),
tab_scrolls: vec![0; count],
total_lines: 0,
theme,
theme_name,
mode: AppMode::Normal,
cmd_popup_filter: String::new(),
cmd_popup_selected: 0,
theme_popup_selected,
message: None,
}
}
pub fn tab_name(&self, idx: usize) -> &str {
self.tab_names.get(idx).map(|s| s.as_str()).unwrap_or("?")
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn current_tab_lines(&mut self, content_width: usize) -> &[Line<'static>] {
let idx = self.active_tab;
if idx >= self.tab_caches.len() || idx >= self.tab_raw_contents.len() {
self.total_lines = 1;
return &[];
}
let need_rebuild = match &self.tab_caches[idx] {
Some(cache) => cache.cached_width != content_width,
None => true,
};
if need_rebuild {
let md_text = &self.tab_raw_contents[idx];
let lines = if md_text.trim().is_empty() {
vec![Line::from(" (暂无内容)")]
} else {
markdown_to_lines(md_text, content_width, &self.theme)
};
self.tab_caches[idx] = Some(TabCache {
lines,
cached_width: content_width,
});
}
let cache = self.tab_caches[idx]
.as_ref()
.expect("缓存应该在 need_rebuild 检查后存在");
self.total_lines = cache.lines.len();
&cache.lines
}
pub fn scroll_offset(&self) -> usize {
self.tab_scrolls.get(self.active_tab).copied().unwrap_or(0)
}
pub fn next_tab(&mut self) {
self.active_tab = (self.active_tab + 1) % self.tab_count;
}
pub fn prev_tab(&mut self) {
self.active_tab = (self.active_tab + self.tab_count - 1) % self.tab_count;
}
pub fn goto_tab(&mut self, idx: usize) {
if idx < self.tab_count {
self.active_tab = idx;
}
}
pub fn scroll_down(&mut self, n: usize) {
if let Some(scroll) = self.tab_scrolls.get_mut(self.active_tab) {
*scroll = scroll.saturating_add(n);
}
}
pub fn scroll_up(&mut self, n: usize) {
if let Some(scroll) = self.tab_scrolls.get_mut(self.active_tab) {
*scroll = scroll.saturating_sub(n);
}
}
pub fn scroll_to_top(&mut self) {
if let Some(scroll) = self.tab_scrolls.get_mut(self.active_tab) {
*scroll = 0;
}
}
pub fn scroll_to_bottom(&mut self) {
if let Some(scroll) = self.tab_scrolls.get_mut(self.active_tab) {
*scroll = usize::MAX;
}
}
pub fn invalidate_cache(&mut self) {
for cache in &mut self.tab_caches {
*cache = None;
}
}
pub fn clamp_scroll(&mut self, visible_height: usize) {
if let Some(scroll) = self.tab_scrolls.get_mut(self.active_tab) {
let max_scroll = self.total_lines.saturating_sub(visible_height);
if *scroll > max_scroll {
*scroll = max_scroll;
}
}
}
pub fn filtered_cmd_items(&self) -> Vec<(usize, &'static str, &'static str)> {
let filter = self.cmd_popup_filter.to_lowercase();
CMD_POPUP_ITEMS
.iter()
.enumerate()
.filter(|(_, (key, label))| {
filter.is_empty()
|| key.contains(filter.as_str())
|| label.contains(filter.as_str())
})
.map(|(i, (key, label))| (i, *key, *label))
.collect()
}
pub fn open_command_popup(&mut self) {
self.mode = AppMode::CommandPopup;
self.cmd_popup_filter.clear();
self.cmd_popup_selected = 0;
}
pub fn open_theme_select(&mut self) {
self.mode = AppMode::ThemeSelect;
self.theme_popup_selected = ThemeName::all()
.iter()
.position(|t| t == &self.theme_name)
.unwrap_or(0);
}
pub fn apply_selected_theme(&mut self) {
let all = ThemeName::all();
if let Some(name) = all.get(self.theme_popup_selected) {
self.theme_name = name.clone();
self.theme = Theme::from_name(name);
self.invalidate_cache();
let mut config = load_agent_config();
config.theme = name.clone();
save_agent_config(&config);
self.message = Some(format!("主题: {}", name.display_name()));
}
}
}