summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
12 files changed, 753 insertions, 23 deletions
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;
+}