use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::api::types::Priority;
use crate::app::{App, FormField, Input, InputMode, Nav, Popup, Screen, TeamSection};
use crate::grouping::Preset;
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 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 => app.apply_popup(),
KeyCode::Char(c @ '1'..='9') => {
let idx = (c as usize) - ('1' as usize);
if idx < app.popup_list_len() {
app.popup_index = idx;
app.apply_popup();
}
}
_ => {}
}
}
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') if !ctrl => app.copy_identifier(),
KeyCode::Char(',' | '<') if ctrl && shift => app.copy_url(),
KeyCode::Char('Y') if !ctrl => app.copy_url(),
KeyCode::Char('.' | '>') if ctrl && shift => app.copy_branch_name(),
KeyCode::Char('b') if !ctrl => app.copy_branch_name(),
KeyCode::Char('m') if !ctrl => 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('a') => {
app.go_to_team_issues();
app.set_preset(Preset::Active);
}
KeyCode::Char('b') => {
app.go_to_team_issues();
app.set_preset(Preset::Backlog);
}
KeyCode::Char('e') => {
app.go_to_team_issues();
app.set_preset(Preset::All);
}
KeyCode::Char('m') => app.activate(Nav::MyIssues),
KeyCode::Char('v') => app.activate(Nav::Views),
KeyCode::Char('p') => app.go_to_team_section(TeamSection::Projects),
KeyCode::Char('c') => app.go_to_team_section(TeamSection::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;
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctrl && key.code == KeyCode::Char('b') {
return app.toggle_sidebar();
}
if app.sidebar_focus {
return handle_sidebar_keys(app, key);
}
if matches!(key.code, KeyCode::Tab) {
return app.focus_sidebar(true);
}
if matches!(
app.screen,
Screen::IssueList | Screen::ProjectList | Screen::CycleList | Screen::ViewList
) {
match key.code {
KeyCode::Char('1') => return app.go_to_team_issues(),
KeyCode::Char('2') => return app.activate(Nav::MyIssues),
KeyCode::Char('3') => return app.go_to_team_section(TeamSection::Projects),
KeyCode::Char('4') => return app.go_to_team_section(TeamSection::Cycles),
KeyCode::Char('5') => return app.activate(Nav::Views),
_ => {}
}
}
if app.screen == Screen::IssueList
|| matches!(app.screen, Screen::ProjectDetail | Screen::CycleDetail)
{
match key.code {
KeyCode::Char('D') => return app.cycle_group_by(),
KeyCode::Char('z') => return app.toggle_selected_group(),
KeyCode::Char('Z') => return app.toggle_all_groups(),
KeyCode::BackTab => return app.cycle_preset(),
_ => {}
}
}
if matches!(
app.screen,
Screen::IssueList | Screen::IssueDetail | Screen::ProjectDetail | Screen::CycleDetail
) {
if app.screen == Screen::IssueDetail {
match key.code {
KeyCode::Char('J') => return app.step_issue(1),
KeyCode::Char('K') => return app.step_issue(-1),
_ => {}
}
}
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 => handle_issue_list_keys(app, key),
Screen::ViewList => handle_view_list_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 handle_sidebar_keys(app: &mut App, key: KeyEvent) {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('j') | KeyCode::Down => app.sidebar_move(1),
KeyCode::Char('k') | KeyCode::Up => app.sidebar_move(-1),
KeyCode::Char('d') if ctrl => app.sidebar_move(app.half_page()),
KeyCode::Char('u') if ctrl => app.sidebar_move(-app.half_page()),
KeyCode::Char('g') | KeyCode::Home => app.sidebar_move(isize::MIN / 2),
KeyCode::Char('G') | KeyCode::End => app.sidebar_move(isize::MAX / 2),
KeyCode::Char('h') | KeyCode::Left | KeyCode::Char('l') | KeyCode::Right => {
app.sidebar_toggle()
}
KeyCode::Enter | KeyCode::Char(' ') => app.sidebar_activate(),
KeyCode::Tab | KeyCode::Esc => app.focus_sidebar(false),
KeyCode::Char('t') => app.open_team_select(),
KeyCode::Char('q') => app.should_quit = true,
_ => {}
}
}
fn handle_view_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_selected_view(),
_ => {}
}
}
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_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') => app.close_detail(),
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.leave_container(),
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.leave_container(),
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),
_ => {}
},
}
}