use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::state::{App, Focus, Mode, ThemeName};
pub fn handle(app: &mut App, key: KeyEvent) {
if app.show_help {
match key.code {
KeyCode::Esc | KeyCode::Char('?') | KeyCode::Char('q') => {
app.show_help = false;
}
_ => {}
}
return;
}
if app.show_comments_list {
handle_comments_list(app, key);
return;
}
if app.preview_mode {
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('r') => {
app.preview_mode = false;
}
_ => {}
}
return;
}
if app.show_revision_selector {
handle_revision_selector(app, key);
return;
}
match app.mode {
Mode::Normal => handle_normal(app, key),
Mode::Insert => handle_insert(app, key),
Mode::Search => handle_search(app, key),
Mode::FileFilter => handle_file_filter(app, key),
Mode::Visual => handle_visual(app, key),
}
}
fn handle_revision_selector(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Down, _) | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
app.revision_selector_next();
return;
}
(KeyCode::Up, _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
app.revision_selector_prev();
return;
}
_ => {}
}
handle_line_input(
app,
key,
App::close_revision_selector,
App::confirm_revision_selector,
Some(App::on_revision_filter_change),
);
}
fn handle_comments_list(app: &mut App, key: KeyEvent) {
if app.pending_d {
app.pending_d = false;
if matches!(key.code, KeyCode::Char('d')) {
app.delete_selected_comment_in_list();
return;
}
}
match (key.code, key.modifiers) {
(KeyCode::Esc, _)
| (KeyCode::Char('q'), _)
| (KeyCode::Char('C'), _)
| (KeyCode::Char('c'), KeyModifiers::CONTROL) => app.close_comments_list(),
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => app.comments_list_next(),
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => app.comments_list_prev(),
(KeyCode::Enter, _) => app.jump_to_selected_comment(),
(KeyCode::Char('d'), KeyModifiers::NONE) => app.pending_d = true,
(KeyCode::Char('y'), KeyModifiers::NONE) => copy_selected_comment_in_list(app),
(KeyCode::Char('Y'), _) => copy_all_comments(app),
_ => {}
}
}
fn copy_selected_comment_in_list(app: &mut App) {
let Some(c) = app.selected_comment_in_list().cloned() else {
return;
};
let text = app.format_comment_full(&c);
if let Ok(mut clipboard) = arboard::Clipboard::new() {
let _ = clipboard.set_text(&text);
app.set_flash("Copied comment".to_string());
}
}
fn handle_line_input(
app: &mut App,
key: KeyEvent,
on_cancel: fn(&mut App),
on_submit: fn(&mut App),
on_change: Option<fn(&mut App)>,
) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => on_cancel(app),
(KeyCode::Enter, _) => on_submit(app),
(KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
app.input_delete_backward();
if let Some(f) = on_change {
f(app);
}
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
app.input_clear();
if let Some(f) = on_change {
f(app);
}
}
(KeyCode::Char('b'), KeyModifiers::CONTROL) | (KeyCode::Left, KeyModifiers::NONE) => {
app.input_move_left();
}
(KeyCode::Char('f'), KeyModifiers::CONTROL) | (KeyCode::Right, KeyModifiers::NONE) => {
app.input_move_right();
}
(KeyCode::Char('a'), KeyModifiers::CONTROL) => app.input_move_line_start(),
(KeyCode::Char('e'), KeyModifiers::CONTROL) => app.input_move_line_end(),
(KeyCode::Char(c), m)
if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) =>
{
app.input_insert_char(c);
if let Some(f) = on_change {
f(app);
}
}
_ => {}
}
}
fn handle_search(app: &mut App, key: KeyEvent) {
handle_line_input(app, key, App::cancel_search, App::submit_search, None);
}
fn handle_file_filter(app: &mut App, key: KeyEvent) {
handle_line_input(
app,
key,
App::cancel_file_filter,
App::submit_file_filter,
Some(App::update_file_filter_live),
);
}
fn handle_visual(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _)
| (KeyCode::Char('c'), KeyModifiers::CONTROL)
| (KeyCode::Char('v'), KeyModifiers::NONE) => {
app.cancel_visual();
}
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
app.move_cursor_down(1);
}
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => {
app.move_cursor_up(1);
}
(KeyCode::Char('i'), KeyModifiers::NONE) => {
app.submit_visual_comment();
}
_ => {}
}
}
fn handle_insert(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => app.cancel_insert(),
(KeyCode::Enter, m)
if m.contains(KeyModifiers::SHIFT) || m.contains(KeyModifiers::CONTROL) =>
{
app.input_insert_newline();
}
(KeyCode::Enter, _) => app.submit_comment(),
(KeyCode::Char('s'), KeyModifiers::CONTROL) => app.submit_comment(),
(KeyCode::Char('j'), KeyModifiers::CONTROL) => app.input_insert_newline(),
(KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
app.input_delete_backward();
}
(KeyCode::Char('w'), KeyModifiers::CONTROL) => app.input_delete_word_backward(),
(KeyCode::Char('k'), KeyModifiers::CONTROL) => app.input_kill_to_end_of_line(),
(KeyCode::Char('u'), KeyModifiers::CONTROL) => app.input_clear(),
(KeyCode::Char('b'), KeyModifiers::CONTROL) | (KeyCode::Left, KeyModifiers::NONE) => {
app.input_move_left();
}
(KeyCode::Char('f'), KeyModifiers::CONTROL) | (KeyCode::Right, KeyModifiers::NONE) => {
app.input_move_right();
}
(KeyCode::Char('p'), KeyModifiers::CONTROL) | (KeyCode::Up, _) => {
app.input_move_up();
}
(KeyCode::Char('n'), KeyModifiers::CONTROL) | (KeyCode::Down, _) => {
app.input_move_down();
}
(KeyCode::Char('a'), KeyModifiers::CONTROL) => app.input_move_line_start(),
(KeyCode::Char('e'), KeyModifiers::CONTROL) => app.input_move_line_end(),
(KeyCode::Char('b'), KeyModifiers::ALT) | (KeyCode::Left, KeyModifiers::ALT) => {
app.input_move_word_left();
}
(KeyCode::Char('f'), KeyModifiers::ALT) | (KeyCode::Right, KeyModifiers::ALT) => {
app.input_move_word_right();
}
(KeyCode::Char(c), m)
if !m.contains(KeyModifiers::CONTROL) && !m.contains(KeyModifiers::ALT) =>
{
app.input_insert_char(c);
}
_ => {}
}
}
fn copy_current_comment(app: &mut App) {
let Some(file) = app.current() else { return };
let comments = app.comments_on(file, app.cursor_line);
let picked = app
.comment_focus
.and_then(|cf| comments.get(cf))
.or(comments.first());
let Some(c) = picked.cloned() else { return };
let text = app.format_comment_full(c);
if let Ok(mut clipboard) = arboard::Clipboard::new() {
let _ = clipboard.set_text(&text);
app.set_flash("Copied comment".to_string());
}
}
fn copy_all_comments(app: &mut App) {
if app.comments.is_empty() {
return;
}
let text = app.format_all_comments();
if let Ok(mut clipboard) = arboard::Clipboard::new() {
let _ = clipboard.set_text(&text);
app.set_flash(format!("Copied {} comments", app.comments.len()));
}
}
fn handle_normal(app: &mut App, key: KeyEvent) {
let half = (app.viewport_height / 2).max(1);
let full = app.viewport_height.max(1);
if app.pending_g {
app.pending_g = false;
if matches!(key.code, KeyCode::Char('g')) {
app.cursor_top();
return;
}
}
if app.pending_d {
app.pending_d = false;
if matches!(key.code, KeyCode::Char('d')) {
if app.focus == Focus::Diff {
app.delete_focused_comment();
}
return;
}
}
match (key.code, key.modifiers) {
(KeyCode::Char('q'), _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
app.should_quit = true;
}
(KeyCode::Char('i'), KeyModifiers::NONE) | (KeyCode::Char('c'), KeyModifiers::NONE)
if app.focus == Focus::Diff =>
{
app.enter_insert();
}
(KeyCode::Char('/'), KeyModifiers::NONE) => {
if app.focus == Focus::Diff {
app.enter_search();
} else if app.focus == Focus::Files {
app.enter_file_filter();
}
}
(KeyCode::Char('n'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
if app.search_query.is_some() {
app.next_search_match();
} else {
app.next_hunk();
}
}
(KeyCode::Char('p'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
if app.search_query.is_some() {
app.prev_search_match();
} else {
app.prev_hunk();
}
}
(KeyCode::Char('N'), _) if app.focus == Focus::Diff => {
app.next_comment();
}
(KeyCode::Char('P'), _) if app.focus == Focus::Diff => {
app.prev_comment();
}
(KeyCode::Esc, _) => {
if app.search_query.is_some() {
app.clear_search();
}
if app.file_filter.is_some() {
app.file_filter = None;
}
}
(KeyCode::Char('V'), _) if app.focus == Focus::Diff => {
app.enter_visual();
}
(KeyCode::Char('<'), _) => {
app.file_list_width = app.file_list_width.saturating_sub(2).max(20);
}
(KeyCode::Char('>'), _) => {
app.file_list_width = (app.file_list_width + 2).min(60);
}
(KeyCode::Char('T'), _) => {
app.theme_name = app.theme_name.next();
}
(KeyCode::Char('1'), KeyModifiers::NONE) => {
app.theme_name = ThemeName::Classic;
}
(KeyCode::Char('2'), KeyModifiers::NONE) => {
app.theme_name = ThemeName::Washi;
}
(KeyCode::Char('3'), KeyModifiers::NONE) => {
app.theme_name = ThemeName::Sumi;
}
(KeyCode::Char('y'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
copy_current_comment(app);
}
(KeyCode::Char('Y'), _) => {
copy_all_comments(app);
}
(KeyCode::Char('e'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
app.pending_open_editor = true;
}
(KeyCode::Char('R'), _) => {
app.pending_reload = true;
}
(KeyCode::Tab, _) => app.select_next_file(),
(KeyCode::BackTab, _) => app.select_prev_file(),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => match app.focus {
Focus::Diff => app.move_cursor_down(1),
Focus::Files => app.file_tree_cursor_down(),
},
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => match app.focus {
Focus::Diff => app.move_cursor_up(1),
Focus::Files => app.file_tree_cursor_up(),
},
(KeyCode::Char('d' | 'D'), KeyModifiers::CONTROL) => app.move_cursor_down(half),
(KeyCode::Char('u' | 'U'), KeyModifiers::CONTROL) => app.move_cursor_up(half),
(KeyCode::Char('f' | 'F'), KeyModifiers::CONTROL) => app.move_cursor_down(full),
(KeyCode::Char('b' | 'B'), KeyModifiers::CONTROL) => app.move_cursor_up(full),
(KeyCode::Char('g'), KeyModifiers::NONE) => {
app.pending_g = true;
}
(KeyCode::Char('G'), _) => app.cursor_bottom(),
(KeyCode::Char('o'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
if let Some(line) = app.current().and_then(|f| f.lines.get(app.cursor_line)) {
if line.kind.is_fold() {
app.pending_expand = Some(20);
}
}
}
(KeyCode::Char('O'), _) if app.focus == Focus::Diff => {
if let Some(line) = app.current().and_then(|f| f.lines.get(app.cursor_line)) {
if line.kind.is_fold() {
app.pending_expand = Some(usize::MAX);
}
}
}
(KeyCode::Enter, _) => match app.focus {
Focus::Diff => {
if let Some(line) = app.current().and_then(|f| f.lines.get(app.cursor_line)) {
if line.kind.is_fold() {
app.pending_expand = Some(20);
}
}
}
Focus::Files => {
app.toggle_file_tree_dir_at_cursor();
}
},
(KeyCode::Char('s'), KeyModifiers::NONE) => {
app.split_view = !app.split_view;
}
(KeyCode::Char('r'), KeyModifiers::NONE) => {
app.preview_mode = !app.preview_mode;
}
(KeyCode::Char('C'), _) => app.toggle_comments_list(),
(KeyCode::Char('F'), _) => {
app.show_file_list = !app.show_file_list;
if !app.show_file_list && app.focus == Focus::Files {
app.focus = Focus::Diff;
}
}
(KeyCode::Char('h'), _) => {
app.focus = Focus::Files;
}
(KeyCode::Char('l'), _) => {
app.focus = Focus::Diff;
}
(KeyCode::Char(']'), KeyModifiers::NONE) => app.select_next_file(),
(KeyCode::Char('['), KeyModifiers::NONE) => app.select_prev_file(),
(KeyCode::Char('}'), _) => app.select_last_file(),
(KeyCode::Char('{'), _) => app.select_first_file(),
(KeyCode::Char('.'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
app.center_cursor();
}
(KeyCode::Char('v'), KeyModifiers::NONE) => app.toggle_viewed(),
(KeyCode::Char('d'), KeyModifiers::NONE) if app.focus == Focus::Diff => {
app.pending_d = true;
}
(KeyCode::Char('?'), _) => {
app.show_help = true;
}
(KeyCode::Char(':'), _) => {
app.open_revision_selector();
}
(KeyCode::Char(' '), KeyModifiers::NONE) if app.focus == Focus::Files => {
app.toggle_file_tree_dir_at_cursor();
}
_ => {}
}
}