use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::studio::events::SideEffect;
use crate::studio::state::{EmojiMode, Modal, PanelId, StudioState};
use super::{copy_to_clipboard, spawn_commit_task};
pub fn handle_commit_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
if state.modes.commit.editing_message {
return handle_editing_key(state, key);
}
match state.focused_panel {
PanelId::Left => handle_files_key(state, key),
PanelId::Center => handle_message_key(state, key),
PanelId::Right => handle_diff_key(state, key),
}
}
fn handle_editing_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
if state.modes.commit.message_editor.handle_key(key) {
state.modes.commit.editing_message = state.modes.commit.message_editor.is_editing();
state.mark_dirty();
}
vec![]
}
fn sync_file_selection(state: &mut StudioState) {
if let Some(path) = state.modes.commit.file_tree.selected_path() {
state.modes.commit.diff_view.select_file_by_path(&path);
}
}
fn sync_tree_selection_from_diff(state: &mut StudioState) {
if let Some(path) = state
.modes
.commit
.diff_view
.current_diff()
.map(|diff| diff.path.clone())
{
let _ = state.modes.commit.file_tree.select_path(&path);
}
}
fn handle_files_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
state.modes.commit.file_tree.select_next();
sync_file_selection(state);
state.mark_dirty();
vec![]
}
KeyCode::Char('k') | KeyCode::Up => {
state.modes.commit.file_tree.select_prev();
sync_file_selection(state);
state.mark_dirty();
vec![]
}
KeyCode::Char('h') | KeyCode::Left => {
state.modes.commit.file_tree.collapse();
state.mark_dirty();
vec![]
}
KeyCode::Char('l') | KeyCode::Right => {
state.modes.commit.file_tree.expand();
state.mark_dirty();
vec![]
}
KeyCode::Char('g') | KeyCode::Home => {
state.modes.commit.file_tree.select_first();
sync_file_selection(state);
state.mark_dirty();
vec![]
}
KeyCode::Char('G') | KeyCode::End => {
state.modes.commit.file_tree.select_last();
sync_file_selection(state);
state.mark_dirty();
vec![]
}
KeyCode::PageDown | KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.modes.commit.file_tree.page_down(10);
sync_file_selection(state);
state.mark_dirty();
vec![]
}
KeyCode::PageUp | KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.modes.commit.file_tree.page_up(10);
sync_file_selection(state);
state.mark_dirty();
vec![]
}
KeyCode::Enter => {
if let Some(entry) = state.modes.commit.file_tree.selected_entry() {
if entry.is_dir {
state.modes.commit.file_tree.toggle_expand();
} else {
sync_file_selection(state);
state.focused_panel = PanelId::Right;
}
}
state.mark_dirty();
vec![]
}
KeyCode::Char('s') => {
if let Some(path) = state.modes.commit.file_tree.selected_path() {
vec![SideEffect::GitStage(path)]
} else {
vec![]
}
}
KeyCode::Char('u') => {
if let Some(path) = state.modes.commit.file_tree.selected_path() {
vec![SideEffect::GitUnstage(path)]
} else {
vec![]
}
}
KeyCode::Char('a') => vec![SideEffect::GitStageAll],
KeyCode::Char('U') => vec![SideEffect::GitUnstageAll],
KeyCode::Char('A') => {
state.modes.commit.show_all_files = !state.modes.commit.show_all_files;
let mode = if state.modes.commit.show_all_files {
"all files"
} else {
"changed files"
};
state.notify(crate::studio::state::Notification::info(format!(
"Showing: {}",
mode
)));
state.mark_dirty();
vec![SideEffect::RefreshGitStatus]
}
_ => vec![],
}
}
fn handle_diff_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
state.modes.commit.diff_view.scroll_down(1);
state.mark_dirty();
vec![]
}
KeyCode::Char('k') | KeyCode::Up => {
state.modes.commit.diff_view.scroll_up(1);
state.mark_dirty();
vec![]
}
KeyCode::PageDown | KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.modes.commit.diff_view.scroll_down(20);
state.mark_dirty();
vec![]
}
KeyCode::PageUp | KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.modes.commit.diff_view.scroll_up(20);
state.mark_dirty();
vec![]
}
KeyCode::Char(']') => {
state.modes.commit.diff_view.next_hunk();
state.mark_dirty();
vec![]
}
KeyCode::Char('[') => {
state.modes.commit.diff_view.prev_hunk();
state.mark_dirty();
vec![]
}
KeyCode::Char('n') => {
state.modes.commit.diff_view.next_file();
sync_tree_selection_from_diff(state);
state.mark_dirty();
vec![]
}
KeyCode::Char('p') => {
state.modes.commit.diff_view.prev_file();
sync_tree_selection_from_diff(state);
state.mark_dirty();
vec![]
}
KeyCode::Char('s') => {
let selected_path = state
.modes
.commit
.diff_view
.current_diff()
.map(|diff| diff.path.clone())
.or_else(|| state.modes.commit.file_tree.selected_path());
selected_path.map_or_else(Vec::new, |path| vec![SideEffect::GitStage(path)])
}
KeyCode::Char('u') => {
let selected_path = state
.modes
.commit
.diff_view
.current_diff()
.map(|diff| diff.path.clone())
.or_else(|| state.modes.commit.file_tree.selected_path());
selected_path.map_or_else(Vec::new, |path| vec![SideEffect::GitUnstage(path)])
}
_ => vec![],
}
}
fn handle_message_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
match key.code {
KeyCode::Char('e') => {
state.modes.commit.message_editor.enter_edit_mode();
state.modes.commit.editing_message = true;
state.mark_dirty();
vec![]
}
KeyCode::Char('p') => {
let presets = state.get_commit_presets();
state.modal = Some(Modal::PresetSelector {
input: String::new(),
presets,
selected: 0,
scroll: 0,
});
state.mark_dirty();
vec![]
}
KeyCode::Char('r') => {
state.set_iris_thinking("Generating commit message...");
state.modes.commit.generating = true;
vec![spawn_commit_task(state)]
}
KeyCode::Char('R') => {
state.modes.commit.message_editor.reset();
state.mark_dirty();
vec![]
}
KeyCode::Char('i') => {
state.modal = Some(Modal::Instructions {
input: state.modes.commit.custom_instructions.clone(),
});
state.mark_dirty();
vec![]
}
KeyCode::Char('g') => {
let emojis = state.get_emoji_list();
let selected = match &state.modes.commit.emoji_mode {
EmojiMode::None => 0,
EmojiMode::Auto => 1,
EmojiMode::Custom(emoji) => {
emojis.iter().position(|e| e.emoji == *emoji).unwrap_or(1)
}
};
state.modal = Some(Modal::EmojiSelector {
input: String::new(),
emojis,
selected,
scroll: 0,
});
state.mark_dirty();
vec![]
}
KeyCode::Char('E') => {
state.modes.commit.emoji_mode = match state.modes.commit.emoji_mode {
EmojiMode::None => EmojiMode::Auto,
_ => EmojiMode::None,
};
state.modes.commit.use_gitmoji = state.modes.commit.emoji_mode != EmojiMode::None;
let status = match &state.modes.commit.emoji_mode {
EmojiMode::None => "off",
EmojiMode::Auto => "auto",
EmojiMode::Custom(e) => e,
};
state.notify(crate::studio::state::Notification::info(format!(
"Emoji: {}",
status
)));
state.mark_dirty();
vec![]
}
KeyCode::Char('A') => {
state.modes.commit.amend_mode = !state.modes.commit.amend_mode;
if state.modes.commit.amend_mode {
if let Some(repo) = &state.repo
&& let Ok(msg) = repo.get_head_commit_message()
{
state.modes.commit.original_message = Some(msg);
}
state.notify(crate::studio::state::Notification::info(
"Amend mode: ON - will replace previous commit".to_string(),
));
} else {
state.modes.commit.original_message = None;
state.notify(crate::studio::state::Notification::info(
"Amend mode: OFF".to_string(),
));
}
state.modes.commit.messages.clear();
state.modes.commit.message_editor.clear();
state.mark_dirty();
vec![]
}
KeyCode::Enter => {
let message = state.modes.commit.message_editor.get_message();
if message.is_empty() {
vec![]
} else if state.modes.commit.amend_mode {
vec![SideEffect::ExecuteAmend { message }]
} else {
vec![SideEffect::ExecuteCommit { message }]
}
}
KeyCode::Right => {
state.modes.commit.message_editor.next_message();
state.modes.commit.current_index = state.modes.commit.message_editor.selected_index();
state.mark_dirty();
vec![]
}
KeyCode::Left => {
state.modes.commit.message_editor.prev_message();
state.modes.commit.current_index = state.modes.commit.message_editor.selected_index();
state.mark_dirty();
vec![]
}
KeyCode::Char('y') => {
let message = state.modes.commit.message_editor.get_message();
if !message.is_empty() {
copy_to_clipboard(state, &message, "Commit message");
}
vec![]
}
_ => vec![],
}
}