diff --git a/src-tauri/src/cli/tui/app/form_handlers/mcp.rs b/src-tauri/src/cli/tui/app/form_handlers/mcp.rs index f3fb31be..890ca444 100644 --- a/src-tauri/src/cli/tui/app/form_handlers/mcp.rs +++ b/src-tauri/src/cli/tui/app/form_handlers/mcp.rs @@ -114,14 +114,14 @@ impl App { key: KeyEvent, ) -> Option { match key.code { - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { let Some(FormState::McpAdd(mcp)) = self.form.as_mut() else { return None; }; mcp.field_idx = mcp.field_idx.saturating_sub(1); Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { let Some(FormState::McpAdd(mcp)) = self.form.as_mut() else { return None; }; @@ -167,11 +167,11 @@ impl App { }; match key.code { - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { mcp.json_scroll = mcp.json_scroll.saturating_sub(1); Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { mcp.json_scroll = mcp.json_scroll.saturating_add(1); Some(Action::None) } diff --git a/src-tauri/src/cli/tui/app/form_handlers/prompt.rs b/src-tauri/src/cli/tui/app/form_handlers/prompt.rs index 109068ea..226345a3 100644 --- a/src-tauri/src/cli/tui/app/form_handlers/prompt.rs +++ b/src-tauri/src/cli/tui/app/form_handlers/prompt.rs @@ -128,14 +128,14 @@ impl App { key: KeyEvent, ) -> Option { match key.code { - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { let Some(FormState::PromptMeta(prompt)) = self.form.as_mut() else { return None; }; prompt.field_idx = prompt.field_idx.saturating_sub(1); Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { let Some(FormState::PromptMeta(prompt)) = self.form.as_mut() else { return None; }; diff --git a/src-tauri/src/cli/tui/app/form_handlers/provider.rs b/src-tauri/src/cli/tui/app/form_handlers/provider.rs index 3c5a8fd8..be858d84 100644 --- a/src-tauri/src/cli/tui/app/form_handlers/provider.rs +++ b/src-tauri/src/cli/tui/app/form_handlers/provider.rs @@ -165,7 +165,7 @@ impl App { data: &UiData, ) -> Option { match key.code { - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { return None; }; @@ -177,7 +177,7 @@ impl App { } Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { return None; }; @@ -449,14 +449,14 @@ impl App { provider.close_usage_query_page(); Some(Action::None) } - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { return None; }; provider.usage_query_field_idx = provider.usage_query_field_idx.saturating_sub(1); Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { return None; }; @@ -592,11 +592,11 @@ impl App { fn handle_codex_provider_preview_key(&mut self, key: KeyEvent) -> Option { match key.code { KeyCode::Enter => Some(self.open_codex_provider_preview_editor()), - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { self.adjust_codex_preview_scroll(|scroll| scroll.saturating_sub(1)); Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { self.adjust_codex_preview_scroll(|scroll| scroll.saturating_add(1)); Some(Action::None) } @@ -707,14 +707,14 @@ impl App { } Some(Action::None) } - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { return None; }; provider.json_scroll = provider.json_scroll.saturating_sub(1); Some(Action::None) } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { return None; }; diff --git a/src-tauri/src/cli/tui/app/tests.rs b/src-tauri/src/cli/tui/app/tests.rs index c00e0369..4ca580f1 100644 --- a/src-tauri/src/cli/tui/app/tests.rs +++ b/src-tauri/src/cli/tui/app/tests.rs @@ -128,6 +128,107 @@ mod tests { } } + fn open_provider_fields_form(app_type: AppType) -> App { + let mut app = App::new(Some(app_type)); + app.open_provider_add_form(&UiData::default()); + app.on_key(key(KeyCode::Enter), &data()); + app + } + + fn open_mcp_fields_form() -> App { + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::Mcp; + app.focus = Focus::Content; + app.on_key(key(KeyCode::Char('a')), &data()); + app.on_key(key(KeyCode::Enter), &data()); + app + } + + fn open_prompt_fields_form() -> App { + let mut app = App::new(Some(AppType::Claude)); + app.route = Route::Prompts; + app.focus = Focus::Content; + app.on_key(key(KeyCode::Char('a')), &data()); + app + } + + fn provider_field_idx(app: &App) -> usize { + match app.form.as_ref() { + Some(FormState::ProviderAdd(form)) => form.field_idx, + _ => panic!("provider form should be open"), + } + } + + fn provider_editing_name(app: &App) -> (bool, String) { + match app.form.as_ref() { + Some(FormState::ProviderAdd(form)) => (form.editing, form.name.value.clone()), + _ => panic!("provider form should be open"), + } + } + + fn provider_json_scroll(app: &App) -> usize { + match app.form.as_ref() { + Some(FormState::ProviderAdd(form)) => form.json_scroll, + _ => panic!("provider form should be open"), + } + } + + fn provider_codex_auth_scroll(app: &App) -> usize { + match app.form.as_ref() { + Some(FormState::ProviderAdd(form)) => form.codex_auth_scroll, + _ => panic!("provider form should be open"), + } + } + + fn focus_provider_json_preview(app: &mut App) { + match app.form.as_mut() { + Some(FormState::ProviderAdd(form)) => form.focus = FormFocus::JsonPreview, + _ => panic!("provider form should be open"), + } + } + + fn mcp_field_idx(app: &App) -> usize { + match app.form.as_ref() { + Some(FormState::McpAdd(form)) => form.field_idx, + _ => panic!("mcp form should be open"), + } + } + + fn mcp_editing_id(app: &App) -> (bool, String) { + match app.form.as_ref() { + Some(FormState::McpAdd(form)) => (form.editing, form.id.value.clone()), + _ => panic!("mcp form should be open"), + } + } + + fn mcp_json_scroll(app: &App) -> usize { + match app.form.as_ref() { + Some(FormState::McpAdd(form)) => form.json_scroll, + _ => panic!("mcp form should be open"), + } + } + + fn focus_mcp_json_preview(app: &mut App) { + match app.form.as_mut() { + Some(FormState::McpAdd(form)) => form.focus = FormFocus::JsonPreview, + _ => panic!("mcp form should be open"), + } + } + + fn prompt_field_idx(app: &App) -> usize { + match app.form.as_ref() { + Some(FormState::PromptMeta(form)) => form.field_idx, + _ => panic!("prompt form should be open"), + } + } + + fn prompt_editing_id(app: &App) -> (bool, String) { + match app.form.as_ref() { + Some(FormState::PromptMeta(form)) => (form.editing, form.id.value.clone()), + _ => panic!("prompt form should be open"), + } + } + fn select_provider_usage_query_row(app: &mut App) { if let Some(FormState::ProviderAdd(form)) = app.form.as_mut() { form.focus = FormFocus::Fields; @@ -11152,6 +11253,201 @@ mod tests { )); } + #[test] + fn provider_form_jk_navigates_fields() { + let mut app = open_provider_fields_form(AppType::Claude); + + assert_eq!(provider_field_idx(&app), 0); + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(provider_field_idx(&app), 1); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(provider_field_idx(&app), 0); + } + + #[test] + fn provider_form_jk_inserts_chars_when_editing() { + let mut app = open_provider_fields_form(AppType::Claude); + + app.on_key(key(KeyCode::Enter), &data()); + let (editing, _) = provider_editing_name(&app); + assert!(editing); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Char('k')), &data()); + let (editing, value) = provider_editing_name(&app); + assert!(editing); + assert!( + value.contains('j') && value.contains('k'), + "j/k should be typed as characters in editing mode, got: {value}" + ); + } + + #[test] + fn provider_form_down_same_as_j() { + let mut app = open_provider_fields_form(AppType::Claude); + + app.on_key(key(KeyCode::Down), &data()); + let after_down = provider_field_idx(&app); + + app.on_key(key(KeyCode::Char('k')), &data()); + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(after_down, provider_field_idx(&app)); + } + + #[test] + fn provider_form_up_same_as_k() { + let mut app = open_provider_fields_form(AppType::Claude); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Up), &data()); + let after_up = provider_field_idx(&app); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(after_up, provider_field_idx(&app)); + } + + #[test] + fn mcp_form_jk_navigates_fields() { + let mut app = open_mcp_fields_form(); + + assert_eq!(mcp_field_idx(&app), 0); + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(mcp_field_idx(&app), 1); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(mcp_field_idx(&app), 0); + } + + #[test] + fn mcp_form_jk_inserts_chars_when_editing() { + let mut app = open_mcp_fields_form(); + + app.on_key(key(KeyCode::Enter), &data()); + let (editing, _) = mcp_editing_id(&app); + assert!(editing); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Char('k')), &data()); + let (editing, value) = mcp_editing_id(&app); + assert!(editing); + assert!( + value.contains('j') && value.contains('k'), + "j/k should be typed as characters in editing mode, got: {value}" + ); + } + + #[test] + fn mcp_form_down_same_as_j() { + let mut app = open_mcp_fields_form(); + + app.on_key(key(KeyCode::Down), &data()); + let after_down = mcp_field_idx(&app); + + app.on_key(key(KeyCode::Char('k')), &data()); + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(after_down, mcp_field_idx(&app)); + } + + #[test] + fn mcp_form_up_same_as_k() { + let mut app = open_mcp_fields_form(); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Up), &data()); + let after_up = mcp_field_idx(&app); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(after_up, mcp_field_idx(&app)); + } + + #[test] + fn prompt_form_jk_navigates_fields() { + let mut app = open_prompt_fields_form(); + + assert_eq!(prompt_field_idx(&app), 0); + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(prompt_field_idx(&app), 1); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(prompt_field_idx(&app), 0); + } + + #[test] + fn prompt_form_jk_inserts_chars_when_editing() { + let mut app = open_prompt_fields_form(); + + app.on_key(key(KeyCode::Enter), &data()); + let (editing, _) = prompt_editing_id(&app); + assert!(editing); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Char('k')), &data()); + let (editing, value) = prompt_editing_id(&app); + assert!(editing); + assert!( + value.contains('j') && value.contains('k'), + "j/k should be typed as characters in editing mode, got: {value}" + ); + } + + #[test] + fn prompt_form_down_same_as_j() { + let mut app = open_prompt_fields_form(); + + app.on_key(key(KeyCode::Down), &data()); + let after_down = prompt_field_idx(&app); + + app.on_key(key(KeyCode::Char('k')), &data()); + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(after_down, prompt_field_idx(&app)); + } + + #[test] + fn prompt_form_up_same_as_k() { + let mut app = open_prompt_fields_form(); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Up), &data()); + let after_up = prompt_field_idx(&app); + + app.on_key(key(KeyCode::Char('j')), &data()); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(after_up, prompt_field_idx(&app)); + } + + #[test] + fn provider_json_preview_jk_scrolls() { + let mut app = open_provider_fields_form(AppType::Claude); + focus_provider_json_preview(&mut app); + + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(provider_json_scroll(&app), 1); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(provider_json_scroll(&app), 0); + } + + #[test] + fn provider_codex_json_preview_jk_scrolls() { + let mut app = open_provider_fields_form(AppType::Codex); + focus_provider_json_preview(&mut app); + + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(provider_codex_auth_scroll(&app), 1); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(provider_codex_auth_scroll(&app), 0); + } + + #[test] + fn mcp_json_preview_jk_scrolls() { + let mut app = open_mcp_fields_form(); + focus_mcp_json_preview(&mut app); + + app.on_key(key(KeyCode::Char('j')), &data()); + assert_eq!(mcp_json_scroll(&app), 1); + app.on_key(key(KeyCode::Char('k')), &data()); + assert_eq!(mcp_json_scroll(&app), 0); + } + #[test] fn provider_form_usage_query_row_opens_secondary_page_and_esc_returns() { let mut app = App::new(Some(AppType::Claude)); @@ -11295,6 +11591,33 @@ mod tests { )); } + #[test] + fn provider_form_usage_query_jk_navigates_fields() { + let mut app = App::new(Some(AppType::Claude)); + app.focus = Focus::Content; + app.form = Some(FormState::ProviderAdd(ProviderAddFormState::new( + AppType::Claude, + ))); + let Some(FormState::ProviderAdd(form)) = app.form.as_mut() else { + panic!("expected ProviderAdd form"); + }; + form.open_usage_query_page(); + form.toggle_usage_query_enabled(); + form.focus = FormFocus::Fields; + + app.on_key(key(KeyCode::Char('j')), &UiData::default()); + assert!(matches!( + app.form, + Some(FormState::ProviderAdd(ref form)) if form.usage_query_field_idx == 1 + )); + + app.on_key(key(KeyCode::Char('k')), &UiData::default()); + assert!(matches!( + app.form, + Some(FormState::ProviderAdd(ref form)) if form.usage_query_field_idx == 0 + )); + } + #[test] fn provider_form_usage_query_side_panel_enter_opens_script_editor() { let mut app = App::new(Some(AppType::Claude));