use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::api::types::Priority;
use crate::app::{App, FormField, Input, InputMode, Popup, Screen, Tab};
pub fn handle_key(app: &mut App, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctrl && key.code == KeyCode::Char('c') {
app.should_quit = true;
return;
}
if app.error_popup.is_some() {
app.dismiss_error();
return;
}
if app.show_help {
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
app.help_scroll = app.help_scroll.saturating_add(1)
}
KeyCode::Char('k') | KeyCode::Up => app.help_scroll = app.help_scroll.saturating_sub(1),
_ => app.show_help = false,
}
return;
}
if key.code == KeyCode::Esc && app.pending_chord.take().is_some() {
return;
}
if app.popup != Popup::None {
handle_popup_keys(app, key);
return;
}
match app.input_mode {
InputMode::Normal => handle_normal_mode(app, key),
InputMode::Search => handle_search_mode(app, key),
InputMode::Comment => handle_comment_mode(app, key),
InputMode::NewIssue => handle_new_issue_mode(app, key),
}
}
fn apply_popup_selection(app: &mut App) {
match app.popup {
Popup::TeamSelect => app.select_team(),
Popup::Filter => app.apply_filter_selection(),
Popup::StatusChange => app.apply_status_selection(),
Popup::PriorityChange => app.apply_priority_selection(),
Popup::AssigneeChange => app.apply_assignee_selection(),
Popup::None => {}
}
}
fn handle_popup_keys(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.close_popup(),
KeyCode::Char('j') | KeyCode::Down => app.popup_next(),
KeyCode::Char('k') | KeyCode::Up => app.popup_prev(),
KeyCode::Char('g') | KeyCode::Home => app.popup_index = 0,
KeyCode::Char('G') | KeyCode::End => {
app.popup_index = app.popup_list_len().saturating_sub(1);
}
KeyCode::Enter => apply_popup_selection(app),
KeyCode::Char(c @ '1'..='9') => {
let idx = (c as usize) - ('1' as usize);
if idx < app.popup_list_len() {
app.popup_index = idx;
apply_popup_selection(app);
}
}
_ => {}
}
}
fn handle_global_actions(app: &mut App, key: KeyEvent) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
match key.code {
KeyCode::Char('.') if ctrl && !shift => app.copy_identifier(),
KeyCode::Char('y') => app.copy_identifier(),
KeyCode::Char(',' | '<') if ctrl && shift => app.copy_url(),
KeyCode::Char('Y') => app.copy_url(),
KeyCode::Char('.' | '>') if ctrl && shift => app.copy_branch_name(),
KeyCode::Char('b') => app.copy_branch_name(),
KeyCode::Char('m') => app.start_comment(),
KeyCode::Char('!') => app.set_priority(Priority::Urgent),
KeyCode::Char('@') => app.set_priority(Priority::High),
KeyCode::Char('#') => app.set_priority(Priority::Medium),
KeyCode::Char('$') => app.set_priority(Priority::Low),
KeyCode::Char(')') => app.set_priority(Priority::None),
_ => return false,
}
true
}
fn handle_goto_chord(app: &mut App, key: KeyEvent) {
app.pending_chord = None;
match key.code {
KeyCode::Char('g') => {
if app.screen == Screen::IssueDetail {
app.scroll_to_top();
} else {
app.select_first();
}
}
KeyCode::Char('m') => app.switch_tab(Tab::MyIssues),
KeyCode::Char('e') => app.switch_tab(Tab::Issues),
KeyCode::Char('p') => app.switch_tab(Tab::Projects),
KeyCode::Char('c') => app.switch_tab(Tab::Cycles),
_ => {}
}
}
fn handle_normal_mode(app: &mut App, key: KeyEvent) {
if app.pending_chord == Some('g') {
handle_goto_chord(app, key);
return;
}
if key.code == KeyCode::Char('?') {
app.show_help = true;
app.help_scroll = 0;
return;
}
if matches!(
app.screen,
Screen::IssueList | Screen::ProjectList | Screen::CycleList
) {
match key.code {
KeyCode::Char('1') => return app.switch_tab(Tab::Issues),
KeyCode::Char('2') => return app.switch_tab(Tab::MyIssues),
KeyCode::Char('3') => return app.switch_tab(Tab::Projects),
KeyCode::Char('4') => return app.switch_tab(Tab::Cycles),
_ => {}
}
}
if matches!(
app.screen,
Screen::IssueList | Screen::IssueDetail | Screen::ProjectDetail | Screen::CycleDetail
) {
match key.code {
KeyCode::Char('s') => return app.open_status_change(),
KeyCode::Char('p') => return app.open_priority_change(),
KeyCode::Char('a') => return app.open_assignee_change(),
KeyCode::Char('i') => return app.assign_to_me(),
_ => {}
}
if handle_global_actions(app, key) {
return;
}
}
if key.code == KeyCode::Char('c') {
return app.start_new_issue();
}
if key.code == KeyCode::Char('o') {
return app.open_in_browser();
}
let refresh = matches!(key.code, KeyCode::F(5))
|| (key.code == KeyCode::Char('r') && key.modifiers.contains(KeyModifiers::CONTROL));
if refresh {
if app.screen == Screen::IssueDetail {
app.refresh_detail();
} else {
app.force_reload();
}
return;
}
match app.screen {
Screen::IssueList => match app.tab {
Tab::Issues => handle_issue_list_keys(app, key),
Tab::MyIssues => handle_my_issues_keys(app, key),
_ => {}
},
Screen::IssueDetail => handle_issue_detail_keys(app, key),
Screen::ProjectList => handle_project_list_keys(app, key),
Screen::ProjectDetail => handle_project_detail_keys(app, key),
Screen::CycleList => handle_cycle_list_keys(app, key),
Screen::CycleDetail => handle_cycle_detail_keys(app, key),
}
}
fn list_delta(app: &App, key: KeyEvent) -> Option<isize> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('j') | KeyCode::Down => Some(1),
KeyCode::Char('k') | KeyCode::Up => Some(-1),
KeyCode::Char('d') if ctrl => Some(app.half_page()),
KeyCode::Char('u') if ctrl => Some(-app.half_page()),
KeyCode::PageDown => Some(app.list_viewport.max(1) as isize),
KeyCode::PageUp => Some(-(app.list_viewport.max(1) as isize)),
_ => None,
}
}
fn handle_list_nav(app: &mut App, key: KeyEvent) -> bool {
if let Some(delta) = list_delta(app, key) {
app.move_selection(delta);
return true;
}
match key.code {
KeyCode::Char('g') => app.pending_chord = Some('g'),
KeyCode::Home => app.select_first(),
KeyCode::Char('G') | KeyCode::End => app.select_last(),
_ => return false,
}
true
}
fn handle_issue_list_keys(app: &mut App, key: KeyEvent) {
if handle_list_nav(app, key) {
return;
}
match key.code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Enter | KeyCode::Char(' ') => app.open_issue_detail(),
KeyCode::Char('t') => app.open_team_select(),
KeyCode::Char('f') => app.open_filter(),
KeyCode::Char('F') => app.clear_filters(),
KeyCode::Char('/') => {
app.input_mode = InputMode::Search;
app.search.clear();
}
KeyCode::Esc => app.clear_search(),
_ => {}
}
}
fn handle_my_issues_keys(app: &mut App, key: KeyEvent) {
if handle_list_nav(app, key) {
return;
}
match key.code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Enter | KeyCode::Char(' ') => {
if let Some(issue) = app
.visible_my_issues()
.get(app.selected_my_issue_index)
.map(|i| (*i).clone())
{
app.open_issue_from_list(&issue);
}
}
KeyCode::Char('/') => {
app.input_mode = InputMode::Search;
app.search.clear();
}
KeyCode::Esc => app.clear_search(),
KeyCode::Char('t') => app.open_team_select(),
_ => {}
}
}
fn handle_issue_detail_keys(app: &mut App, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let page = app.detail_viewport as i16;
match key.code {
KeyCode::Esc | KeyCode::Char('q') => match app.tab {
Tab::Issues | Tab::MyIssues => app.screen = Screen::IssueList,
Tab::Projects => app.screen = Screen::ProjectDetail,
Tab::Cycles => app.screen = Screen::CycleDetail,
},
KeyCode::Char('j') | KeyCode::Down => app.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => app.scroll_up(),
KeyCode::Char('d') if ctrl => app.scroll_by(page / 2),
KeyCode::Char('u') if ctrl => app.scroll_by(-page / 2),
KeyCode::PageDown => app.scroll_by(page),
KeyCode::PageUp => app.scroll_by(-page),
KeyCode::Char('g') => app.pending_chord = Some('g'),
KeyCode::Home => app.scroll_to_top(),
KeyCode::Char('G') | KeyCode::End => app.scroll_to_bottom(),
_ => {}
}
}
fn handle_project_list_keys(app: &mut App, key: KeyEvent) {
if handle_list_nav(app, key) {
return;
}
match key.code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Enter | KeyCode::Char(' ') => app.open_project_detail(),
KeyCode::Char('t') => app.open_team_select(),
_ => {}
}
}
fn handle_project_detail_keys(app: &mut App, key: KeyEvent) {
if handle_list_nav(app, key) {
return;
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.screen = Screen::ProjectList,
KeyCode::Enter | KeyCode::Char(' ') => {
if let Some(issue) = app
.project_issues
.get(app.selected_project_issue_index)
.cloned()
{
app.open_issue_from_list(&issue);
}
}
_ => {}
}
}
fn handle_cycle_list_keys(app: &mut App, key: KeyEvent) {
if handle_list_nav(app, key) {
return;
}
match key.code {
KeyCode::Char('q') => app.should_quit = true,
KeyCode::Enter | KeyCode::Char(' ') => app.open_cycle_detail(),
KeyCode::Char('t') => app.open_team_select(),
_ => {}
}
}
fn handle_cycle_detail_keys(app: &mut App, key: KeyEvent) {
if handle_list_nav(app, key) {
return;
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.screen = Screen::CycleList,
KeyCode::Enter | KeyCode::Char(' ') => {
if let Some(issue) = app
.cycle_issues
.get(app.selected_cycle_issue_index)
.cloned()
{
app.open_issue_from_list(&issue);
}
}
_ => {}
}
}
fn edit_input(input: &mut Input, key: KeyEvent) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('w') if ctrl => input.kill_word(),
KeyCode::Char('u') if ctrl => input.kill_to_start(),
KeyCode::Char('k') if ctrl => input.kill_to_end(),
KeyCode::Char('a') if ctrl => input.home(),
KeyCode::Char('e') if ctrl => input.end(),
KeyCode::Left => input.left(),
KeyCode::Right => input.right(),
KeyCode::Home => input.home(),
KeyCode::End => input.end(),
KeyCode::Backspace => input.backspace(),
KeyCode::Delete => input.delete(),
KeyCode::Char(c) if !ctrl => input.insert(c),
_ => return false,
}
true
}
fn handle_search_mode(app: &mut App, key: KeyEvent) {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('g') {
app.search_workspace();
return;
}
match key.code {
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
app.clear_search();
return;
}
KeyCode::Enter => {
app.input_mode = InputMode::Normal;
app.apply_search();
return;
}
_ => {}
}
if edit_input(&mut app.search, key) {
app.apply_search();
}
}
fn handle_comment_mode(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
app.comment.clear();
return;
}
KeyCode::Enter => {
if key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
{
app.submit_comment();
} else {
app.comment.insert('\n');
}
return;
}
_ => {}
}
edit_input(&mut app.comment, key);
}
fn handle_new_issue_mode(app: &mut App, key: KeyEvent) {
let ctrl_or_alt = key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT);
match key.code {
KeyCode::Esc => return app.cancel_new_issue(),
KeyCode::Tab => return app.new_issue_cycle_field(true),
KeyCode::BackTab => return app.new_issue_cycle_field(false),
KeyCode::Enter if ctrl_or_alt => return app.submit_new_issue(),
_ => {}
}
let Some(form) = &mut app.new_issue else {
return;
};
match form.field {
FormField::Title => {
if key.code == KeyCode::Enter {
app.new_issue_cycle_field(true);
} else {
edit_input(&mut form.title, key);
}
}
FormField::Description => {
if key.code == KeyCode::Enter {
form.description.insert('\n');
} else {
edit_input(&mut form.description, key);
}
}
FormField::Priority => match key.code {
KeyCode::Char('j') | KeyCode::Down | KeyCode::Right => app.new_issue_cycle_priority(1),
KeyCode::Char('k') | KeyCode::Up | KeyCode::Left => app.new_issue_cycle_priority(-1),
_ => {}
},
}
}