use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::sync::atomic::Ordering; use super::{InputContext, InputResult}; use crate::commands::AppCommand; use crate::model::categories; use crate::model::docs; use crate::state::{ConfirmAction, DictFocus, FlashKind, HelpFocus, Modal}; pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); if ctx.app.ui.help_search_active { match key.code { KeyCode::Esc => ctx.dispatch(AppCommand::HelpClearSearch), KeyCode::Enter => ctx.dispatch(AppCommand::HelpSearchConfirm), KeyCode::Backspace => ctx.dispatch(AppCommand::HelpSearchBackspace), KeyCode::Char(c) if !ctrl => ctx.dispatch(AppCommand::HelpSearchInput(c)), _ => {} } return InputResult::Continue; } match key.code { KeyCode::Char('/') | KeyCode::Char('f') if key.code == KeyCode::Char('/') || ctrl => { ctx.dispatch(AppCommand::HelpActivateSearch); } KeyCode::Esc if !ctx.app.ui.help_search_query.is_empty() => { ctx.dispatch(AppCommand::HelpClearSearch); } KeyCode::Esc if ctx.app.ui.help_focused_block.is_some() => { ctx.app.ui.help_focused_block = None; ctx.app.ui.help_block_output = None; } KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus), KeyCode::Left if ctx.app.ui.help_focus == HelpFocus::Topics => { collapse_help_section(ctx); } KeyCode::Right if ctx.app.ui.help_focus == HelpFocus::Topics => { expand_help_section(ctx); } KeyCode::Char('n') if ctx.app.ui.help_focus == HelpFocus::Content => { navigate_code_block(ctx, true); } KeyCode::Char('p') if ctx.app.ui.help_focus == HelpFocus::Content => { navigate_code_block(ctx, false); } KeyCode::Enter if ctx.app.ui.help_focus == HelpFocus::Content && ctx.app.ui.help_focused_block.is_some() => { execute_focused_block(ctx); } KeyCode::Char('j') | KeyCode::Down if ctrl => { ctx.dispatch(AppCommand::HelpNextTopic(5)); } KeyCode::Char('k') | KeyCode::Up if ctrl => { ctx.dispatch(AppCommand::HelpPrevTopic(5)); } KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.help_focus { HelpFocus::Topics => ctx.dispatch(AppCommand::HelpNextTopic(1)), HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollDown(1)), }, KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.help_focus { HelpFocus::Topics => ctx.dispatch(AppCommand::HelpPrevTopic(1)), HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollUp(1)), }, KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)), KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)), KeyCode::Char('q') if !ctx.app.plugin_mode => { ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { action: ConfirmAction::Quit, selected: false, })); } KeyCode::Char('s') => super::open_save(ctx), KeyCode::Char('l') => super::open_load(ctx), KeyCode::Char('?') => { ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); } KeyCode::Char(' ') if !ctx.app.plugin_mode => { ctx.dispatch(AppCommand::TogglePlaying); ctx.playing .store(ctx.app.playback.playing, Ordering::Relaxed); } _ => {} } InputResult::Continue } fn navigate_code_block(ctx: &mut InputContext, forward: bool) { let cache = ctx.app.ui.help_parsed.borrow(); let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else { return; }; let count = parsed.code_blocks.len(); if count == 0 { return; } let next = match ctx.app.ui.help_focused_block { Some(cur) if forward => (cur + 1) % count, Some(0) if !forward => count - 1, Some(cur) if !forward => cur - 1, _ if forward => 0, _ => count - 1, }; let scroll_to = parsed.code_blocks[next].start_line.saturating_sub(2); drop(cache); ctx.app.ui.help_focused_block = Some(next); ctx.app.ui.help_block_output = None; *ctx.app.ui.help_scroll_mut() = scroll_to; } fn execute_focused_block(ctx: &mut InputContext) { let source = { let cache = ctx.app.ui.help_parsed.borrow(); let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else { return; }; let idx = ctx.app.ui.help_focused_block.unwrap(); let Some(block) = parsed.code_blocks.get(idx) else { return; }; block.source.clone() }; let cleaned: String = source .lines() .map(|l| l.split(" => ").next().unwrap_or(l)) .collect::>() .join("\n"); let topic = ctx.app.ui.help_topic; let block_idx = ctx.app.ui.help_focused_block.expect("block focused in code nav"); match ctx .app .execute_script_oneshot(&cleaned, ctx.link, ctx.audio_tx) { Ok(Some(output)) => { ctx.app.ui.flash(&output, 200, FlashKind::Info); ctx.app.ui.help_block_output = Some((topic, block_idx, output)); } Ok(None) => ctx.app.ui.flash("Executed", 100, FlashKind::Info), Err(e) => ctx .app .ui .flash(&format!("Error: {e}"), 200, FlashKind::Error), } } fn collapse_help_section(ctx: &mut InputContext) { if let Some(s) = ctx.app.ui.help_on_section { if ctx.app.ui.help_collapsed.get(s).copied().unwrap_or(false) { return; } } let section = match ctx.app.ui.help_on_section { Some(s) => s, None => docs::section_index_for_topic(ctx.app.ui.help_topic), }; if let Some(v) = ctx.app.ui.help_collapsed.get_mut(section) { *v = true; } ctx.app.ui.help_on_section = Some(section); ctx.app.ui.help_focused_block = None; ctx.app.ui.help_block_output = None; } fn expand_help_section(ctx: &mut InputContext) { let Some(section) = ctx.app.ui.help_on_section else { return; }; if let Some(v) = ctx.app.ui.help_collapsed.get_mut(section) { *v = false; } ctx.app.ui.help_on_section = None; if let Some(first) = docs::first_topic_in_section(section) { ctx.app.ui.help_topic = first; } ctx.app.ui.help_focused_block = None; ctx.app.ui.help_block_output = None; } fn collapse_dict_section(ctx: &mut InputContext) { if let Some(s) = ctx.app.ui.dict_on_section { if ctx.app.ui.dict_collapsed.get(s).copied().unwrap_or(false) { return; } } let section = match ctx.app.ui.dict_on_section { Some(s) => s, None => categories::section_index_for_category(ctx.app.ui.dict_category), }; if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(section) { *v = true; } ctx.app.ui.dict_on_section = Some(section); } fn expand_dict_section(ctx: &mut InputContext) { let Some(section) = ctx.app.ui.dict_on_section else { return; }; if let Some(v) = ctx.app.ui.dict_collapsed.get_mut(section) { *v = false; } ctx.app.ui.dict_on_section = None; if let Some(first) = categories::first_category_in_section(section) { ctx.app.ui.dict_category = first; } } pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); if ctx.app.ui.dict_search_active { match key.code { KeyCode::Esc => ctx.dispatch(AppCommand::DictClearSearch), KeyCode::Enter => ctx.dispatch(AppCommand::DictSearchConfirm), KeyCode::Backspace => ctx.dispatch(AppCommand::DictSearchBackspace), KeyCode::Char(c) if !ctrl => ctx.dispatch(AppCommand::DictSearchInput(c)), _ => {} } return InputResult::Continue; } match key.code { KeyCode::Char('/') | KeyCode::Char('f') if key.code == KeyCode::Char('/') || ctrl => { ctx.dispatch(AppCommand::DictActivateSearch); } KeyCode::Esc if !ctx.app.ui.dict_search_query.is_empty() => { ctx.dispatch(AppCommand::DictClearSearch); } KeyCode::Tab => ctx.dispatch(AppCommand::DictToggleFocus), KeyCode::Left if ctx.app.ui.dict_focus == DictFocus::Categories => { collapse_dict_section(ctx); } KeyCode::Right if ctx.app.ui.dict_focus == DictFocus::Categories => { expand_dict_section(ctx); } KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.dict_focus { DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory), DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)), }, KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.dict_focus { DictFocus::Categories => ctx.dispatch(AppCommand::DictPrevCategory), DictFocus::Words => ctx.dispatch(AppCommand::DictScrollUp(1)), }, KeyCode::PageDown => ctx.dispatch(AppCommand::DictScrollDown(10)), KeyCode::PageUp => ctx.dispatch(AppCommand::DictScrollUp(10)), KeyCode::Char('q') if !ctx.app.plugin_mode => { ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { action: ConfirmAction::Quit, selected: false, })); } KeyCode::Char('s') => super::open_save(ctx), KeyCode::Char('l') => super::open_load(ctx), KeyCode::Char('?') => { ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); } KeyCode::Char(' ') if !ctx.app.plugin_mode => { ctx.dispatch(AppCommand::TogglePlaying); ctx.playing .store(ctx.app.playback.playing, Ordering::Relaxed); } _ => {} } InputResult::Continue }