j_cli/command/help/
app.rs1use crate::assets::HELP_TEXT;
2use crate::command::chat::markdown::markdown_to_lines;
3use crate::command::chat::theme::{Theme, ThemeName};
4use ratatui::text::Line;
5
6struct 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
55fn split_help_into_tabs() -> Vec<String> {
57 let mut sections: Vec<(String, String)> = Vec::new(); 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 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 let mut tab_contents: Vec<String> = vec![String::new(); TAB_DEFS.len()];
83
84 for (heading, content) in §ions {
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
106struct TabCache {
108 lines: Vec<Line<'static>>,
109 cached_width: usize,
110}
111
112pub 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 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 pub fn current_tab_lines(&mut self, content_width: usize) -> &[Line<'static>] {
152 let idx = self.active_tab;
153
154 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 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 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}