use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crate::error::Result;
use crate::utils::{generate_worktree_preview, validate_branch_name};
use super::app::{App, AppState, ConfirmChoice};
pub fn is_cancel_key(key: &KeyEvent) -> bool {
matches!(
(key.modifiers, key.code),
(KeyModifiers::CONTROL, KeyCode::Char('c')) | (KeyModifiers::NONE, KeyCode::Esc)
)
}
pub fn poll_event(timeout: Duration) -> Result<Option<Event>> {
if event::poll(timeout)? {
Ok(Some(event::read()?))
} else {
Ok(None)
}
}
pub fn handle_key_event(app: &mut App, key: KeyEvent) {
match &app.state {
AppState::Loading { .. } => {
if is_cancel_key(&key) {
app.quit();
}
}
AppState::Success { .. } | AppState::Error { .. } => {
app.quit();
}
AppState::TextInput { .. } => {
handle_text_input_key(app, key);
}
AppState::SelectList { .. } => {
handle_select_list_key(app, key);
}
AppState::Confirm { .. } => {
handle_confirm_key(app, key);
}
AppState::Progress { .. } => {
if is_cancel_key(&key) {
app.quit();
}
}
}
}
fn handle_text_input_key(app: &mut App, key: KeyEvent) {
if is_cancel_key(&key) {
app.quit();
return;
}
if key.code == KeyCode::Enter {
return;
}
if key.code == KeyCode::Tab {
return;
}
if let AppState::TextInput { input, .. } = &mut app.state {
match (key.modifiers, key.code) {
(KeyModifiers::CONTROL, KeyCode::Char('u')) => {
input.clear();
}
(KeyModifiers::CONTROL, KeyCode::Char('w'))
| (KeyModifiers::ALT, KeyCode::Backspace) => {
input.delete_word_backward();
}
(_, KeyCode::Backspace) => {
input.delete_backward();
}
(_, KeyCode::Delete) => {
input.delete_forward();
}
(_, KeyCode::Left) | (KeyModifiers::CONTROL, KeyCode::Char('b')) => {
input.move_left();
}
(_, KeyCode::Right) | (KeyModifiers::CONTROL, KeyCode::Char('f')) => {
input.move_right();
}
(KeyModifiers::CONTROL, KeyCode::Char('a')) | (_, KeyCode::Home) => {
input.move_start();
}
(KeyModifiers::CONTROL, KeyCode::Char('e')) | (_, KeyCode::End) => {
input.move_end();
}
(KeyModifiers::NONE | KeyModifiers::SHIFT, KeyCode::Char(c)) => {
input.insert(c);
}
_ => {}
}
}
update_text_input_validation(app);
}
pub fn update_text_input_validation(app: &mut App) {
if let AppState::TextInput {
input,
validation_error,
preview,
..
} = &mut app.state
{
let value = input.value.trim();
*validation_error = validate_branch_name(value);
*preview = if value.is_empty() || validation_error.is_some() {
None
} else {
generate_worktree_preview(value, &app.config)
};
}
}
fn handle_select_list_key(app: &mut App, key: KeyEvent) {
if is_cancel_key(&key) {
app.quit();
return;
}
if key.code == KeyCode::Enter {
return;
}
let mut needs_preview_update = false;
if let AppState::SelectList { input, state, .. } = &mut app.state {
match (key.modifiers, key.code) {
(_, KeyCode::Up) | (KeyModifiers::CONTROL, KeyCode::Char('p')) => {
state.move_up();
needs_preview_update = true;
}
(_, KeyCode::Down) | (KeyModifiers::CONTROL, KeyCode::Char('n')) => {
state.move_down();
needs_preview_update = true;
}
(KeyModifiers::CONTROL, KeyCode::Char('u')) => {
input.clear();
state.update_filter(&input.value);
needs_preview_update = true;
}
(KeyModifiers::CONTROL, KeyCode::Char('w'))
| (KeyModifiers::ALT, KeyCode::Backspace) => {
input.delete_word_backward();
state.update_filter(&input.value);
needs_preview_update = true;
}
(_, KeyCode::Backspace) => {
input.delete_backward();
state.update_filter(&input.value);
needs_preview_update = true;
}
(_, KeyCode::Delete) => {
input.delete_forward();
state.update_filter(&input.value);
needs_preview_update = true;
}
(_, KeyCode::Left) | (KeyModifiers::CONTROL, KeyCode::Char('b')) => {
input.move_left();
}
(_, KeyCode::Right) | (KeyModifiers::CONTROL, KeyCode::Char('f')) => {
input.move_right();
}
(KeyModifiers::CONTROL, KeyCode::Char('a')) | (_, KeyCode::Home) => {
input.move_start();
}
(KeyModifiers::CONTROL, KeyCode::Char('e')) | (_, KeyCode::End) => {
input.move_end();
}
(KeyModifiers::NONE | KeyModifiers::SHIFT, KeyCode::Char(c)) => {
input.insert(c);
state.update_filter(&input.value);
needs_preview_update = true;
}
_ => {}
}
}
if needs_preview_update {
update_select_list_preview(app);
}
}
pub fn update_select_list_preview(app: &mut App) {
if let AppState::SelectList { state, preview, .. } = &mut app.state {
*preview = state
.selected_item()
.and_then(|item| generate_worktree_preview(&item.value, &app.config));
}
}
fn handle_confirm_key(app: &mut App, key: KeyEvent) {
if matches!(
(key.modifiers, key.code),
(KeyModifiers::CONTROL, KeyCode::Char('c'))
) {
app.quit();
return;
}
if let AppState::Confirm { selected, .. } = &mut app.state {
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('n') => {
*selected = selected.next();
}
KeyCode::Char('p') => {
*selected = selected.prev();
}
_ => {}
}
return;
}
match key.code {
KeyCode::Esc => {
*selected = ConfirmChoice::SkipHooks;
}
KeyCode::Enter => {
}
KeyCode::Left | KeyCode::Up => {
*selected = selected.prev();
}
KeyCode::Right | KeyCode::Down | KeyCode::Tab => {
*selected = selected.next();
}
KeyCode::Char('t') | KeyCode::Char('T') => {
*selected = ConfirmChoice::Trust;
}
KeyCode::Char('o') | KeyCode::Char('O') => {
*selected = ConfirmChoice::Once;
}
KeyCode::Char('c') | KeyCode::Char('C') => {
*selected = ConfirmChoice::SkipHooks;
}
_ => {}
}
}
}
pub fn get_selected_item(app: &App) -> Option<&super::app::SelectItem> {
if let AppState::SelectList { state, .. } = &app.state {
return state.selected_item();
}
None
}
pub fn get_input_value(app: &App) -> Option<String> {
if let AppState::TextInput { input, .. } = &app.state {
let value = input.value.trim().to_string();
if !value.is_empty() {
return Some(value);
}
}
None
}
pub fn get_validation_error(app: &App) -> Option<&str> {
if let AppState::TextInput {
validation_error, ..
} = &app.state
{
return validation_error.as_deref();
}
None
}
pub fn get_confirm_choice(app: &App) -> Option<ConfirmChoice> {
if let AppState::Confirm { selected, .. } = &app.state {
return Some(*selected);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::ui::app::SelectItem;
fn create_test_app() -> App {
App::new(Config::default())
}
#[test]
fn test_handle_text_input_escape() {
let mut app = create_test_app();
app.set_text_input("Test", "Enter text...");
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
handle_key_event(&mut app, key);
assert!(app.should_quit);
}
#[test]
fn test_handle_text_input_character() {
let mut app = create_test_app();
app.set_text_input("Test", "Enter text...");
let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
handle_key_event(&mut app, key);
if let AppState::TextInput { input, .. } = &app.state {
assert_eq!(input.value, "a");
} else {
panic!("Expected TextInput state");
}
}
#[test]
fn test_handle_select_list_navigation() {
let mut app = create_test_app();
let items = vec![
SelectItem {
label: "Item 1".to_string(),
value: "item1".to_string(),
description: None,
metadata: None,
},
SelectItem {
label: "Item 2".to_string(),
value: "item2".to_string(),
description: None,
metadata: None,
},
];
app.set_select_list("Test", "Search...", items);
let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
handle_key_event(&mut app, key);
if let AppState::SelectList { state, .. } = &app.state {
assert_eq!(state.cursor_index, 1);
} else {
panic!("Expected SelectList state");
}
}
}