summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-03-12 19:04:12 +0800
committer魏曹先生 <1992414357@qq.com>2026-03-12 19:04:12 +0800
commit72c57380883a1c1cc796dea6d35048ab5bed5f53 (patch)
tree936e04d2ec0f5bae54667beac6bf069208900a80
parent9d812580557cdc343378816cd65678b8aa75d944 (diff)
Add helpdoc system with interactive viewer
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml7
-rw-r--r--macros/helpdoc_system_macros/Cargo.toml12
-rw-r--r--macros/helpdoc_system_macros/src/lib.rs192
-rw-r--r--resources/helpdoc/Welcome_To_JVCS.en.md1
-rw-r--r--resources/helpdoc/Welcome_To_JVCS.zh-CN.md1
-rw-r--r--resources/helpdoc/commands/hexdump.en.md6
-rw-r--r--resources/helpdoc/commands/hexdump.zh-CN.md6
-rw-r--r--resources/helpdoc/commands/sheetdump.en.md13
-rw-r--r--resources/helpdoc/commands/sheetdump.zh-CN.md12
-rw-r--r--resources/helpdoc/commands/sheetedit.en.md12
-rw-r--r--resources/helpdoc/commands/sheetedit.zh-CN.md12
-rw-r--r--resources/locales/jvn/en.yml27
-rw-r--r--resources/locales/jvn/helpdoc_viewer/en.yml5
-rw-r--r--resources/locales/jvn/helpdoc_viewer/zh-CN.yml5
-rw-r--r--resources/locales/jvn/zh-CN.yml27
-rw-r--r--src/bin/jvn.rs3
-rw-r--r--src/cmds/arg/helpdoc.rs6
-rw-r--r--src/cmds/cmd/helpdoc.rs49
-rw-r--r--src/cmds/cmd/hexdump.rs14
-rw-r--r--src/cmds/cmd/sheetdump.rs14
-rw-r--r--src/cmds/cmd/sheetedit.rs14
-rw-r--r--src/cmds/in/helpdoc.rs4
-rw-r--r--src/systems.rs1
-rw-r--r--src/systems/cmd/cmd_system.rs9
-rw-r--r--src/systems/cmd/macros.rs8
-rw-r--r--src/systems/helpdoc.rs18
-rw-r--r--src/systems/helpdoc/helpdoc_viewer.rs636
-rw-r--r--utils/src/display.rs2
-rw-r--r--utils/src/display/markdown.rs (renamed from utils/src/display/colorful.rs)0
30 files changed, 1071 insertions, 56 deletions
diff --git a/Cargo.lock b/Cargo.lock
index afc5e63..47482bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1027,6 +1027,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
+name = "helpdoc_system_macros"
+version = "0.1.0-dev"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "hex_display"
version = "0.1.0"
@@ -1207,6 +1216,7 @@ dependencies = [
"colored",
"crossterm",
"env_logger",
+ "helpdoc_system_macros",
"just_enough_vcs",
"just_fmt",
"just_progress",
@@ -1219,6 +1229,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
+ "strip-ansi-escapes",
"thiserror",
"tokio",
"toml 0.9.8",
diff --git a/Cargo.toml b/Cargo.toml
index e6c061f..4eba850 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,8 +9,9 @@ homepage = "https://github.com/JustEnoughVCS/CommandLine/"
members = [
"utils/",
"tools/build_helper",
- "macros/render_system_macros",
"macros/cmd_system_macros",
+ "macros/helpdoc_system_macros",
+ "macros/render_system_macros",
]
[workspace.package]
@@ -58,8 +59,9 @@ serde = { version = "1", features = ["derive"] }
[dependencies]
cli_utils = { path = "utils" }
-cmd_system_macros = { path = "macros/cmd_system_macros" }
just_enough_vcs = { path = "../VersionControl", features = ["all"] }
+cmd_system_macros = { path = "macros/cmd_system_macros" }
+helpdoc_system_macros = { path = "macros/helpdoc_system_macros" }
render_system_macros = { path = "macros/render_system_macros" }
crossterm.workspace = true
@@ -77,6 +79,7 @@ ron = "0.11.0"
rust-i18n = "3"
serde_json = "1"
serde_yaml = "0.9"
+strip_ansi_escapes = { version = "0.2", package = "strip-ansi-escapes" }
tokio = { version = "1", features = ["full"] }
toml = "0.9"
walkdir = "2.5.0"
diff --git a/macros/helpdoc_system_macros/Cargo.toml b/macros/helpdoc_system_macros/Cargo.toml
new file mode 100644
index 0000000..c5c8483
--- /dev/null
+++ b/macros/helpdoc_system_macros/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "helpdoc_system_macros"
+edition = "2024"
+version.workspace = true
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2.workspace = true
+quote.workspace = true
+syn.workspace = true
diff --git a/macros/helpdoc_system_macros/src/lib.rs b/macros/helpdoc_system_macros/src/lib.rs
new file mode 100644
index 0000000..a9008bf
--- /dev/null
+++ b/macros/helpdoc_system_macros/src/lib.rs
@@ -0,0 +1,192 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use std::fs;
+use std::path::Path;
+use syn;
+
+#[proc_macro]
+pub fn generate_helpdoc_mapping(_input: TokenStream) -> TokenStream {
+ let manifest_dir =
+ std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get CARGO_MANIFEST_DIR");
+ let repo_root = Path::new(&manifest_dir);
+ let helpdoc_dir = repo_root.join("resources").join("helpdoc");
+
+ if !helpdoc_dir.exists() {
+ return quote! {
+ fn get_doc(_doc_name: &str, _lang: &str) -> &'static str {
+ ""
+ }
+ }
+ .into();
+ }
+
+ let mut doc_entries = Vec::new();
+
+ scan_directory(&helpdoc_dir, &mut doc_entries, &helpdoc_dir);
+
+ let match_arms = generate_match_arms(&doc_entries);
+
+ let expanded = quote! {
+ fn get_doc(doc_name: &str, lang: &str) -> &'static str {
+ let key = format!("{}.{}", doc_name, lang);
+ match key.as_str() {
+ #(#match_arms)*
+ _ => "",
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+fn scan_directory(dir: &Path, entries: &mut Vec<(String, String)>, base_dir: &Path) {
+ if let Ok(entries_iter) = fs::read_dir(dir) {
+ for entry in entries_iter.filter_map(Result::ok) {
+ let path = entry.path();
+
+ if path.is_dir() {
+ scan_directory(&path, entries, base_dir);
+ } else if let Some(extension) = path.extension() {
+ if extension == "md" {
+ if let Ok(relative_path) = path.strip_prefix(base_dir) {
+ if let Some(file_stem) = path.file_stem() {
+ let file_stem_str = file_stem.to_string_lossy();
+
+ if let Some(dot_pos) = file_stem_str.rfind('.') {
+ let doc_name = &file_stem_str[..dot_pos];
+ let lang = &file_stem_str[dot_pos + 1..];
+
+ let parent = relative_path.parent();
+ let full_doc_name = if let Some(parent) = parent {
+ if parent.to_string_lossy().is_empty() {
+ doc_name.to_string()
+ } else {
+ format!("{}/{}", parent.to_string_lossy(), doc_name)
+ }
+ } else {
+ doc_name.to_string()
+ };
+
+ entries.push((full_doc_name, lang.to_string()));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn generate_match_arms(entries: &[(String, String)]) -> Vec<proc_macro2::TokenStream> {
+ let mut arms = Vec::new();
+
+ for (doc_name, lang) in entries {
+ let key = format!("{}.{}", doc_name, lang);
+ let file_path = format!("resources/helpdoc/{}.{}.md", doc_name, lang);
+
+ let arm = quote! {
+ #key => include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #file_path)),
+ };
+
+ arms.push(arm);
+ }
+
+ arms
+}
+
+#[proc_macro]
+pub fn generate_helpdoc_list(_input: TokenStream) -> TokenStream {
+ let manifest_dir =
+ std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get CARGO_MANIFEST_DIR");
+ let repo_root = Path::new(&manifest_dir);
+ let helpdoc_dir = repo_root.join("resources").join("helpdoc");
+
+ if !helpdoc_dir.exists() {
+ return quote! {
+ fn get_docs_list() -> Vec<&'static str> {
+ Vec::new()
+ }
+ }
+ .into();
+ }
+
+ let mut doc_entries = Vec::new();
+ scan_directory(&helpdoc_dir, &mut doc_entries, &helpdoc_dir);
+
+ let mut unique_docs = std::collections::HashSet::new();
+ for (doc_name, _) in &doc_entries {
+ unique_docs.insert(doc_name.clone());
+ }
+
+ let mut doc_list = Vec::new();
+ for doc_name in unique_docs {
+ doc_list.push(quote! {
+ #doc_name
+ });
+ }
+
+ let expanded = quote! {
+ fn get_docs_list() -> Vec<&'static str> {
+ vec![
+ #(#doc_list),*
+ ]
+ }
+ };
+
+ expanded.into()
+}
+
+#[proc_macro]
+pub fn generate_helpdoc_test(_input: TokenStream) -> TokenStream {
+ let manifest_dir =
+ std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get CARGO_MANIFEST_DIR");
+ let repo_root = Path::new(&manifest_dir);
+ let helpdoc_dir = repo_root.join("resources").join("helpdoc");
+
+ if !helpdoc_dir.exists() {
+ return quote! {
+ #[cfg(test)]
+ mod helpdoc_tests {
+ #[test]
+ fn test_no_docs() {
+ }
+ }
+ }
+ .into();
+ }
+
+ let mut doc_entries = Vec::new();
+ scan_directory(&helpdoc_dir, &mut doc_entries, &helpdoc_dir);
+
+ let mut test_cases = Vec::new();
+
+ for (doc_name, lang) in &doc_entries {
+ let test_name_str = format!(
+ "test_doc_{}_{}",
+ doc_name
+ .replace('/', "_")
+ .replace('.', "_")
+ .replace('-', "_"),
+ lang.replace('-', "_")
+ );
+ let test_name = syn::Ident::new(&test_name_str, proc_macro2::Span::call_site());
+ let test_case = quote! {
+ #[test]
+ fn #test_name() {
+ let doc = super::get_doc(#doc_name, #lang);
+ assert!(!doc.is_empty(), "Document {}.{} should not be empty", #doc_name, #lang);
+ }
+ };
+
+ test_cases.push(test_case);
+ }
+
+ let expanded = quote! {
+ #[cfg(test)]
+ mod helpdoc_tests {
+ #(#test_cases)*
+ }
+ };
+
+ expanded.into()
+}
diff --git a/resources/helpdoc/Welcome_To_JVCS.en.md b/resources/helpdoc/Welcome_To_JVCS.en.md
new file mode 100644
index 0000000..f82f58e
--- /dev/null
+++ b/resources/helpdoc/Welcome_To_JVCS.en.md
@@ -0,0 +1 @@
+# Welcome
diff --git a/resources/helpdoc/Welcome_To_JVCS.zh-CN.md b/resources/helpdoc/Welcome_To_JVCS.zh-CN.md
new file mode 100644
index 0000000..2dfd3ee
--- /dev/null
+++ b/resources/helpdoc/Welcome_To_JVCS.zh-CN.md
@@ -0,0 +1 @@
+# 你好
diff --git a/resources/helpdoc/commands/hexdump.en.md b/resources/helpdoc/commands/hexdump.en.md
new file mode 100644
index 0000000..60d5275
--- /dev/null
+++ b/resources/helpdoc/commands/hexdump.en.md
@@ -0,0 +1,6 @@
+# Hexdump
+
+> Print binary information
+
+## Usage
+jvn hexdump <FILE>
diff --git a/resources/helpdoc/commands/hexdump.zh-CN.md b/resources/helpdoc/commands/hexdump.zh-CN.md
new file mode 100644
index 0000000..9b5b8ad
--- /dev/null
+++ b/resources/helpdoc/commands/hexdump.zh-CN.md
@@ -0,0 +1,6 @@
+# Hexdump
+
+> 打印二进制信息
+
+## 使用
+jvn hexdump <FILE>
diff --git a/resources/helpdoc/commands/sheetdump.en.md b/resources/helpdoc/commands/sheetdump.en.md
new file mode 100644
index 0000000..1616f12
--- /dev/null
+++ b/resources/helpdoc/commands/sheetdump.en.md
@@ -0,0 +1,13 @@
+# Sheetdump
+
+> Visually output the internal structure of a `Sheet`
+
+## Usage
+jvn sheetdump <SHEET_FILE> # Default output
+jvn sheetdump <SHEET_FILE> --no-sort # No sorting
+jvn sheetdump <SHEET_FILE> --no-pretty # No prettifying
+
+## Tip
+You can also use `renderer override` to access the internal structure of a `Sheet`,
+for example:
+jvn sheetdump <SHEET_FILE> --renderer json | jq ".mappings"
diff --git a/resources/helpdoc/commands/sheetdump.zh-CN.md b/resources/helpdoc/commands/sheetdump.zh-CN.md
new file mode 100644
index 0000000..0ae46ef
--- /dev/null
+++ b/resources/helpdoc/commands/sheetdump.zh-CN.md
@@ -0,0 +1,12 @@
+# Sheetdump
+
+> 可视化地输出 `Sheet` 的内部结构
+
+## 使用
+jvn sheetdump <SHEET_FILE> # 默认输出
+jvn sheetdump <SHEET_FILE> --no-sort # 无排序
+jvn sheetdump <SHEET_FILE> --no-pretty # 无美化
+
+## 提示
+您也可以使用 `渲染器重载` 来访问 `Sheet` 的内部结构,例如
+jvn sheetdump <SHEET_FILE> --renderer json | jq ".mappings"
diff --git a/resources/helpdoc/commands/sheetedit.en.md b/resources/helpdoc/commands/sheetedit.en.md
new file mode 100644
index 0000000..80f2a7b
--- /dev/null
+++ b/resources/helpdoc/commands/sheetedit.en.md
@@ -0,0 +1,12 @@
+# Sheetedit
+
+> Edit `Sheet` files using your system editor
+
+## Usage
+jvn sheetedit <FILE>
+
+## Note
+It reads and uses a command-line editor program in the following priority order:
+1. JV\_TEXT\_EDITOR
+2. EDITOR
+3. If neither exists, it falls back to using `jvii`
diff --git a/resources/helpdoc/commands/sheetedit.zh-CN.md b/resources/helpdoc/commands/sheetedit.zh-CN.md
new file mode 100644
index 0000000..f30825a
--- /dev/null
+++ b/resources/helpdoc/commands/sheetedit.zh-CN.md
@@ -0,0 +1,12 @@
+# Sheetedit
+
+> 使用系统编辑器修改 `Sheet` 文件
+
+## 使用
+jvn sheetedit <FILE>
+
+## 注意
+它读取按照以下优先级寻找命令行编辑器程序:
+1. JV\_TEXT\_EDITOR
+2. EDITOR
+3. 若都不存在,使用 `jvii` 回退
diff --git a/resources/locales/jvn/en.yml b/resources/locales/jvn/en.yml
index 6e632f2..7df57fd 100644
--- a/resources/locales/jvn/en.yml
+++ b/resources/locales/jvn/en.yml
@@ -1,28 +1,25 @@
-help: |
- NO
-
process_error:
no_matching_command: |
No matching command found!
- Use `jv -h` to get help
+ Use `jvn -h` to get help
no_matching_command_but_similar: |
No matching command found, but similar commands were found:
- jv %{similars}
+ jvn %{similars}
- Use `jv -h` to get help
+ Use `jvn -h` to get help
no_node_found: |
Unable to find command `%{node}`
This is usually a compilation-phase issue, not a user input error.
- Please use `jv -v -C` to get detailed version traceback and contact the developers.
+ Please use `jvn -v -C` to get detailed version traceback and contact the developers.
github: https://github.com/JustEnoughVCS/CommandLine/
parse_error: |
An error occurred while parsing your command arguments!
- Please use `jv <command> --help` to view help
+ Please use `jvn <command> --help` to view help
renderer_override_but_request_help: |
Renderer override mode is enabled, but help output was requested.
@@ -38,7 +35,7 @@ process_error:
Type conversion failed!
This is usually a compilation-phase issue, not a user input error.
- Please use `jv -v -C` to get detailed version traceback and contact the developers.
+ Please use `jvn -v -C` to get detailed version traceback and contact the developers.
github: https://github.com/JustEnoughVCS/CommandLine/
@@ -57,19 +54,19 @@ prepare_error:
local_config_not_found: |
Failed to read workspace config. File may not exist or format mismatch.
- Use `jv update` and try again.
+ Use `jvn update` and try again.
latest_info_not_found: |
Unable to read latest upstream info!
- Use `jv update` and try again.
+ Use `jvn update` and try again.
latest_file_data_not_exist: |
Unable to read latest file info for member `%{member_id}`!
- Use `jv update` and try again.
+ Use `jvn update` and try again.
cached_sheet_not_found: |
Unable to read cached upstream sheet `%{sheet_name}`!
- Use `jv update` and try again.
+ Use `jvn update` and try again.
local_sheet_not_found: |
Unable to read local sheet `%{sheet_name}` for member `%{member_id}`.
@@ -80,7 +77,7 @@ prepare_error:
Also, verify that a structure sheet is in use.
no_sheet_in_use: |
- No sheet in use. Use `jv use <structure_sheet_name>`.
+ No sheet in use. Use `jvn use <structure_sheet_name>`.
execute_error:
io: |
@@ -110,7 +107,7 @@ render_error:
type_mismatch: |
Render type mismatch!
This is usually a compilation-phase issue, not a user input error.
- Please use `jv -v -C` to get detailed version traceback and contact the developers.
+ Please use `jvn -v -C` to get detailed version traceback and contact the developers.
github: https://github.com/JustEnoughVCS/CommandLine/
diff --git a/resources/locales/jvn/helpdoc_viewer/en.yml b/resources/locales/jvn/helpdoc_viewer/en.yml
new file mode 100644
index 0000000..1f5d9d4
--- /dev/null
+++ b/resources/locales/jvn/helpdoc_viewer/en.yml
@@ -0,0 +1,5 @@
+helpdoc_viewer:
+ tree_area_hint: |
+ _List_ | ↑↓: Page | ←→/Space: Switch view | Q: Quit
+ content_area_hint: |
+ _Read_ | ↑↓: Page | ←→/Space: Switch view | Q: Quit
diff --git a/resources/locales/jvn/helpdoc_viewer/zh-CN.yml b/resources/locales/jvn/helpdoc_viewer/zh-CN.yml
new file mode 100644
index 0000000..65825d5
--- /dev/null
+++ b/resources/locales/jvn/helpdoc_viewer/zh-CN.yml
@@ -0,0 +1,5 @@
+helpdoc_viewer:
+ tree_area_hint: |
+ _列表_ | ↑↓: 翻页 | ←→/Space: 切换视图 | Q: 退出
+ content_area_hint: |
+ _阅读_ | ↑↓: 浏览 | ←→/Space: 切换视图 | Q: 退出
diff --git a/resources/locales/jvn/zh-CN.yml b/resources/locales/jvn/zh-CN.yml
index f62b53b..ff1b67a 100644
--- a/resources/locales/jvn/zh-CN.yml
+++ b/resources/locales/jvn/zh-CN.yml
@@ -1,27 +1,24 @@
-help: |
- NO
-
process_error:
no_matching_command: |
- 无法匹配该命令,使用 `jv -h` 查看帮助
+ 无法匹配该命令,使用 `jvn -h` 查看帮助
no_matching_command_but_similar: |
无法找到匹配的命令,但找到相似命令:
- jv %{similars}
+ jvn %{similars}
- 使用 `jv -h` 查看帮助
+ 使用 `jvn -h` 查看帮助
no_node_found: |
无法找到命令 `%{node}`
这通常是编译期造成的问题,而非用户的错误输入
- 请使用 `jv -v -C` 获得详细的版本追溯,并联系开发人员
+ 请使用 `jvn -v -C` 获得详细的版本追溯,并联系开发人员
github: https://github.com/JustEnoughVCS/CommandLine/
parse_error: |
在将您的命令参数进行解析时出现错误!
- 请使用 `jv <命令> --help` 查看帮助
+ 请使用 `jvn <命令> --help` 查看帮助
renderer_override_but_request_help: |
启用渲染器覆盖模式,但请求输出帮助信息
@@ -37,7 +34,7 @@ process_error:
类型转换失败!
这通常是编译期造成的问题,而非用户的错误输入
- 请使用 `jv -v -C` 获得详细的版本追溯,并联系开发人员
+ 请使用 `jvn -v -C` 获得详细的版本追溯,并联系开发人员
github: https://github.com/JustEnoughVCS/CommandLine/
@@ -56,19 +53,19 @@ prepare_error:
local_config_not_found: |
读取本地工作区配置文件失败,它可能不存在或格式不匹配
- 请使用 `jv update` 更新工作区信息后再尝试
+ 请使用 `jvn update` 更新工作区信息后再尝试
latest_info_not_found: |
无法找到或读取最新上游信息!
- 请使用 `jv update` 更新工作区信息后再尝试
+ 请使用 `jvn update` 更新工作区信息后再尝试
latest_file_data_not_exist: |
无法找到或读取成员 `%{member_id}` 的最新文件信息!
- 请使用 `jv update` 更新工作区信息后再尝试
+ 请使用 `jvn update` 更新工作区信息后再尝试
cached_sheet_not_found: |
无法找到或读取上游结构表 `%{sheet_name}` 的缓存信息!
- 请使用 `jv update` 更新工作区信息后再尝试
+ 请使用 `jvn update` 更新工作区信息后再尝试
local_sheet_not_found: |
无法找到或读取成员 `%{member_id}` 的本地结构表 `%{sheet_name}`
@@ -78,7 +75,7 @@ prepare_error:
请检查当前工作区是否正确设置上游,并正在使用结构表
no_sheet_in_use: |
- 当前没有在使用表,请使用 `jv use <结构表名称>` 使用一张结构表
+ 当前没有在使用表,请使用 `jvn use <结构表名称>` 使用一张结构表
execute_error:
io: |
@@ -108,7 +105,7 @@ render_error:
type_mismatch: |
渲染类型不匹配!
这通常是编译期造成的问题,而非用户的错误输入
- 请使用 `jv -v -C` 获得详细的版本追溯,并联系开发人员
+ 请使用 `jvn -v -C` 获得详细的版本追溯,并联系开发人员
github: https://github.com/JustEnoughVCS/CommandLine/
diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs
index 3a95a45..0e5a9f6 100644
--- a/src/bin/jvn.rs
+++ b/src/bin/jvn.rs
@@ -11,6 +11,7 @@ use just_enough_vcs_cli::{
processer::jv_cmd_process,
},
debug::verbose_logger::init_verbose_logger,
+ helpdoc::helpdoc_viewer,
},
};
use just_progress::{
@@ -100,7 +101,7 @@ async fn main() {
// Handle help when no arguments provided
if args.len() < 1 && help {
warn!("{}", t!("verbose.no_arguments"));
- eprintln!("{}", md(t!("help")));
+ helpdoc_viewer::display_with_lang("Welcome_To_JVCS", &lang).await;
exit(1);
}
diff --git a/src/cmds/arg/helpdoc.rs b/src/cmds/arg/helpdoc.rs
new file mode 100644
index 0000000..3a34123
--- /dev/null
+++ b/src/cmds/arg/helpdoc.rs
@@ -0,0 +1,6 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+pub struct JVHelpdocArgument {
+ pub doc_name: String,
+}
diff --git a/src/cmds/cmd/helpdoc.rs b/src/cmds/cmd/helpdoc.rs
new file mode 100644
index 0000000..8b692ee
--- /dev/null
+++ b/src/cmds/cmd/helpdoc.rs
@@ -0,0 +1,49 @@
+use crate::{
+ cmd_output,
+ cmds::{
+ arg::helpdoc::JVHelpdocArgument, collect::empty::JVEmptyCollect,
+ r#in::helpdoc::JVHelpdocInput, out::none::JVNoneOutput,
+ },
+ systems::{
+ cmd::{
+ cmd_system::JVCommandContext,
+ errors::{CmdExecuteError, CmdPrepareError},
+ },
+ helpdoc::helpdoc_viewer,
+ },
+};
+use cmd_system_macros::exec;
+use std::any::TypeId;
+
+pub struct JVHelpdocCommand;
+type Cmd = JVHelpdocCommand;
+type Arg = JVHelpdocArgument;
+type In = JVHelpdocInput;
+type Collect = JVEmptyCollect;
+
+async fn help_str() -> String {
+ helpdoc_viewer::display("Welcome_To_JVCS").await;
+ String::new()
+}
+
+async fn prepare(args: &Arg, ctx: &JVCommandContext) -> Result<In, CmdPrepareError> {
+ Ok(JVHelpdocInput {
+ name: args.doc_name.clone(),
+ lang: ctx.lang.clone(),
+ })
+}
+
+async fn collect(_args: &Arg, _ctx: &JVCommandContext) -> Result<Collect, CmdPrepareError> {
+ Ok(JVEmptyCollect)
+}
+
+#[exec]
+async fn exec(
+ input: In,
+ _collect: Collect,
+) -> Result<(Box<dyn std::any::Any + Send + 'static>, TypeId), CmdExecuteError> {
+ helpdoc_viewer::display_with_lang(&input.name.as_str(), &input.lang.as_str()).await;
+ cmd_output!(JVNoneOutput => JVNoneOutput)
+}
+
+crate::command_template!();
diff --git a/src/cmds/cmd/hexdump.rs b/src/cmds/cmd/hexdump.rs
index 66022ef..928b626 100644
--- a/src/cmds/cmd/hexdump.rs
+++ b/src/cmds/cmd/hexdump.rs
@@ -6,9 +6,12 @@ use crate::{
arg::single_file::JVSingleFileArgument, collect::single_file::JVSingleFileCollect,
r#in::empty::JVEmptyInput, out::hex::JVHexOutput,
},
- systems::cmd::{
- cmd_system::JVCommandContext,
- errors::{CmdExecuteError, CmdPrepareError},
+ systems::{
+ cmd::{
+ cmd_system::JVCommandContext,
+ errors::{CmdExecuteError, CmdPrepareError},
+ },
+ helpdoc::helpdoc_viewer,
},
};
use cmd_system_macros::exec;
@@ -20,8 +23,9 @@ type Arg = JVSingleFileArgument;
type In = JVEmptyInput;
type Collect = JVSingleFileCollect;
-fn help_str() -> String {
- "Hello".to_string()
+async fn help_str() -> String {
+ helpdoc_viewer::display("commands/hexdump").await;
+ String::new()
}
async fn prepare(_args: &Arg, _ctx: &JVCommandContext) -> Result<In, CmdPrepareError> {
diff --git a/src/cmds/cmd/sheetdump.rs b/src/cmds/cmd/sheetdump.rs
index fefa6d4..8140a0d 100644
--- a/src/cmds/cmd/sheetdump.rs
+++ b/src/cmds/cmd/sheetdump.rs
@@ -8,9 +8,12 @@ use crate::{
r#in::sheetdump::JVSheetdumpInput,
out::{mappings::JVMappingsOutput, mappings_pretty::JVMappingsPrettyOutput},
},
- systems::cmd::{
- cmd_system::JVCommandContext,
- errors::{CmdExecuteError, CmdPrepareError},
+ systems::{
+ cmd::{
+ cmd_system::JVCommandContext,
+ errors::{CmdExecuteError, CmdPrepareError},
+ },
+ helpdoc::helpdoc_viewer,
},
};
use cmd_system_macros::exec;
@@ -25,8 +28,9 @@ type Arg = JVSheetdumpArgument;
type In = JVSheetdumpInput;
type Collect = JVSheetdumpCollect;
-fn help_str() -> String {
- todo!()
+async fn help_str() -> String {
+ helpdoc_viewer::display("commands/sheetdump").await;
+ String::new()
}
async fn prepare(args: &Arg, _ctx: &JVCommandContext) -> Result<In, CmdPrepareError> {
diff --git a/src/cmds/cmd/sheetedit.rs b/src/cmds/cmd/sheetedit.rs
index 2dfbbfb..08918f4 100644
--- a/src/cmds/cmd/sheetedit.rs
+++ b/src/cmds/cmd/sheetedit.rs
@@ -4,9 +4,12 @@ use crate::{
arg::sheetedit::JVSheeteditArgument, collect::single_file::JVSingleFileCollect,
r#in::sheetedit::JVSheeteditInput, out::none::JVNoneOutput,
},
- systems::cmd::{
- cmd_system::JVCommandContext,
- errors::{CmdExecuteError, CmdPrepareError},
+ systems::{
+ cmd::{
+ cmd_system::JVCommandContext,
+ errors::{CmdExecuteError, CmdPrepareError},
+ },
+ helpdoc::helpdoc_viewer,
},
};
use cli_utils::{
@@ -26,8 +29,9 @@ type Arg = JVSheeteditArgument;
type In = JVSheeteditInput;
type Collect = JVSingleFileCollect;
-fn help_str() -> String {
- todo!()
+async fn help_str() -> String {
+ helpdoc_viewer::display("commands/sheetedit").await;
+ String::new()
}
async fn prepare(args: &Arg, _ctx: &JVCommandContext) -> Result<In, CmdPrepareError> {
diff --git a/src/cmds/in/helpdoc.rs b/src/cmds/in/helpdoc.rs
new file mode 100644
index 0000000..6b72d43
--- /dev/null
+++ b/src/cmds/in/helpdoc.rs
@@ -0,0 +1,4 @@
+pub struct JVHelpdocInput {
+ pub name: String,
+ pub lang: String,
+}
diff --git a/src/systems.rs b/src/systems.rs
index 2ca53be..e0d4491 100644
--- a/src/systems.rs
+++ b/src/systems.rs
@@ -1,3 +1,4 @@
pub mod cmd;
pub mod debug;
+pub mod helpdoc;
pub mod render;
diff --git a/src/systems/cmd/cmd_system.rs b/src/systems/cmd/cmd_system.rs
index 67f5c7f..3ae4d5e 100644
--- a/src/systems/cmd/cmd_system.rs
+++ b/src/systems/cmd/cmd_system.rs
@@ -28,7 +28,7 @@ where
Collect: Send,
{
/// Get help string for the command
- fn get_help_str() -> String;
+ fn get_help_str() -> impl Future<Output = String> + Send;
/// Run the command and convert the result into type-agnostic serialized information,
/// then hand it over to the universal renderer for rendering.
@@ -72,7 +72,10 @@ where
// skip execution and directly render help information
if ctx.help {
let mut r = JVRenderResult::default();
- r_println!(r, "{}", Self::get_help_str());
+ let help_str = Self::get_help_str().await;
+ if !help_str.is_empty() {
+ r_println!(r, "{}", help_str);
+ }
return Ok(r);
}
@@ -112,7 +115,7 @@ where
t = type_name::<Argument>()
)
);
- return Err(CmdProcessError::ParseError(Self::get_help_str()));
+ return Err(CmdProcessError::ParseError(Self::get_help_str().await));
}
};
diff --git a/src/systems/cmd/macros.rs b/src/systems/cmd/macros.rs
index 093d178..e9af1ac 100644
--- a/src/systems/cmd/macros.rs
+++ b/src/systems/cmd/macros.rs
@@ -51,7 +51,7 @@
/// type Collect = JVCustomCollect;
///
/// /// Return a string, rendered when the user needs help (command specifies `--help` or syntax error)
-/// fn help_str() -> String {
+/// async fn help_str() -> String {
/// todo!()
/// }
///
@@ -97,7 +97,7 @@
/// type In = JVCustomInput;
/// type Collect = JVCustomCollect;
///
-/// fn help_str() -> String {
+/// async fn help_str() -> String {
/// todo!()
/// }
///
@@ -121,8 +121,8 @@
macro_rules! command_template {
() => {
impl $crate::systems::cmd::cmd_system::JVCommand<Arg, In, Collect> for Cmd {
- fn get_help_str() -> String {
- help_str()
+ async fn get_help_str() -> String {
+ help_str().await
}
async fn prepare(
diff --git a/src/systems/helpdoc.rs b/src/systems/helpdoc.rs
new file mode 100644
index 0000000..4e5b2a6
--- /dev/null
+++ b/src/systems/helpdoc.rs
@@ -0,0 +1,18 @@
+pub mod helpdoc_viewer;
+
+helpdoc_system_macros::generate_helpdoc_mapping!();
+helpdoc_system_macros::generate_helpdoc_list!();
+helpdoc_system_macros::generate_helpdoc_test!();
+
+pub fn get_helpdoc<'a>(doc_name: &'a str, lang: &'a str) -> &'a str {
+ let doc = get_doc(doc_name, lang);
+ if doc.is_empty() && lang != "en" {
+ get_doc(doc_name, "en")
+ } else {
+ doc
+ }
+}
+
+pub fn get_helpdoc_list<'a>() -> Vec<&'a str> {
+ get_docs_list()
+}
diff --git a/src/systems/helpdoc/helpdoc_viewer.rs b/src/systems/helpdoc/helpdoc_viewer.rs
new file mode 100644
index 0000000..1968775
--- /dev/null
+++ b/src/systems/helpdoc/helpdoc_viewer.rs
@@ -0,0 +1,636 @@
+use crate::systems::helpdoc::{get_helpdoc, get_helpdoc_list};
+use cli_utils::{display::markdown::Markdown, env::locales::current_locales};
+use crossterm::{
+ cursor::{Hide, MoveTo, Show},
+ event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
+ execute,
+ style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
+ terminal::{
+ Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
+ enable_raw_mode,
+ },
+};
+use rust_i18n::t;
+use std::{
+ collections::{BTreeMap, HashMap},
+ io::{Write, stdout},
+};
+
+struct HelpdocViewer {
+ /// Current language
+ lang: String,
+
+ /// Document tree structure
+ doc_tree: DocTree,
+
+ /// Currently selected document path
+ current_doc: String,
+
+ /// Scroll position history
+ scroll_history: HashMap<String, usize>,
+
+ /// Current focus area
+ focus: FocusArea,
+
+ /// Currently selected node index in tree view
+ tree_selection_index: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+enum FocusArea {
+ Tree,
+ Content,
+}
+
+#[derive(Debug, Clone)]
+struct DocTreeNode {
+ /// Node name
+ name: String,
+
+ /// Full document path
+ path: String,
+
+ /// Child nodes
+ children: BTreeMap<String, DocTreeNode>,
+
+ /// Whether it is a document file
+ is_document: bool,
+}
+
+#[derive(Debug, Clone)]
+struct DocTree {
+ /// Root node
+ root: DocTreeNode,
+
+ /// Flattened document list
+ flat_docs: Vec<String>,
+}
+
+impl HelpdocViewer {
+ fn new(default_doc: &str, lang: &str) -> Self {
+ // Build the document tree
+ let doc_tree = Self::build_doc_tree();
+
+ // Validate if the default document exists
+ let current_doc = if doc_tree.contains_doc(default_doc) {
+ default_doc.to_string()
+ } else {
+ // If the default document does not exist, use the first document
+ doc_tree.flat_docs.first().cloned().unwrap_or_default()
+ };
+
+ // Calculate the initial tree selection index
+ let tree_selection_index = doc_tree
+ .flat_docs
+ .iter()
+ .position(|doc| *doc == current_doc)
+ .unwrap_or(0);
+
+ Self {
+ lang: lang.to_string(),
+ doc_tree,
+ current_doc,
+ scroll_history: HashMap::new(),
+ focus: FocusArea::Content,
+ tree_selection_index,
+ }
+ }
+
+ /// Build document tree
+ fn build_doc_tree() -> DocTree {
+ // Get all document list
+ let doc_list = get_helpdoc_list();
+
+ // Create root node
+ let mut root = DocTreeNode {
+ name: "helpdoc".to_string(),
+ path: "".to_string(),
+ children: BTreeMap::new(),
+ is_document: false,
+ };
+
+ // Build tree structure for each document path
+ for doc_path in doc_list {
+ Self::add_doc_to_tree(&mut root, doc_path);
+ }
+
+ // Build flattened document list
+ let flat_docs = Self::flatten_doc_tree(&root);
+
+ DocTree { root, flat_docs }
+ }
+
+ /// Add document to tree
+ fn add_doc_to_tree(root: &mut DocTreeNode, doc_path: &str) {
+ let parts: Vec<&str> = doc_path.split('/').collect();
+
+ // Use recursive helper function to avoid borrowing issues
+ Self::add_doc_to_tree_recursive(root, &parts, 0);
+ }
+
+ /// Recursively add document to tree
+ fn add_doc_to_tree_recursive(node: &mut DocTreeNode, parts: &[&str], depth: usize) {
+ if depth >= parts.len() {
+ return;
+ }
+
+ let part = parts[depth];
+ let is_document = depth == parts.len() - 1;
+ let current_path = parts[0..=depth].join("/");
+
+ // Check if node already exists, create if not
+ if !node.children.contains_key(part) {
+ let new_node = DocTreeNode {
+ name: part.to_string(),
+ path: current_path.clone(),
+ children: BTreeMap::new(),
+ is_document,
+ };
+ node.children.insert(part.to_string(), new_node);
+ }
+
+ // Get mutable reference to child node
+ if let Some(child) = node.children.get_mut(part) {
+ // If this is a document node, ensure it's marked as document
+ if is_document {
+ child.is_document = true;
+ }
+ // Recursively process next part
+ Self::add_doc_to_tree_recursive(child, parts, depth + 1);
+ }
+ }
+
+ /// Flatten document tree
+ fn flatten_doc_tree(node: &DocTreeNode) -> Vec<String> {
+ let mut result = Vec::new();
+
+ if node.is_document && !node.path.is_empty() {
+ result.push(node.path.clone());
+ }
+
+ // Traverse child nodes in alphabetical order
+ for child in node.children.values() {
+ result.extend(Self::flatten_doc_tree(child));
+ }
+
+ result
+ }
+
+ /// Get current document content
+ fn current_doc_content(&self) -> String {
+ let content = get_helpdoc(&self.current_doc, &self.lang);
+ if content.is_empty() {
+ format!("Document '{}.{}' not found", self.current_doc, self.lang)
+ } else {
+ content.to_string()
+ }
+ }
+
+ /// Get formatted document content
+ fn formatted_doc_content(&self) -> Vec<String> {
+ let content = self.current_doc_content();
+ let formatted = content.markdown();
+ formatted.lines().map(|s| s.to_string()).collect()
+ }
+
+ /// Run viewer
+ async fn run(&mut self) -> std::io::Result<()> {
+ enable_raw_mode()?;
+ execute!(stdout(), EnterAlternateScreen, Hide)?;
+
+ let mut should_exit = false;
+
+ while !should_exit {
+ self.draw()?;
+
+ if event::poll(std::time::Duration::from_millis(100))? {
+ if let Event::Key(key) = event::read()? {
+ should_exit = self.handle_key(key);
+ }
+ }
+ }
+
+ execute!(stdout(), Show, LeaveAlternateScreen)?;
+ disable_raw_mode()?;
+
+ Ok(())
+ }
+
+ /// Draw interface
+ fn draw(&self) -> std::io::Result<()> {
+ let (width, height) = crossterm::terminal::size()?;
+
+ if height <= 3 {
+ let content = self.current_doc_content();
+ println!("{}", content);
+ return Ok(());
+ }
+
+ execute!(stdout(), Clear(ClearType::All))?;
+
+ let tree_width = 25;
+ let content_width = width - tree_width - 1;
+
+ // Draw title
+ execute!(
+ stdout(),
+ MoveTo(0, 0),
+ SetForegroundColor(Color::Cyan),
+ Print(format!("JVCS Help Docs - {}", self.lang)),
+ ResetColor
+ )?;
+
+ // Draw separator line
+ for y in 1..height {
+ execute!(stdout(), MoveTo(tree_width, y), Print("│"))?;
+ }
+
+ // Draw tree view
+ self.draw_tree(0, 2, tree_width, height - 4)?;
+
+ // Draw content area
+ self.draw_content(tree_width + 1, 2, content_width, height - 4)?;
+
+ // Draw status bar
+ self.draw_status_bar(height - 1, width)?;
+
+ stdout().flush()?;
+ Ok(())
+ }
+
+ /// Draw tree view
+ fn draw_tree(&self, x: u16, y: u16, width: u16, height: u16) -> std::io::Result<()> {
+ // Draw tree view title
+ execute!(
+ stdout(),
+ MoveTo(x, y - 1),
+ SetForegroundColor(Color::Yellow),
+ Print("Documents"),
+ ResetColor
+ )?;
+
+ // Recursively draw tree structure
+ let mut line_counter = 0;
+ let max_lines = height as usize;
+
+ // Skip root node, start drawing from children
+ for child in self.doc_tree.root.children.values() {
+ line_counter = self.draw_tree_node(child, x, y, width, 0, line_counter, max_lines)?;
+ if line_counter >= max_lines {
+ break;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Recursively draw tree node
+ fn draw_tree_node(
+ &self,
+ node: &DocTreeNode,
+ x: u16,
+ start_y: u16,
+ width: u16,
+ depth: usize,
+ mut line_counter: usize,
+ max_lines: usize,
+ ) -> std::io::Result<usize> {
+ if line_counter >= max_lines {
+ return Ok(line_counter);
+ }
+
+ let line_y = start_y + line_counter as u16;
+ line_counter += 1;
+
+ // Build indentation and suffix
+ let indent = " ".repeat(depth);
+ let suffix = if node.children.is_empty() { "" } else { "/" };
+
+ // If this is the currently selected document, highlight it (white background, black text)
+ let is_selected = node.path == self.current_doc;
+
+ if is_selected {
+ // Highlight with white background and black text
+ execute!(
+ stdout(),
+ MoveTo(x, line_y),
+ SetForegroundColor(Color::Black),
+ SetBackgroundColor(Color::White),
+ Print(" ".repeat(width as usize)),
+ MoveTo(x, line_y),
+ SetForegroundColor(Color::Black),
+ )?;
+ } else {
+ // Normal display
+ execute!(
+ stdout(),
+ MoveTo(x, line_y),
+ SetForegroundColor(Color::White),
+ SetBackgroundColor(Color::Black),
+ )?;
+ }
+
+ // Display node name
+ let display_text = format!("{} {}{}", indent, node.name, suffix);
+ execute!(stdout(), Print(display_text))?;
+ execute!(stdout(), ResetColor, SetBackgroundColor(Color::Black))?;
+
+ // Recursively draw child nodes
+ if !node.children.is_empty() {
+ for child in node.children.values() {
+ line_counter = self.draw_tree_node(
+ child,
+ x,
+ start_y,
+ width,
+ depth + 1,
+ line_counter,
+ max_lines,
+ )?;
+ if line_counter >= max_lines {
+ break;
+ }
+ }
+ }
+
+ Ok(line_counter)
+ }
+
+ /// Draw content area
+ fn draw_content(&self, x: u16, y: u16, width: u16, height: u16) -> std::io::Result<()> {
+ // Draw content area title
+ execute!(
+ stdout(),
+ MoveTo(x, y - 1),
+ SetForegroundColor(Color::Yellow),
+ Print(format!("> {}", self.current_doc)),
+ ResetColor
+ )?;
+
+ // Get formatted content
+ let content_lines = self.formatted_doc_content();
+ let scroll_pos = self
+ .scroll_history
+ .get(&self.current_doc)
+ .copied()
+ .unwrap_or(0);
+ let start_line = scroll_pos.min(content_lines.len().saturating_sub(1));
+ let end_line = (start_line + height as usize).min(content_lines.len());
+
+ for (i, line) in content_lines
+ .iter()
+ .enumerate()
+ .take(end_line)
+ .skip(start_line)
+ {
+ let line_y = y + i as u16 - start_line as u16;
+ let display_line = if line.len() > width as usize {
+ let mut end = width as usize;
+ while end > 0 && !line.is_char_boundary(end) {
+ end -= 1;
+ }
+ if end == 0 {
+ // If the first character is multi-byte, display at least one character
+ let mut end = 1;
+ while end < line.len() && !line.is_char_boundary(end) {
+ end += 1;
+ }
+ &line[..end]
+ } else {
+ &line[..end]
+ }
+ } else {
+ line
+ };
+
+ execute!(stdout(), MoveTo(x, line_y), Print(display_line))?;
+ }
+
+ // Display scroll position indicator
+ if content_lines.len() > height as usize && content_lines.len() > 0 {
+ let scroll_percent = if content_lines.len() > 0 {
+ (scroll_pos * 100) / content_lines.len()
+ } else {
+ 0
+ };
+ execute!(
+ stdout(),
+ MoveTo(x + width - 5, y - 1),
+ SetForegroundColor(Color::DarkGrey),
+ Print(format!("{:3}%", scroll_percent)),
+ ResetColor
+ )?;
+ }
+
+ Ok(())
+ }
+
+ /// Draw status bar
+ fn draw_status_bar(&self, y: u16, width: u16) -> std::io::Result<()> {
+ // Draw status bar background
+ execute!(
+ stdout(),
+ MoveTo(0, y),
+ SetForegroundColor(Color::Black),
+ Print(" ".repeat(width as usize)),
+ MoveTo(0, y),
+ SetForegroundColor(Color::White),
+ )?;
+
+ let status_text = match self.focus {
+ FocusArea::Tree => t!("helpdoc_viewer.tree_area_hint").to_string().markdown(),
+ FocusArea::Content => t!("helpdoc_viewer.content_area_hint")
+ .to_string()
+ .markdown(),
+ }
+ .to_string();
+
+ let truncated_text = if status_text.len() > width as usize {
+ &status_text[..width as usize]
+ } else {
+ &status_text
+ };
+ execute!(stdout(), Print(truncated_text))?;
+ execute!(stdout(), ResetColor)?;
+
+ Ok(())
+ }
+
+ /// Handle key input
+ fn handle_key(&mut self, key: KeyEvent) -> bool {
+ match key.code {
+ KeyCode::Char('q') | KeyCode::Esc => return true,
+ KeyCode::Char(' ') => self.toggle_focus(),
+ KeyCode::Left => self.move_left(),
+ KeyCode::Right => self.move_right(),
+ KeyCode::Up => self.move_up(),
+ KeyCode::Down => self.move_down(),
+ KeyCode::Char('g') if key.modifiers == KeyModifiers::NONE => self.go_to_top(),
+ KeyCode::Char('G') if key.modifiers == KeyModifiers::SHIFT => self.go_to_bottom(),
+ KeyCode::Enter => self.select_item(),
+ _ => {}
+ }
+ false
+ }
+
+ /// Toggle focus area
+ fn toggle_focus(&mut self) {
+ self.focus = match self.focus {
+ FocusArea::Tree => FocusArea::Content,
+ FocusArea::Content => FocusArea::Tree,
+ };
+ }
+
+ /// Move left
+ fn move_left(&mut self) {
+ if self.focus == FocusArea::Content {
+ self.focus = FocusArea::Tree;
+ }
+ }
+
+ /// Move right
+ fn move_right(&mut self) {
+ if self.focus == FocusArea::Tree {
+ self.focus = FocusArea::Content;
+ }
+ }
+
+ /// Move up
+ fn move_up(&mut self) {
+ match self.focus {
+ FocusArea::Tree => self.previous_doc(),
+ FocusArea::Content => self.scroll_up(),
+ }
+ }
+
+ /// Move down
+ fn move_down(&mut self) {
+ match self.focus {
+ FocusArea::Tree => self.next_doc(),
+ FocusArea::Content => self.scroll_down(),
+ }
+ }
+
+ /// Scroll to top
+ fn go_to_top(&mut self) {
+ match self.focus {
+ FocusArea::Content => {
+ self.scroll_history.insert(self.current_doc.clone(), 0);
+ }
+ FocusArea::Tree => {
+ // Select first document
+ self.tree_selection_index = 0;
+ if let Some(first_doc) = self.doc_tree.flat_docs.first() {
+ self.current_doc = first_doc.clone();
+ }
+ }
+ }
+ }
+
+ /// Scroll to bottom
+ fn go_to_bottom(&mut self) {
+ match self.focus {
+ FocusArea::Content => {
+ let content_lines = self.formatted_doc_content();
+ if content_lines.len() > 10 {
+ self.scroll_history
+ .insert(self.current_doc.clone(), content_lines.len() - 10);
+ }
+ }
+ FocusArea::Tree => {
+ // Select last document
+ self.tree_selection_index = self.doc_tree.flat_docs.len().saturating_sub(1);
+ if let Some(last_doc) = self.doc_tree.flat_docs.last() {
+ self.current_doc = last_doc.clone();
+ }
+ }
+ }
+ }
+
+ /// Select current item
+ fn select_item(&mut self) {
+ match self.focus {
+ FocusArea::Tree => {
+ // Update current document to the one selected in tree view
+ if let Some(doc) = self.doc_tree.flat_docs.get(self.tree_selection_index) {
+ self.current_doc = doc.clone();
+ }
+ // Switch focus to content area
+ self.focus = FocusArea::Content;
+ }
+ _ => {}
+ }
+ }
+
+ /// Previous document
+ fn previous_doc(&mut self) {
+ if self.tree_selection_index > 0 {
+ self.tree_selection_index -= 1;
+ if let Some(doc) = self.doc_tree.flat_docs.get(self.tree_selection_index) {
+ self.current_doc = doc.clone();
+ // Reset scroll position
+ self.scroll_history.remove(&self.current_doc);
+ }
+ }
+ }
+
+ /// Next document
+ fn next_doc(&mut self) {
+ if self.tree_selection_index + 1 < self.doc_tree.flat_docs.len() {
+ self.tree_selection_index += 1;
+ if let Some(doc) = self.doc_tree.flat_docs.get(self.tree_selection_index) {
+ self.current_doc = doc.clone();
+ // Reset scroll position
+ self.scroll_history.remove(&self.current_doc);
+ }
+ }
+ }
+
+ /// Scroll up
+ fn scroll_up(&mut self) {
+ let current_scroll = self
+ .scroll_history
+ .get(&self.current_doc)
+ .copied()
+ .unwrap_or(0);
+ if current_scroll > 0 {
+ self.scroll_history
+ .insert(self.current_doc.clone(), current_scroll - 1);
+ }
+ }
+
+ /// Scroll down
+ fn scroll_down(&mut self) {
+ let content_lines = self.formatted_doc_content();
+ let current_scroll = self
+ .scroll_history
+ .get(&self.current_doc)
+ .copied()
+ .unwrap_or(0);
+ if current_scroll + 1 < content_lines.len() {
+ self.scroll_history
+ .insert(self.current_doc.clone(), current_scroll + 1);
+ }
+ }
+}
+
+impl DocTree {
+ /// Check if document exists
+ fn contains_doc(&self, doc_path: &str) -> bool {
+ self.flat_docs.contains(&doc_path.to_string())
+ }
+}
+
+/// Display help document viewer
+pub async fn display_with_lang(default_focus_doc: &str, lang: &str) {
+ let mut viewer = HelpdocViewer::new(default_focus_doc, lang);
+
+ if let Err(e) = viewer.run().await {
+ eprintln!("Error running helpdoc viewer: {}", e);
+ }
+}
+
+/// Display help document viewer
+pub async fn display(default_focus_doc: &str) {
+ display_with_lang(default_focus_doc, current_locales().as_str()).await;
+}
diff --git a/utils/src/display.rs b/utils/src/display.rs
index 16c94a9..5b452ce 100644
--- a/utils/src/display.rs
+++ b/utils/src/display.rs
@@ -1,3 +1,3 @@
-pub mod colorful;
+pub mod markdown;
pub mod pager;
pub mod table;
diff --git a/utils/src/display/colorful.rs b/utils/src/display/markdown.rs
index 40f83bf..40f83bf 100644
--- a/utils/src/display/colorful.rs
+++ b/utils/src/display/markdown.rs