diff --git a/Cargo.lock b/Cargo.lock index 228669d..e6535aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,7 +1123,7 @@ dependencies = [ [[package]] name = "ostiary" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 7b41531..dae2d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ostiary" -version = "1.0.1" +version = "1.0.2" edition = "2024" [dependencies] diff --git a/src/app.rs b/src/app.rs index 7614bbd..316c531 100644 --- a/src/app.rs +++ b/src/app.rs @@ -297,6 +297,11 @@ impl App { Some(executor::StructuredCommand::Confirm { .. }) ); let has_command = current_command.is_some(); + // Vim motions are disabled only when free text input is active. + let is_text_input = matches!( + current_command, + Some(executor::StructuredCommand::Input { .. }) + ); match key.code { // ── Log scrolling (PageUp / PageDown always work) ─ @@ -394,7 +399,66 @@ impl App { let _ = current_command.take(); let _ = reply_tx.send("n".to_string()); } else { - // Kill the script if still running. + if let Some(kx) = kill_tx.take() { + let _ = kx.send(()); + } + self.popup = None; + } + } + + // ── Vim motions (off during text input) ─────────── + KeyCode::Char('k') if !is_text_input => { + if is_menu { + if *menu_selected_index > 0 { + *menu_selected_index -= 1; + } + } else if !has_command { + *log_scroll_pos = log_scroll_pos.saturating_sub(1); + *log_follow_bottom = false; + } + } + KeyCode::Char('j') if !is_text_input => { + if is_menu { + if let Some(executor::StructuredCommand::Menu { + options, .. + }) = current_command + { + if *menu_selected_index + 1 < options.len() { + *menu_selected_index += 1; + } + } + } else if !has_command { + *log_scroll_pos = log_scroll_pos.saturating_add(1); + *log_follow_bottom = + *log_scroll_pos + 1 >= log_buffer.len(); + } + } + KeyCode::Char('l') if !is_text_input => { + // Forward / confirm — same logic as Enter. + if let Some(cmd) = current_command.take() { + let response = match &cmd { + executor::StructuredCommand::Confirm { .. } => { + "y".to_string() + } + executor::StructuredCommand::Menu { + options, .. + } => options + .get(*menu_selected_index) + .map(|o| o.id.clone()) + .unwrap_or_default(), + _ => String::new(), + }; + let _ = reply_tx.send(response); + input_buffer.clear(); + *menu_selected_index = 0; + } + } + KeyCode::Char('h') if !is_text_input => { + // Back / cancel — same logic as Esc. + if is_confirm { + let _ = current_command.take(); + let _ = reply_tx.send("n".to_string()); + } else { if let Some(kx) = kill_tx.take() { let _ = kx.send(()); } @@ -414,7 +478,7 @@ impl App { Popup::UpdateConfirm { info } => { match key.code { - KeyCode::Char('y') | KeyCode::Char('Y') => { + KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Char('l') => { let info = info.clone(); self.popup = Some(Popup::Updating { info: info.clone(), @@ -453,7 +517,8 @@ impl App { } }); } - KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { + KeyCode::Char('n') | KeyCode::Char('N') + | KeyCode::Esc | KeyCode::Char('h') => { self.popup = None; } _ => {} @@ -478,24 +543,24 @@ impl App { // ── Main menu navigation ───────────────────────────────────────── match key.code { KeyCode::Char('q') => return Ok(true), - KeyCode::Up => { + KeyCode::Up | KeyCode::Char('k') => { let len = self.current_items().len(); if len > 0 { self.selected_index = (self.selected_index + len - 1) % len; } } - KeyCode::Down => { + KeyCode::Down | KeyCode::Char('j') => { let len = self.current_items().len(); if len > 0 { self.selected_index = (self.selected_index + 1) % len; } } - KeyCode::Enter => { + KeyCode::Enter | KeyCode::Char('l') => { if let Some(item) = self.selected_item().cloned() { self.activate_item(item).await?; } } - KeyCode::Esc => { + KeyCode::Esc | KeyCode::Char('h') => { if !self.breadcrumbs.is_empty() { self.breadcrumbs.pop(); self.selected_index = 0;