mod changelog;
mod commit;
mod explore;
mod modals;
mod pr;
mod release_notes;
mod review;
use arboard::Clipboard;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::studio::events::{AgentTask, ChatContext, DataType, SideEffect};
use crate::studio::state::{Modal, Mode, Notification, SettingsState, StudioState};
pub use changelog::handle_changelog_key;
pub use commit::handle_commit_key;
pub use explore::handle_explore_key;
pub use modals::handle_modal_key;
pub use pr::handle_pr_key;
pub use release_notes::handle_release_notes_key;
pub use review::handle_review_key;
pub fn handle_key_event(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
if state.modal.is_some() {
return handle_modal_key(state, key);
}
if let Some(effects) = handle_global_key(state, key) {
return effects;
}
match state.active_mode {
Mode::Explore => handle_explore_key(state, key),
Mode::Commit => handle_commit_key(state, key),
Mode::Review => handle_review_key(state, key),
Mode::PR => handle_pr_key(state, key),
Mode::Changelog => handle_changelog_key(state, key),
Mode::ReleaseNotes => handle_release_notes_key(state, key),
}
}
fn handle_global_key(state: &mut StudioState, key: KeyEvent) -> Option<Vec<SideEffect>> {
if matches_shift_char(&key, 'e') {
return Some(switch_mode(state, Mode::Explore));
}
if matches_shift_char(&key, 'c') {
return Some(switch_mode(state, Mode::Commit));
}
if matches_shift_char(&key, 'r') {
return Some(switch_mode(state, Mode::Review));
}
if matches_shift_char(&key, 'p') {
return Some(switch_mode(state, Mode::PR));
}
if matches_shift_char(&key, 'l') {
return Some(switch_mode(state, Mode::Changelog));
}
if matches_shift_char(&key, 'n') {
return Some(switch_mode(state, Mode::ReleaseNotes));
}
if matches_shift_char(&key, 's') {
state.modal = Some(Modal::Settings(Box::new(SettingsState::from_config(
&state.config,
))));
state.mark_dirty();
return Some(vec![]);
}
match key.code {
KeyCode::Char('q') if !is_editing(state) => Some(vec![SideEffect::Quit]),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
Some(vec![SideEffect::Quit])
}
KeyCode::Char('?') if !is_editing(state) => {
state.show_help();
Some(vec![])
}
KeyCode::Char('/') if !is_editing(state) => {
state.show_chat();
Some(vec![])
}
KeyCode::Tab => {
if key.modifiers.contains(KeyModifiers::SHIFT) {
state.focus_prev_panel();
} else {
state.focus_next_panel();
}
Some(vec![])
}
KeyCode::Esc => {
if state.modal.is_some() {
state.close_modal();
Some(vec![])
} else {
None
}
}
_ => None,
}
}
fn matches_shift_char(key: &KeyEvent, expected: char) -> bool {
*key == KeyEvent::new_with_kind_and_state(
KeyCode::Char(expected),
KeyModifiers::SHIFT,
key.kind,
key.state,
)
}
fn switch_mode(state: &mut StudioState, mode: Mode) -> Vec<SideEffect> {
if state.active_mode == mode {
return vec![];
}
state.switch_mode(mode);
match mode {
Mode::Commit => vec![SideEffect::LoadData {
data_type: DataType::CommitDiff,
from_ref: None,
to_ref: None,
}],
Mode::Review => vec![SideEffect::LoadData {
data_type: DataType::ReviewDiff,
from_ref: Some(state.modes.review.from_ref.clone()),
to_ref: Some(state.modes.review.to_ref.clone()),
}],
Mode::PR => vec![SideEffect::LoadData {
data_type: DataType::PRDiff,
from_ref: Some(state.modes.pr.base_branch.clone()),
to_ref: Some(state.modes.pr.to_ref.clone()),
}],
Mode::Changelog => vec![SideEffect::LoadData {
data_type: DataType::ChangelogCommits,
from_ref: Some(state.modes.changelog.from_ref.clone()),
to_ref: Some(state.modes.changelog.to_ref.clone()),
}],
Mode::ReleaseNotes => vec![SideEffect::LoadData {
data_type: DataType::ReleaseNotesCommits,
from_ref: Some(state.modes.release_notes.from_ref.clone()),
to_ref: Some(state.modes.release_notes.to_ref.clone()),
}],
Mode::Explore => {
if state.modes.explore.file_tree.is_empty() {
vec![SideEffect::LoadData {
data_type: DataType::ExploreFiles,
from_ref: None,
to_ref: None,
}]
} else {
vec![]
}
}
}
}
pub fn is_editing(state: &StudioState) -> bool {
match state.active_mode {
Mode::Commit => state.modes.commit.editing_message,
_ => false,
}
}
#[allow(dead_code)] pub fn get_keybindings(mode: Mode) -> Vec<(&'static str, &'static str)> {
let mut bindings = vec![
("q", "Quit"),
("?", "Help"),
("Tab", "Next panel"),
("S-Tab", "Previous panel"),
("/", "Search"),
("E", "Explore mode"),
("C", "Commit mode"),
];
match mode {
Mode::Explore => {
bindings.extend([
("j/k", "Navigate up/down"),
("h/l", "Collapse/expand"),
("g/G", "First/last"),
("Enter", "Open/select"),
("w", "Ask why"),
("H", "Toggle heat map"),
("o", "Copy editor command"),
]);
}
Mode::Commit => {
bindings.extend([
("j/k", "Navigate/scroll"),
("h/l", "Collapse/expand"),
("[/]", "Prev/next hunk"),
("n/p", "Cycle diff files"),
("Left/Right", "Cycle messages"),
("s", "Stage file"),
("u", "Unstage file"),
("a", "Stage all"),
("U", "Unstage all"),
("e", "Edit message"),
("r", "Regenerate"),
("R", "Reset message"),
("Enter", "Commit/select"),
]);
}
_ => {}
}
bindings
}
pub fn copy_to_clipboard(state: &mut StudioState, content: &str, description: &str) {
match Clipboard::new() {
Ok(mut clipboard) => match clipboard.set_text(content) {
Ok(()) => {
state.notify(Notification::success(format!(
"{description} copied to clipboard"
)));
}
Err(e) => {
state.notify(Notification::error(format!("Failed to copy: {e}")));
}
},
Err(e) => {
state.notify(Notification::error(format!("Clipboard unavailable: {e}")));
}
}
state.mark_dirty();
}
pub fn spawn_commit_task(state: &StudioState) -> SideEffect {
use crate::studio::state::EmojiMode;
SideEffect::SpawnAgent {
task: AgentTask::Commit {
instructions: if state.modes.commit.custom_instructions.is_empty() {
None
} else {
Some(state.modes.commit.custom_instructions.clone())
},
preset: state.modes.commit.preset.clone(),
use_gitmoji: state.modes.commit.emoji_mode != EmojiMode::None,
amend: state.modes.commit.amend_mode,
},
}
}
pub fn spawn_review_task(state: &StudioState) -> SideEffect {
SideEffect::SpawnAgent {
task: AgentTask::Review {
from_ref: state.modes.review.from_ref.clone(),
to_ref: state.modes.review.to_ref.clone(),
},
}
}
pub fn spawn_pr_task(state: &StudioState) -> SideEffect {
SideEffect::SpawnAgent {
task: AgentTask::PR {
base_branch: state.modes.pr.base_branch.clone(),
to_ref: state.modes.pr.to_ref.clone(),
},
}
}
pub fn spawn_changelog_task(state: &StudioState) -> SideEffect {
SideEffect::SpawnAgent {
task: AgentTask::Changelog {
from_ref: state.modes.changelog.from_ref.clone(),
to_ref: state.modes.changelog.to_ref.clone(),
},
}
}
pub fn spawn_release_notes_task(state: &StudioState) -> SideEffect {
SideEffect::SpawnAgent {
task: AgentTask::ReleaseNotes {
from_ref: state.modes.release_notes.from_ref.clone(),
to_ref: state.modes.release_notes.to_ref.clone(),
},
}
}
pub fn spawn_chat_task(message: String, mode: Mode) -> SideEffect {
SideEffect::SpawnAgent {
task: AgentTask::Chat {
message,
context: ChatContext {
mode,
..Default::default()
},
},
}
}
pub fn reload_pr_data(state: &StudioState) -> SideEffect {
SideEffect::LoadData {
data_type: DataType::PRDiff,
from_ref: Some(state.modes.pr.base_branch.clone()),
to_ref: Some(state.modes.pr.to_ref.clone()),
}
}
pub fn reload_review_data(state: &StudioState) -> SideEffect {
SideEffect::LoadData {
data_type: DataType::ReviewDiff,
from_ref: Some(state.modes.review.from_ref.clone()),
to_ref: Some(state.modes.review.to_ref.clone()),
}
}
pub fn reload_changelog_data(state: &StudioState) -> SideEffect {
SideEffect::LoadData {
data_type: DataType::ChangelogCommits,
from_ref: Some(state.modes.changelog.from_ref.clone()),
to_ref: Some(state.modes.changelog.to_ref.clone()),
}
}
pub fn reload_release_notes_data(state: &StudioState) -> SideEffect {
SideEffect::LoadData {
data_type: DataType::ReleaseNotesCommits,
from_ref: Some(state.modes.release_notes.from_ref.clone()),
to_ref: Some(state.modes.release_notes.to_ref.clone()),
}
}