Skip to main content

j_cli/command/help/
app.rs

1use crate::assets::HELP_TEXT;
2use crate::command::chat::markdown::markdown_to_lines;
3use crate::command::chat::theme::{Theme, ThemeName};
4use ratatui::text::Line;
5
6/// Tab 定义:名称 + 匹配的 ## 标题关键词列表
7struct TabDef {
8    name: &'static str,
9    heading_keywords: &'static [&'static str],
10}
11
12const TAB_DEFS: &[TabDef] = &[
13    TabDef {
14        name: "快速上手",
15        heading_keywords: &["快速上手"],
16    },
17    TabDef {
18        name: "数据目录",
19        heading_keywords: &["数据目录"],
20    },
21    TabDef {
22        name: "别名 & 打开",
23        heading_keywords: &["别名管理", "分类标记", "列表", "打开"],
24    },
25    TabDef {
26        name: "日报",
27        heading_keywords: &["日报系统"],
28    },
29    TabDef {
30        name: "待办",
31        heading_keywords: &["待办备忘录"],
32    },
33    TabDef {
34        name: "脚本 & 计时",
35        heading_keywords: &["脚本"],
36    },
37    TabDef {
38        name: "系统 & 语音",
39        heading_keywords: &["系统设置", "语音转文字"],
40    },
41    TabDef {
42        name: "AI 对话",
43        heading_keywords: &["AI 对话"],
44    },
45    TabDef {
46        name: "安装 & 卸载",
47        heading_keywords: &["安装", "卸载"],
48    },
49    TabDef {
50        name: "使用技巧",
51        heading_keywords: &["使用技巧"],
52    },
53];
54
55/// 按 `## ` 标题行将 HELP_TEXT 分割到各 Tab
56fn split_help_into_tabs() -> Vec<String> {
57    // 先按 ## 标题切分所有 section
58    let mut sections: Vec<(String, String)> = Vec::new(); // (标题行文本, 内容)
59    let mut current_heading = String::new();
60    let mut current_content = String::new();
61
62    for line in HELP_TEXT.lines() {
63        if line.starts_with("## ") {
64            // 保存上一个 section
65            if !current_heading.is_empty() {
66                sections.push((current_heading.clone(), current_content.clone()));
67            }
68            current_heading = line.to_string();
69            current_content = String::new();
70            current_content.push_str(line);
71            current_content.push('\n');
72        } else {
73            current_content.push_str(line);
74            current_content.push('\n');
75        }
76    }
77    if !current_heading.is_empty() {
78        sections.push((current_heading, current_content));
79    }
80
81    // 将 sections 分配到各 tab
82    let mut tab_contents: Vec<String> = vec![String::new(); TAB_DEFS.len()];
83
84    for (heading, content) in &sections {
85        let mut matched = false;
86        for (tab_idx, tab_def) in TAB_DEFS.iter().enumerate() {
87            for kw in tab_def.heading_keywords {
88                if heading.contains(kw) {
89                    if !tab_contents[tab_idx].is_empty() {
90                        tab_contents[tab_idx].push_str("\n---\n\n");
91                    }
92                    tab_contents[tab_idx].push_str(content);
93                    matched = true;
94                    break;
95                }
96            }
97            if matched {
98                break;
99            }
100        }
101    }
102
103    tab_contents
104}
105
106/// 每个 Tab 的缓存数据
107struct TabCache {
108    lines: Vec<Line<'static>>,
109    cached_width: usize,
110}
111
112/// HelpApp 状态
113pub struct HelpApp {
114    pub active_tab: usize,
115    pub tab_count: usize,
116    tab_names: Vec<&'static str>,
117    tab_raw_contents: Vec<String>,
118    tab_caches: Vec<Option<TabCache>>,
119    tab_scrolls: Vec<usize>,
120    /// 当前 Tab 的总渲染行数(用于滚动限制)
121    pub total_lines: usize,
122    theme: Theme,
123}
124
125impl HelpApp {
126    pub fn new() -> Self {
127        let tab_raw_contents = split_help_into_tabs();
128        let count = TAB_DEFS.len();
129        let tab_names: Vec<&'static str> = TAB_DEFS.iter().map(|t| t.name).collect();
130        Self {
131            active_tab: 0,
132            tab_count: count,
133            tab_names,
134            tab_raw_contents,
135            tab_caches: (0..count).map(|_| None).collect(),
136            tab_scrolls: vec![0; count],
137            total_lines: 0,
138            theme: Theme::from_name(&ThemeName::default()),
139        }
140    }
141
142    pub fn tab_name(&self, idx: usize) -> &str {
143        self.tab_names.get(idx).copied().unwrap_or("?")
144    }
145
146    pub fn theme(&self) -> &Theme {
147        &self.theme
148    }
149
150    /// 获取当前 Tab 的渲染行(带缓存)
151    pub fn current_tab_lines(&mut self, content_width: usize) -> &[Line<'static>] {
152        let idx = self.active_tab;
153
154        // 检查缓存是否有效
155        let need_rebuild = match &self.tab_caches[idx] {
156            Some(cache) => cache.cached_width != content_width,
157            None => true,
158        };
159
160        if need_rebuild {
161            let md_text = &self.tab_raw_contents[idx];
162            let lines = if md_text.trim().is_empty() {
163                vec![Line::from("  (暂无内容)")]
164            } else {
165                markdown_to_lines(md_text, content_width, &self.theme)
166            };
167            self.tab_caches[idx] = Some(TabCache {
168                lines,
169                cached_width: content_width,
170            });
171        }
172
173        let cache = self.tab_caches[idx].as_ref().unwrap();
174        self.total_lines = cache.lines.len();
175        &cache.lines
176    }
177
178    pub fn scroll_offset(&self) -> usize {
179        self.tab_scrolls[self.active_tab]
180    }
181
182    pub fn next_tab(&mut self) {
183        self.active_tab = (self.active_tab + 1) % self.tab_count;
184    }
185
186    pub fn prev_tab(&mut self) {
187        self.active_tab = (self.active_tab + self.tab_count - 1) % self.tab_count;
188    }
189
190    pub fn goto_tab(&mut self, idx: usize) {
191        if idx < self.tab_count {
192            self.active_tab = idx;
193        }
194    }
195
196    pub fn scroll_down(&mut self, n: usize) {
197        let idx = self.active_tab;
198        self.tab_scrolls[idx] = self.tab_scrolls[idx].saturating_add(n);
199    }
200
201    pub fn scroll_up(&mut self, n: usize) {
202        let idx = self.active_tab;
203        self.tab_scrolls[idx] = self.tab_scrolls[idx].saturating_sub(n);
204    }
205
206    pub fn scroll_to_top(&mut self) {
207        let idx = self.active_tab;
208        self.tab_scrolls[idx] = 0;
209    }
210
211    pub fn scroll_to_bottom(&mut self) {
212        let idx = self.active_tab;
213        // total_lines 会在 draw 时更新,这里设一个很大的值,在 draw 时会被钳制
214        self.tab_scrolls[idx] = usize::MAX;
215    }
216
217    pub fn invalidate_cache(&mut self) {
218        for cache in &mut self.tab_caches {
219            *cache = None;
220        }
221    }
222
223    /// 钳制滚动偏移(在 draw 后调用,确保不超出内容范围)
224    pub fn clamp_scroll(&mut self, visible_height: usize) {
225        let idx = self.active_tab;
226        let max_scroll = self.total_lines.saturating_sub(visible_height);
227        if self.tab_scrolls[idx] > max_scroll {
228            self.tab_scrolls[idx] = max_scroll;
229        }
230    }
231}