use std::io;
use std::path::PathBuf;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{backend::CrosstermBackend, Terminal};
use gitstack::{
analyze_related_files, analyze_topology, checkout_branch, compare_branches, create_branch,
create_commit, delete_branch, get_commit_files, get_file_patch, get_head_hash_cached,
get_stash_list, get_status_cached, has_staged_files, list_branches_cached, stage_all,
stage_file, stash_apply, stash_drop, stash_pop, stash_save, unstage_all, unstage_file, App,
CommitType, FileStatusKind, LanguageConfig, QuickAction, SidebarPanel, TopologyConfig,
};
use crate::actions::{execute_quick_action, friendly_error_message};
use crate::{
BackgroundAnalysisState, BackgroundBlameState, BackgroundDiffState, BackgroundGitOpState,
BackgroundLoadState, RemoteFetchState, MAX_LOAD_COUNT,
};
pub fn reload_events_bg(bg_load_state: &mut BackgroundLoadState) {
bg_load_state.start(0, MAX_LOAD_COUNT);
}
pub fn post_checkout_reload(
app: &mut App,
branch: String,
bg_load_state: &mut BackgroundLoadState,
) {
app.update_branch(branch);
app.filter_clear();
reload_events_bg(bg_load_state);
if let Ok(head_hash) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(head_hash);
}
if let Ok(branches) = list_branches_cached(app.get_repo()) {
app.update_branches(branches);
}
}
pub fn overlay_visible_lines(
terminal: &Terminal<CrosstermBackend<io::Stdout>>,
height_ratio: f32,
padding: usize,
) -> usize {
let term_height = terminal.size().map(|s| s.height).unwrap_or(24);
((term_height as f32 * height_ratio) as usize).saturating_sub(padding)
}
pub fn set_error_status(app: &mut App, prefix: &str, e: &anyhow::Error) {
app.set_status_message(format!(
"{}: {}",
prefix,
friendly_error_message(e, app.language)
));
}
pub fn handle_filter_keys(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) => {
app.filter_clear();
app.end_filter();
}
(KeyCode::Enter, _) => {
app.end_filter();
if app.filter_query.has_file_filter() {
app.preload_file_cache(|hash| get_commit_files(hash).ok());
app.reapply_filter();
}
}
(KeyCode::Char('s'), KeyModifiers::CONTROL) => {
app.start_preset_save();
}
(KeyCode::Backspace, _) => app.filter_pop(),
(KeyCode::Char(c), _) => app.filter_push(c),
_ => {}
}
}
pub fn handle_preset_save_keys(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc => app.end_preset_save(),
KeyCode::Enter => {
if app.save_preset() {
app.end_preset_save();
app.end_filter();
}
}
KeyCode::Up | KeyCode::Left => app.preset_slot_up(),
KeyCode::Down | KeyCode::Right => app.preset_slot_down(),
KeyCode::Char(c) if c.is_ascii_digit() && c != '0' => {
if let Some(digit) = c.to_digit(10) {
let slot = digit as usize;
if slot <= 5 {
app.preset_save_slot = slot;
}
}
}
KeyCode::Backspace => app.preset_name_pop(),
KeyCode::Char(c) => app.preset_name_push(c),
_ => {}
}
}
pub fn handle_branch_select_keys(
app: &mut App,
key: KeyEvent,
bg_load_state: &mut BackgroundLoadState,
) {
match key.code {
KeyCode::Esc => app.end_branch_select(),
KeyCode::Char('j') | KeyCode::Down => app.branch_move_down(),
KeyCode::Char('k') | KeyCode::Up => app.branch_move_up(),
KeyCode::Enter => {
if let Some(branch) = app.selected_branch() {
let branch = branch.to_string();
match checkout_branch(&branch) {
Ok(_) => {
app.update_branch(branch);
reload_events_bg(bg_load_state);
if let Ok(head_hash) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(head_hash);
}
app.set_status_message(format!(
"{} {}",
app.language.status_switched_to(),
app.branch_name()
));
}
Err(e) => {
set_error_status(app, app.language.status_checkout_failed(), &e);
}
}
}
app.end_branch_select();
}
KeyCode::Char('n') => {
app.start_branch_create();
}
KeyCode::Char('d') => {
if let Some(branch) = app.selected_branch() {
let branch = branch.to_string();
match delete_branch(&branch) {
Ok(_) => {
app.set_status_message(format!(
"{}: {}",
app.language.status_deleted_branch(),
branch
));
if let Ok(branches) = list_branches_cached(app.get_repo()) {
app.start_branch_select(branches);
}
}
Err(e) => {
set_error_status(app, app.language.status_delete_failed(), &e);
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_branch_create_keys(
app: &mut App,
key: KeyEvent,
bg_load_state: &mut BackgroundLoadState,
) {
match key.code {
KeyCode::Esc => app.end_branch_create(),
KeyCode::Enter => {
let branch_name = app.branch_create_name().to_string();
if !branch_name.is_empty() {
match create_branch(&branch_name) {
Ok(_) => {
app.set_status_message(format!(
"{}: {}",
app.language.status_created_branch(),
branch_name
));
if checkout_branch(&branch_name).is_ok() {
app.update_branch(branch_name);
reload_events_bg(bg_load_state);
if let Ok(head_hash) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(head_hash);
}
}
app.end_branch_create();
app.end_branch_select();
}
Err(e) => {
set_error_status(app, app.language.status_create_failed(), &e);
}
}
}
}
KeyCode::Backspace => app.branch_create_pop(),
KeyCode::Char(c) => app.branch_create_push(c),
_ => {}
}
}
pub fn handle_quick_action_keys(
app: &mut App,
key: KeyEvent,
bg_analysis_state: &mut BackgroundAnalysisState,
) {
match key.code {
KeyCode::Esc | KeyCode::Char('.') => app.end_quick_action_view(),
KeyCode::Char('j') | KeyCode::Down => app.quick_action_move_down(),
KeyCode::Char('k') | KeyCode::Up => app.quick_action_move_up(),
KeyCode::Char(c) if c.is_ascii_digit() && c != '0' => {
let idx = c.to_digit(10).unwrap_or(1) as usize - 1;
if idx < QuickAction::all().len() {
app.quick_action_selected_index = idx;
}
}
KeyCode::Enter => {
if let Some(action) = app.selected_quick_action() {
execute_quick_action(app, action, bg_analysis_state);
}
app.end_quick_action_view();
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_help_keys(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc | KeyCode::Char('?') => app.close_help(),
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_detail_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
bg_diff_state: &mut BackgroundDiffState,
bg_blame_state: &mut BackgroundBlameState,
) {
if app.detail_diff_cache.is_none() && !bg_diff_state.is_running() {
if let Some(event) = app.selected_event() {
bg_diff_state.start(event.short_hash.clone());
}
}
let term_height = terminal.size().map(|s| s.height).unwrap_or(24);
let overlay_height = 20u16.min(term_height.saturating_sub(4));
let visible_file_lines = overlay_height.saturating_sub(10) as usize;
match key.code {
KeyCode::Esc => app.close_detail(),
KeyCode::Char('j') | KeyCode::Down => {
app.detail_move_down();
app.detail_adjust_scroll(visible_file_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.detail_move_up(),
KeyCode::Char('F') => {
if let Some(ref diff) = app.detail_diff_cache {
if let Some(file) = diff.files.get(app.detail_nav.selected_index) {
let path = file.path.clone();
let commit_files = app
.all_events()
.iter()
.filter_map(|e| get_commit_files(&e.short_hash).ok());
let related = analyze_related_files(&path, commit_files);
app.start_related_files_view(related);
}
}
}
KeyCode::Char('B') => {
if !bg_blame_state.is_running() {
if let Some(ref diff) = app.detail_diff_cache {
if let Some(file) = diff.files.get(app.detail_nav.selected_index) {
let path = file.path.clone();
app.set_status_message("Loading blame...".to_string());
bg_blame_state.start(path);
}
}
}
}
KeyCode::Char('P') => {
if let Some(event) = app.selected_event() {
if let Some(ref diff) = app.detail_diff_cache {
if let Some(file) = diff.files.get(app.detail_nav.selected_index) {
let hash = event.short_hash.clone();
let path = file.path.clone();
if let Ok(patch) = get_file_patch(&hash, &path) {
app.start_patch_view(patch);
}
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_status_view_keys(
app: &mut App,
key: KeyEvent,
bg_git_op_state: &mut BackgroundGitOpState,
) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('s'), KeyModifiers::NONE) => app.end_status_view(),
(KeyCode::Char('S'), KeyModifiers::SHIFT) => {
let groups = gitstack::staging::suggest_groups(&app.file_statuses, None);
if groups.is_empty() {
app.set_status_message("No staging groups suggested".to_string());
} else {
let summary: Vec<String> = groups
.iter()
.enumerate()
.map(|(i, g)| format!("[{}] {} ({} files)", i + 1, g.reason, g.files.len()))
.collect();
app.staging_groups = groups;
app.set_status_message(format!("Staging groups: {}", summary.join(" | ")));
}
}
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => app.status_move_down(),
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => app.status_move_up(),
(KeyCode::Char('a'), KeyModifiers::NONE)
| (KeyCode::Char('A'), KeyModifiers::SHIFT)
| (KeyCode::Char(' '), _) => handle_staging_keys(app, key),
(KeyCode::Char('c'), KeyModifiers::NONE) => {
if has_staged_files().unwrap_or(false) {
app.start_commit_input();
app.generate_commit_suggestions();
} else {
app.set_status_message(app.language.status_no_staged_files().to_string());
}
}
(KeyCode::Char('l'), _) => handle_pull(app, bg_git_op_state),
(KeyCode::Char('p'), _) => handle_push(app, bg_git_op_state),
(KeyCode::Char('q'), _) => app.quit(),
_ => {}
}
}
fn handle_staging_keys(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Char('a'), KeyModifiers::NONE) => {
if stage_all().is_ok() {
app.set_status_message(app.language.status_all_staged().to_string());
}
}
(KeyCode::Char('A'), KeyModifiers::SHIFT) => {
if unstage_all().is_ok() {
app.set_status_message(app.language.status_all_unstaged().to_string());
}
}
(KeyCode::Char(' '), _) => {
if let Some(status) = app.selected_file_status() {
let path = status.path.clone();
let is_staged = matches!(
status.kind,
FileStatusKind::StagedNew
| FileStatusKind::StagedModified
| FileStatusKind::StagedDeleted
);
let result = if is_staged {
unstage_file(&path)
} else {
stage_file(&path)
};
if let Err(e) = result {
set_error_status(app, app.language.status_error(), &e);
}
}
}
_ => {}
}
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.update_file_statuses(statuses);
}
}
fn handle_pull(app: &mut App, bg_git_op_state: &mut BackgroundGitOpState) {
if bg_git_op_state.is_running() {
app.set_status_message("Git operation already in progress...".to_string());
return;
}
app.set_status_message(app.language.status_pulling().to_string());
bg_git_op_state.start_pull();
}
fn handle_push(app: &mut App, bg_git_op_state: &mut BackgroundGitOpState) {
if bg_git_op_state.is_running() {
app.set_status_message("Git operation already in progress...".to_string());
return;
}
app.set_status_message("Pushing...".to_string());
bg_git_op_state.start_push();
}
pub fn handle_commit_input_keys(
app: &mut App,
key: KeyEvent,
bg_load_state: &mut BackgroundLoadState,
) -> bool {
match key.code {
KeyCode::Esc => app.end_commit_input(),
KeyCode::Enter => {
if !app.commit_message.is_empty() {
match create_commit(&app.commit_message) {
Ok(_) => {
app.set_status_message(app.language.status_committed().to_string());
reload_events_bg(bg_load_state);
if let Ok(head_hash) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(head_hash);
}
}
Err(e) => {
set_error_status(app, app.language.status_commit_failed(), &e);
}
}
app.commit_message_clear();
app.end_commit_input();
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.update_file_statuses(statuses);
}
}
}
KeyCode::Backspace => app.commit_message_pop(),
KeyCode::Char(c) => {
if app.commit_message.is_empty() && !app.commit_suggestions.is_empty() {
match c {
'1' => {
app.apply_suggestion(0);
return true;
}
'2' if app.commit_suggestions.len() > 1 => {
app.apply_suggestion(1);
return true;
}
'3' if app.commit_suggestions.len() > 2 => {
app.apply_suggestion(2);
return true;
}
_ => {}
}
}
if app.commit_type.is_none() && app.commit_message.is_empty() {
match c {
'f' => app.select_commit_type(CommitType::Feat),
'x' => app.select_commit_type(CommitType::Fix),
'd' => app.select_commit_type(CommitType::Docs),
's' => app.select_commit_type(CommitType::Style),
'r' => app.select_commit_type(CommitType::Refactor),
't' => app.select_commit_type(CommitType::Test),
'c' => app.select_commit_type(CommitType::Chore),
'p' => app.select_commit_type(CommitType::Perf),
_ => app.commit_message_push(c),
}
} else {
app.commit_message_push(c);
}
}
_ => {}
}
false
}
pub fn handle_topology_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
bg_load_state: &mut BackgroundLoadState,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 6);
match key.code {
KeyCode::Esc | KeyCode::Char('t') => app.end_topology_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.topology_move_down();
app.topology_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.topology_move_up(),
KeyCode::Enter => {
if let Some(branch) = app.selected_topology_branch() {
let branch = branch.to_string();
match checkout_branch(&branch) {
Ok(_) => {
post_checkout_reload(app, branch, bg_load_state);
app.set_status_message(format!(
"{} {}",
app.language.status_switched_to(),
app.branch_name()
));
}
Err(e) => {
set_error_status(app, app.language.status_checkout_failed(), &e);
}
}
}
app.end_topology_view();
}
KeyCode::Char('D') => {
if let Some(target_branch) = app.selected_topology_branch() {
let target = target_branch.to_string();
let base = if app
.topology_cache
.as_ref()
.is_some_and(|t| t.branches.iter().any(|b| b.name == "main"))
{
"main"
} else {
"master"
};
match compare_branches(base, &target) {
Ok(compare) => {
app.start_branch_compare_view(compare);
}
Err(e) => {
set_error_status(app, app.language.status_compare_failed(), &e);
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_branch_compare_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.7, 8);
match key.code {
KeyCode::Esc => app.end_branch_compare_view(),
KeyCode::Tab => app.branch_compare_toggle_tab(),
KeyCode::Char('j') | KeyCode::Down => {
app.branch_compare_move_down();
app.branch_compare_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.branch_compare_move_up(),
KeyCode::Enter => {
if let Some(commit) = app.selected_branch_compare_commit() {
let hash = commit.hash.clone();
app.end_branch_compare_view();
app.end_topology_view();
let events: Vec<_> = app.events().collect();
for (i, event) in events.iter().enumerate() {
if event.short_hash == hash {
app.selected_index = i;
app.open_detail();
break;
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_related_files_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.7, 6);
match key.code {
KeyCode::Esc => app.close_related_files_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.related_files_move_down();
app.related_files_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.related_files_move_up(),
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_stats_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.end_stats_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.stats_move_down();
app.stats_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.stats_move_up(),
KeyCode::Enter => {
if let Some(author) = app.selected_author() {
let author = author.to_string();
app.end_stats_view();
app.filter_by_author(&author);
}
}
KeyCode::Char('e') => {
app.export_stats();
}
_ => {}
}
}
pub fn handle_heatmap_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.end_heatmap_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.heatmap_move_down();
app.heatmap_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.heatmap_move_up(),
KeyCode::Enter => {
if let Some(file_path) = app.selected_heatmap_file() {
let file_path = file_path.to_string();
app.end_heatmap_view();
app.filter_by_file(&file_path);
}
}
KeyCode::Char('e') => {
app.export_heatmap();
}
KeyCode::Char('+') | KeyCode::Char('=') => {
app.heatmap_increase_aggregation();
}
KeyCode::Char('-') => {
app.heatmap_decrease_aggregation();
}
_ => {}
}
}
pub fn handle_file_history_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('F') => app.end_file_history_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.file_history_move_down();
app.file_history_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.file_history_move_up(),
KeyCode::Enter => {
if let Some(entry) = app.selected_file_history() {
let hash = entry.hash.clone();
app.end_file_history_view();
let events: Vec<_> = app.events().collect();
for (i, event) in events.iter().enumerate() {
if event.short_hash == hash {
app.selected_index = i;
app.open_detail();
break;
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_timeline_view_keys(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Esc | KeyCode::Char('T') => app.end_timeline_view(),
KeyCode::Char('e') => {
app.export_timeline();
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_blame_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.85, 4);
match key.code {
KeyCode::Esc | KeyCode::Char('B') => app.end_blame_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.blame_move_down();
app.blame_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.blame_move_up(),
KeyCode::Enter => {
if let Some(line) = app.selected_blame_line() {
let hash = line.hash.clone();
app.end_blame_view();
let events: Vec<_> = app.events().collect();
for (i, event) in events.iter().enumerate() {
if event.short_hash == hash {
app.selected_index = i;
app.open_detail();
break;
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_ownership_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('O') => app.end_ownership_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.ownership_move_down();
app.ownership_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.ownership_move_up(),
KeyCode::Char('e') => {
app.export_ownership();
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_impact_score_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('I') => app.end_impact_score_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.impact_score_move_down();
app.impact_score_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.impact_score_move_up(),
KeyCode::Enter => {
if let Some(commit) = app.selected_impact_score() {
let hash = commit.commit_hash.clone();
app.end_impact_score_view();
let events: Vec<_> = app.events().collect();
for (i, event) in events.iter().enumerate() {
if event.short_hash == hash {
app.selected_index = i;
app.open_detail();
break;
}
}
}
}
KeyCode::Char('e') => {
app.export_impact_scores();
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_change_coupling_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.85, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('C') => app.end_change_coupling_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.change_coupling_move_down();
app.change_coupling_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.change_coupling_move_up(),
KeyCode::Char('e') => {
app.export_change_coupling();
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_quality_score_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('Q') => app.end_quality_score_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.quality_score_move_down();
app.quality_score_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.quality_score_move_up(),
KeyCode::Enter => {
if let Some(commit) = app.selected_quality_score() {
let hash = commit.commit_hash.clone();
app.end_quality_score_view();
let events: Vec<_> = app.events().collect();
for (i, event) in events.iter().enumerate() {
if event.short_hash == hash {
app.selected_index = i;
app.open_detail();
break;
}
}
}
}
KeyCode::Char('e') => {
app.export_quality_scores();
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_stash_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.6, 6);
match key.code {
KeyCode::Esc | KeyCode::Char('z') => app.end_stash_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.stash_move_down();
app.stash_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.stash_move_up(),
KeyCode::Char(' ') => {
if let Some(entry) = app.selected_stash_entry() {
let index = entry.index;
match stash_apply(index) {
Ok(_) => {
app.set_status_message(format!(
"{} stash@{{{}}}",
app.language.status_applied_stash(),
index
));
}
Err(e) => {
set_error_status(app, app.language.status_apply_failed(), &e);
}
}
}
}
KeyCode::Char('p') => {
if let Some(entry) = app.selected_stash_entry() {
let index = entry.index;
match stash_pop(index) {
Ok(_) => {
app.set_status_message(format!(
"{} stash@{{{}}}",
app.language.status_popped_stash(),
index
));
if let Ok(stashes) = get_stash_list() {
app.update_stash_cache(stashes);
}
}
Err(e) => {
set_error_status(app, app.language.status_pop_failed(), &e);
}
}
}
}
KeyCode::Char('d') => {
if let Some(entry) = app.selected_stash_entry() {
let index = entry.index;
match stash_drop(index) {
Ok(_) => {
app.set_status_message(format!(
"{} stash@{{{}}}",
app.language.status_dropped_stash(),
index
));
if let Ok(stashes) = get_stash_list() {
app.update_stash_cache(stashes);
}
}
Err(e) => {
set_error_status(app, app.language.status_drop_failed(), &e);
}
}
}
}
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_patch_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.85, 6);
match key.code {
KeyCode::Esc | KeyCode::Char('P') => app.end_patch_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.patch_scroll_down(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.patch_scroll_up(),
KeyCode::Char('q') => app.quit(),
_ => {}
}
}
pub fn handle_normal_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
fetch_state: &mut RemoteFetchState,
repo_path: &Option<PathBuf>,
bg_load_state: &mut BackgroundLoadState,
) {
if handle_normal_navigation_keys(app, key, terminal) {
return;
}
if handle_normal_action_keys(app, key, bg_load_state) {
return;
}
handle_normal_view_keys(app, key, fetch_state, repo_path);
}
fn handle_normal_navigation_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> bool {
match (key.code, key.modifiers) {
(KeyCode::Char('j') | KeyCode::Down, _) => {
if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Branches {
app.branch_move_down();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Files {
app.status_move_down();
app.clear_file_diff();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Stash {
app.stash_move_down();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.move_down();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
} else if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail_file_move_down();
let h = terminal.size().map(|s| s.height).unwrap_or(24) as usize;
app.commit_detail_auto_scroll(h.saturating_sub(6));
} else {
app.move_down();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
}
(KeyCode::Char('k') | KeyCode::Up, _) => {
if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Branches {
app.branch_move_up();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Files {
app.status_move_up();
app.clear_file_diff();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Stash {
app.stash_move_up();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.move_up();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
} else if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail_file_move_up();
let h = terminal.size().map(|s| s.height).unwrap_or(24) as usize;
app.commit_detail_auto_scroll(h.saturating_sub(6));
} else {
app.move_up();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
}
(KeyCode::Char('g') | KeyCode::Home, _) => {
app.move_to_top();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char('G') | KeyCode::End, _) => {
app.move_to_bottom();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char('@'), _) => {
app.jump_to_head();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char(']'), _) => {
app.jump_to_next_label();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char('['), _) => {
app.jump_to_prev_label();
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
app.page_down(10);
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
app.page_up(10);
app.detail_diff_cache = None;
app.reset_commit_detail_file_state();
}
(KeyCode::Char('J'), _) => {
if app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail.scroll = app.commit_detail.scroll.saturating_add(3);
}
}
(KeyCode::Char('K'), _) => {
if app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail.scroll = app.commit_detail.scroll.saturating_sub(3);
}
}
(KeyCode::Left, _) => {
if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail.h_scroll = app.commit_detail.h_scroll.saturating_sub(4);
}
}
(KeyCode::Right, _) => {
if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail.h_scroll = app.commit_detail.h_scroll.saturating_add(4);
}
}
_ => return false,
}
true
}
fn handle_normal_action_keys(
app: &mut App,
key: KeyEvent,
bg_load_state: &mut BackgroundLoadState,
) -> bool {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), _) => app.quit(),
(KeyCode::Char('?'), _) => app.toggle_help(),
(KeyCode::Enter, _) => {
if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail_toggle_file_diff();
} else if app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Branches {
handle_branch_checkout(app, bg_load_state);
} else {
app.open_detail();
}
}
(KeyCode::Char(' '), _) => {
if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits {
app.commit_detail_toggle_file_diff();
}
}
(KeyCode::Char('e'), _)
if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits =>
{
app.commit_detail_expand_all();
}
(KeyCode::Char('E'), _)
if !app.sidebar_focused && app.active_sidebar_panel == SidebarPanel::Commits =>
{
app.commit_detail_collapse_all();
}
_ => return false,
}
true
}
pub fn handle_branch_checkout(app: &mut App, bg_load_state: &mut BackgroundLoadState) {
if let Some(branch) = app.selected_branch() {
let branch = branch.to_string();
match checkout_branch(&branch) {
Ok(_) => {
post_checkout_reload(app, branch, bg_load_state);
app.set_status_message(format!(
"{} {}",
app.language.status_switched_to(),
app.branch_name()
));
}
Err(e) => {
app.set_status_message(format!("{}: {}", app.language.status_checkout_failed(), e));
}
}
}
}
fn handle_normal_view_keys(
app: &mut App,
key: KeyEvent,
fetch_state: &mut RemoteFetchState,
repo_path: &Option<PathBuf>,
) {
match (key.code, key.modifiers) {
(KeyCode::Char('/'), _) => app.start_filter(),
(KeyCode::Char(c @ '1'..='5'), _) => {
if let Some(panel) = SidebarPanel::from_number(c as u8 - b'0') {
app.active_sidebar_panel = panel;
app.sidebar_focused = true;
}
}
(KeyCode::Tab, _) => {
app.sidebar_focused = !app.sidebar_focused;
}
(KeyCode::Char('h'), _) if !app.sidebar_focused => {
app.sidebar_focused = true;
}
(KeyCode::Char('l'), _) if app.sidebar_focused => {
app.sidebar_focused = false;
}
(KeyCode::Char('L'), KeyModifiers::SHIFT) => {
app.language = app.language.toggle();
let config = LanguageConfig {
language: app.language,
};
let _ = config.save();
}
(KeyCode::Char('b'), _) => {
if let Ok(branches) = list_branches_cached(app.get_repo()) {
app.start_branch_select(branches);
}
}
(KeyCode::Char('t'), _) => {
let config = TopologyConfig::default();
if let Ok(topology) = analyze_topology(&config) {
app.start_topology_view(topology);
}
}
(KeyCode::Char('s'), _) => {
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.start_status_view(statuses);
}
}
(KeyCode::Char('z'), KeyModifiers::NONE) => {
if let Ok(stashes) = get_stash_list() {
app.start_stash_view(stashes);
}
}
(KeyCode::Char('Z'), KeyModifiers::SHIFT) => match stash_save("WIP: stashed by gitstack") {
Ok(_) => {
app.set_status_message(app.language.status_stashed().to_string());
}
Err(e) => {
set_error_status(app, app.language.status_stash_failed(), &e);
}
},
(KeyCode::Char('y'), KeyModifiers::NONE) => {
if let Some(event) = app.selected_event() {
let hash = event.short_hash.clone();
match arboard::Clipboard::new() {
Ok(mut clipboard) => {
if clipboard.set_text(&hash).is_ok() {
app.set_status_message(format!(
"{}: {}",
app.language.status_copied(),
hash
));
} else {
app.set_status_message(app.language.status_copy_failed().to_string());
}
}
Err(_) => {
app.set_status_message(
app.language.status_clipboard_unavailable().to_string(),
);
}
}
}
}
(KeyCode::Char('r'), KeyModifiers::NONE) => {
if !fetch_state.is_running() {
if let Some(ref path) = repo_path {
app.set_status_message(app.language.status_fetching().to_string());
fetch_state.start(path.clone(), false);
}
}
}
(KeyCode::Char('W'), KeyModifiers::SHIFT) => {
app.watch_mode = !app.watch_mode;
if app.watch_mode {
app.set_status_message(app.language.watch_mode_enabled().to_string());
} else {
app.set_status_message(app.language.watch_mode_disabled().to_string());
}
}
(KeyCode::Char('P'), KeyModifiers::SHIFT) => {
app.start_pr_create();
}
(KeyCode::Char('.'), KeyModifiers::NONE) => {
app.start_quick_action_view();
}
(KeyCode::Esc, _) if !app.filter_text.is_empty() => app.filter_clear(),
_ => {}
}
}
pub fn handle_review_queue_keys(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('q'), KeyModifiers::NONE) => {
app.end_review_queue_view();
}
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
app.review_queue_move_down();
}
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => {
app.review_queue_move_up();
}
(KeyCode::Char('a'), KeyModifiers::NONE) => {
if let Some(ref mut queue) = app.review_queue_view.cache {
let idx = app.review_queue_view.nav.selected_index;
if let Some(item) = queue.items.get(idx) {
let id = item.id.clone();
let _ = queue.resolve_review(
&id,
gitstack::ReviewStatus::Approved,
"Approved via TUI",
);
app.set_status_message("Review approved".to_string());
}
}
}
(KeyCode::Char('x'), KeyModifiers::NONE) => {
if let Some(ref mut queue) = app.review_queue_view.cache {
let idx = app.review_queue_view.nav.selected_index;
if let Some(item) = queue.items.get(idx) {
let id = item.id.clone();
let _ = queue.resolve_review(
&id,
gitstack::ReviewStatus::Rejected,
"Rejected via TUI",
);
app.set_status_message("Review rejected".to_string());
}
}
}
_ => {}
}
}
pub fn handle_review_pack_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.8, 8);
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.end_review_pack_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.review_pack_move_down();
app.review_pack_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.review_pack_move_up(),
_ => {}
}
}
pub fn handle_next_actions_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.7, 6);
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.end_next_actions_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.next_actions_move_down();
app.next_actions_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.next_actions_move_up(),
_ => {}
}
}
pub fn handle_handoff_view_keys(
app: &mut App,
key: KeyEvent,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) {
let visible_lines = overlay_visible_lines(terminal, 0.75, 6);
match key.code {
KeyCode::Esc | KeyCode::Char('q') => app.end_handoff_view(),
KeyCode::Char('j') | KeyCode::Down => {
app.handoff_move_down();
app.handoff_adjust_scroll(visible_lines);
}
KeyCode::Char('k') | KeyCode::Up => app.handoff_move_up(),
KeyCode::Char('y') => {
if let Some(ref ctx) = app.handoff_view.cache {
let text = ctx.prompt.clone();
match arboard::Clipboard::new() {
Ok(mut clipboard) => {
if clipboard.set_text(&text).is_ok() {
app.set_status_message(format!(
"{}: {}",
app.language.status_copied(),
"handoff prompt"
));
} else {
app.set_status_message(app.language.status_copy_failed().to_string());
}
}
Err(_) => {
app.set_status_message(
app.language.status_clipboard_unavailable().to_string(),
);
}
}
}
}
_ => {}
}
}
pub fn handle_pr_create_keys(app: &mut App, key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Esc, _) => {
app.end_pr_create();
}
(KeyCode::Tab, _) => {
app.pr_create_state.editing_body = !app.pr_create_state.editing_body;
}
(KeyCode::Enter, _) => {
match gitstack::create_pr(&app.pr_create_state) {
Ok(url) => {
app.set_status_message(format!("PR created: {}", url));
app.end_pr_create();
}
Err(e) => {
app.set_status_message(format!("Failed: {}", e));
}
}
}
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
if app.pr_create_state.editing_body {
app.pr_create_state.body.push(c);
} else {
app.pr_create_state.title.push(c);
}
}
(KeyCode::Backspace, _) => {
if app.pr_create_state.editing_body {
app.pr_create_state.body.pop();
} else {
app.pr_create_state.title.pop();
}
}
_ => {}
}
}