use super::{App, Level, Pane, branches, commits, repos, tags};
use crate::tui::input::{
is_back, is_ctrl_c, is_down, is_help, is_left, is_open, is_right, is_up, norm_esc,
};
use crate::tui::widgets::{Command, CommandLine, FormKey, Typed};
use ratatui::DefaultTerminal;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
pub(super) fn on_key(app: &mut App, key: KeyEvent, terminal: &mut DefaultTerminal) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let mut code = norm_esc(key.code, ctrl);
if is_ctrl_c(code, ctrl) {
let open = app.modal.is_some()
|| app.form.is_some()
|| app.confirm.is_some()
|| app.command.is_some()
|| app.editing.is_some();
if !open {
return true;
}
code = KeyCode::Esc;
}
if let Some(modal) = &mut app.modal {
if modal.on_key(code, ctrl) {
app.modal = None;
}
return false;
}
if let Some(f) = &mut app.form {
match f.form.on_key(code, key) {
FormKey::Open => {}
FormKey::Cancel => app.form = None,
FormKey::Submit => app.submit_remote(),
}
return false;
}
if let Some(confirm) = &mut app.confirm
&& confirm.target.typed()
{
match code {
KeyCode::Enter if confirm.typed.text == confirm.target.name() => {
if let Some(confirm) = app.confirm.take() {
app.delete(confirm.target);
}
}
KeyCode::Esc => app.confirm = None,
_ => {
confirm.typed.on_key(key);
}
}
return false;
}
if let Some(confirm) = &mut app.confirm {
if is_left(code) {
confirm.yes = true;
} else if is_right(code) {
confirm.yes = false;
} else if code == KeyCode::Char('y') || (is_open(code) && confirm.yes) {
if let Some(confirm) = app.confirm.take() {
app.delete(confirm.target);
}
} else if is_open(code) || is_back(code) || matches!(code, KeyCode::Char('q' | 'n')) {
app.confirm = None;
}
return false;
}
if let Some(cmd) = &mut app.command {
match cmd.on_key(code) {
Typed::Open => {}
Typed::Cancel => app.command = None,
Typed::Run(run) => {
app.command = None;
match run {
Command::Help => app.help(),
Command::Quit => return true,
Command::Unknown(what) => {
app.set_failed(format!("unknown command `{what}` · ? lists every key"))
}
}
}
}
return false;
}
if let Some(pane) = app.editing {
if is_help(code) {
app.help();
} else {
query_key(app, pane, code, key);
}
return false;
}
if code == KeyCode::Char('q') {
return true;
}
if is_help(code) {
app.help();
return false;
}
if code == KeyCode::Char(':') {
app.command = Some(CommandLine::default());
return false;
}
if !ctrl && code == KeyCode::Char('/') {
app.open_query(Pane::Top);
return false;
}
if ctrl && code == KeyCode::Char('f') {
app.open_query(Pane::Bottom);
return false;
}
if code == KeyCode::Char('r') {
app.refresh();
return false;
}
let moving = !ctrl && (is_up(code) || is_down(code) || is_left(code) || is_right(code));
if app.pending && !moving {
app.settle();
}
if !ctrl && code == KeyCode::Char('K') {
app.inspect();
return false;
}
let steps = app.accel.steps(code, ctrl);
match app.level {
Level::Repos => repos_key(app, code, ctrl, steps),
Level::Remotes => remotes_key(app, code, ctrl, steps),
Level::Branches => branches_key(app, code, ctrl, steps),
Level::Tags => tags_key(app, code, ctrl, steps),
Level::Commits => commits_key(app, code, ctrl, steps),
Level::Diff => diff_key(app, code, ctrl, steps, terminal),
}
}
fn query_key(app: &mut App, pane: Pane, code: KeyCode, key: KeyEvent) {
if code == KeyCode::Enter {
app.editing = None;
return;
}
if code == KeyCode::Esc {
if let Some(sel) = app.pane_sel(pane) {
sel.query.clear();
}
app.editing = None;
app.refilter(pane);
return;
}
let Some(sel) = app.pane_sel(pane) else {
app.editing = None;
return;
};
if sel.query.on_key(key) {
app.refilter(pane);
}
}
fn repos_key(app: &mut App, code: KeyCode, ctrl: bool, steps: usize) -> bool {
if !ctrl && matches!(code, KeyCode::Char('s' | 'S')) {
let pull = code == KeyCode::Char('S');
app.set_status(if pull {
"pulling every repo…"
} else {
"syncing every repo…"
});
app.sync_next = Some(pull);
return false;
}
if ctrl && (is_down(code) || is_up(code)) {
app.bsel.step(is_down(code), steps);
} else if is_down(code) || is_up(code) {
if app.rsel.step(is_down(code), steps) {
app.pending = true;
}
} else if !ctrl && (is_left(code) || is_right(code)) {
if app.rsel.slide(is_right(code), repos::SCOPES.len()) {
app.rescope_repos();
app.pending = true;
}
} else if is_open(code) && !app.bsel.is_empty() {
app.enter_branch();
app.level = Level::Branches;
} else if is_back(code) {
return app.back();
}
false
}
fn remotes_key(app: &mut App, code: KeyCode, ctrl: bool, steps: usize) -> bool {
if !ctrl {
match code {
KeyCode::Char('p') => app.ask_push(),
KeyCode::Char('c') => app.create_remote(),
KeyCode::Char('e') => app.edit_remote(),
KeyCode::Char('d') => app.ask_delete_remote(),
KeyCode::Char(c @ ('s' | 'S')) if app.remote().is_some() => {
let name = app.remote().map(|r| r.name.clone()).unwrap_or_default();
app.set_status(if c == 'S' {
format!("pulling from {name}…")
} else {
format!("syncing {name}…")
});
app.sync_next = Some(c == 'S');
}
_ => {}
}
if matches!(code, KeyCode::Char('p' | 'c' | 'e' | 'd' | 's' | 'S')) {
return false;
}
}
if ctrl && (is_down(code) || is_up(code)) {
app.bsel.step(is_down(code), steps);
} else if is_down(code) || is_up(code) {
if app.msel.step(is_down(code), steps) {
app.pending = true;
}
} else if is_open(code) && !app.bsel.is_empty() {
app.enter_branch();
app.level = Level::Branches;
} else if is_back(code) {
return app.back();
}
false
}
fn branches_key(app: &mut App, code: KeyCode, ctrl: bool, steps: usize) -> bool {
if !ctrl && code == KeyCode::Char('d') {
app.ask_delete_branch();
return false;
}
if !ctrl && code == KeyCode::Char('u') {
if app.undo.as_ref().is_some_and(|u| u.branch) {
app.undo();
}
return false;
}
if !ctrl && matches!(code, KeyCode::Char('s' | 'S')) {
let pull = code == KeyCode::Char('S');
app.set_status(if pull { "pulling…" } else { "syncing…" });
app.sync_next = Some(pull);
return false;
}
if ctrl && (is_down(code) || is_up(code)) {
app.csel.step(is_down(code), steps);
} else if is_down(code) || is_up(code) {
if app.bsel.step(is_down(code), steps) {
app.pending = true;
}
} else if !ctrl && (is_left(code) || is_right(code)) {
if !app.remote_branches() && app.bsel.slide(is_right(code), branches::SCOPES.len()) {
app.rescope_branches();
app.pending = true;
}
} else if is_open(code) && !app.csel.is_empty() {
app.level = Level::Commits;
app.enter_commit();
} else if is_back(code) {
return app.back();
}
false
}
fn tags_key(app: &mut App, code: KeyCode, ctrl: bool, steps: usize) -> bool {
if !ctrl && code == KeyCode::Char('d') {
app.ask_delete_tag();
return false;
}
if !ctrl && code == KeyCode::Char('u') {
if app.undo.as_ref().is_some_and(|u| !u.branch) {
app.undo();
}
return false;
}
if !ctrl && code == KeyCode::Char('t') {
app.offer_tracking();
return false;
}
if ctrl && (is_down(code) || is_up(code)) {
app.csel.step(is_down(code), steps);
} else if is_down(code) || is_up(code) {
if app.tsel.step(is_down(code), steps) {
app.pending = true;
}
} else if !ctrl && (is_left(code) || is_right(code)) {
if app.tsel.slide(is_right(code), tags::SCOPES.len()) {
app.rescope_tags();
app.pending = true;
}
} else if is_open(code) && !app.csel.is_empty() {
app.level = Level::Commits;
app.enter_commit();
} else if is_back(code) {
return app.back();
}
false
}
fn commits_key(app: &mut App, code: KeyCode, ctrl: bool, steps: usize) -> bool {
if app.stashes && !ctrl {
match code {
KeyCode::Char('a') => app.apply_stash(false),
KeyCode::Char('p') => app.apply_stash(true),
KeyCode::Char('d') => app.ask_drop_stash(),
_ => {}
}
if matches!(code, KeyCode::Char('a' | 'p' | 'd')) || is_left(code) || is_right(code) {
return false;
}
}
if ctrl && (is_down(code) || is_up(code)) {
app.fsel.step(is_down(code), steps);
} else if is_down(code) || is_up(code) {
if app.csel.step(is_down(code), steps) {
app.pending = true;
}
} else if !ctrl && (is_left(code) || is_right(code)) {
if app.csel.slide(is_right(code), commits::SCOPES.len()) {
app.rescope_commits();
app.pending = true;
}
} else if is_open(code) && !app.fsel.is_empty() {
app.open_diff();
} else if is_back(code) {
return app.back();
}
false
}
fn diff_key(
app: &mut App,
code: KeyCode,
ctrl: bool,
steps: usize,
terminal: &mut DefaultTerminal,
) -> bool {
if ctrl && (is_down(code) || is_up(code)) {
app.scroll_diff(is_down(code), steps);
} else if ctrl && (is_left(code) || is_right(code)) {
app.pan_diff(is_right(code), steps);
} else if is_open(code) {
app.difftool(terminal);
} else if is_back(code) {
return app.back();
}
app.clamp_diff();
false
}