use crate::ai_command_runner::AiCommandRunner;
use crate::config::OrchestratorConfig;
use crate::error::Result;
use crate::tui::events::{LogEntry, OrchestratorEvent, TuiCommand};
use crate::tui::state::AppState;
use crate::tui::types::{AppMode, StopMode};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::DefaultTerminal;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::debug;
use super::terminal::{execute_worktree_command, suspend_terminal_and_execute_sync};
use super::worktrees::load_worktrees_with_conflict_check;
pub struct KeyEventContext<'a> {
pub app: &'a mut AppState,
pub terminal: &'a mut DefaultTerminal,
pub repo_root: &'a Path,
pub config: &'a OrchestratorConfig,
pub worktree_base_dir: &'a Path,
pub tx: &'a mpsc::Sender<OrchestratorEvent>,
pub cmd_tx: &'a mpsc::Sender<TuiCommand>,
pub ai_runner: &'a AiCommandRunner,
pub graceful_stop_flag: &'a Arc<AtomicBool>,
pub orchestrator_cancel: &'a Option<CancellationToken>,
pub orchestrator_handle: &'a Option<tokio::task::JoinHandle<Result<()>>>,
}
pub async fn handle_tab_key(ctx: &mut KeyEventContext<'_>) -> Result<()> {
use crate::tui::types::ViewMode;
let new_view = match ctx.app.view_mode {
ViewMode::Changes => ViewMode::Worktrees,
ViewMode::Worktrees => ViewMode::Changes,
};
if new_view == ViewMode::Worktrees {
let load_tx = ctx.tx.clone();
let load_repo_root = ctx.repo_root.to_path_buf();
tokio::spawn(async move {
match load_worktrees_with_conflict_check(&load_repo_root).await {
Ok(worktrees) => {
let _ = load_tx
.send(OrchestratorEvent::WorktreesRefreshed { worktrees })
.await;
}
Err(e) => {
let _ = load_tx
.send(OrchestratorEvent::Log(LogEntry::error(format!(
"Failed to load worktrees: {}",
e
))))
.await;
}
}
});
}
ctx.app.view_mode = new_view;
Ok(())
}
pub fn handle_cursor_movement(app: &mut AppState, is_up: bool) {
use crate::tui::types::ViewMode;
match app.view_mode {
ViewMode::Changes => {
if is_up {
app.cursor_up()
} else {
app.cursor_down()
}
}
ViewMode::Worktrees => {
if is_up {
app.worktree_cursor_up()
} else {
app.worktree_cursor_down()
}
}
}
}
pub async fn handle_editor_launch(ctx: &mut KeyEventContext<'_>) -> Result<()> {
use crate::tui::types::ViewMode;
let view_mode = ctx.app.view_mode;
let change_id = if !ctx.app.changes.is_empty() && ctx.app.cursor_index < ctx.app.changes.len() {
Some(ctx.app.changes[ctx.app.cursor_index].id.clone())
} else {
None
};
let worktree_path = ctx.app.get_selected_worktree_path();
suspend_terminal_and_execute_sync(ctx.terminal, || {
match view_mode {
ViewMode::Changes => {
if let Some(id) = change_id {
if let Err(e) = crate::tui::utils::launch_editor_for_change(&id) {
eprintln!("Failed to launch editor: {}", e);
}
}
}
ViewMode::Worktrees => {
if let Some(path) = worktree_path {
if let Err(e) = crate::tui::utils::launch_editor_in_dir(&path) {
eprintln!("Failed to launch editor: {}", e);
}
}
}
}
Ok(())
})
}
pub async fn handle_merge_key(ctx: &mut KeyEventContext<'_>) -> Result<()> {
use crate::tui::types::ViewMode;
debug!("M key pressed: view_mode={:?}", ctx.app.view_mode);
match ctx.app.view_mode {
ViewMode::Changes => {
debug!("M key (Changes view): attempting resolve_merge");
if let Some(cmd) = ctx.app.resolve_merge() {
debug!("M key (Changes view): sending command {:?}", cmd);
let _ = ctx.cmd_tx.send(cmd).await;
} else {
debug!("M key (Changes view): resolve_merge returned None");
}
}
ViewMode::Worktrees => {
debug!("M key (Worktrees view): attempting request_merge_worktree_branch");
if let Some(cmd) = ctx.app.request_merge_worktree_branch() {
debug!("M key (Worktrees view): sending command {:?}", cmd);
let _ = ctx.cmd_tx.send(cmd).await;
} else {
debug!("M key (Worktrees view): request_merge_worktree_branch returned None");
}
}
}
Ok(())
}
pub fn handle_esc_key(ctx: &mut KeyEventContext<'_>) {
match ctx.app.mode {
AppMode::Running => {
ctx.app.stop_mode = StopMode::GracefulPending;
ctx.graceful_stop_flag.store(true, Ordering::SeqCst);
ctx.app.mode = AppMode::Stopping;
ctx.app
.add_log(LogEntry::warn("Stopping after current change completes..."));
}
AppMode::Stopping => {
ctx.app.stop_mode = StopMode::ForceStopped;
if let Some(cancel) = ctx.orchestrator_cancel {
cancel.cancel();
}
ctx.app
.handle_orchestrator_event(OrchestratorEvent::Stopped);
ctx.app.current_change = None;
ctx.app.add_log(LogEntry::warn("Force stopped"));
}
_ => {}
}
}
pub fn handle_f5_key(ctx: &mut KeyEventContext<'_>) -> Option<TuiCommand> {
if ctx.app.mode == AppMode::Stopping {
if ctx
.orchestrator_handle
.as_ref()
.is_some_and(|h| !h.is_finished())
{
ctx.graceful_stop_flag.store(false, Ordering::SeqCst);
ctx.app.stop_mode = StopMode::None;
ctx.app.mode = AppMode::Running;
ctx.app
.add_log(LogEntry::info("Stop canceled, continuing..."));
} else {
ctx.app.add_log(LogEntry::warn(
"Cannot cancel stop: processing already completed",
));
}
return None;
}
if !ctx.app.changes.is_empty() && ctx.app.cursor_index < ctx.app.changes.len() {
let change = &ctx.app.changes[ctx.app.cursor_index];
if change.display_status_cache == "merge wait" {
return ctx.app.resolve_merge();
}
}
if ctx.app.is_resolving {
ctx.app.warning_message =
Some("Cannot start processing while merge resolve is in progress".to_string());
return None;
}
if ctx.app.mode == AppMode::Error {
ctx.app.retry_error_changes()
} else if ctx.app.mode == AppMode::Stopped {
ctx.app.resume_processing()
} else {
ctx.app.start_processing()
}
}
pub async fn handle_enter_key(ctx: &mut KeyEventContext<'_>) -> Result<()> {
use crate::tui::types::ViewMode;
if ctx.app.view_mode != ViewMode::Worktrees {
ctx.app
.add_log(LogEntry::warn("Enter ignored: not in Worktrees view"));
return Ok(());
}
let Some(worktree_path_str) = ctx.app.get_selected_worktree_path() else {
ctx.app
.add_log(LogEntry::warn("Enter ignored: no worktree selected"));
return Ok(());
};
let Some(template) = ctx.config.get_worktree_command().map(str::to_string) else {
ctx.app.add_log(LogEntry::warn(
"Enter ignored: worktree_command not configured",
));
return Ok(());
};
let Some(repo_root_str) = ctx.repo_root.to_str() else {
ctx.app.add_log(LogEntry::error(
"Failed to resolve repo root path".to_string(),
));
return Ok(());
};
let command =
OrchestratorConfig::expand_worktree_command(&template, &worktree_path_str, repo_root_str);
ctx.app.add_log(LogEntry::info(format!(
"Running worktree command in {}",
worktree_path_str
)));
let worktree_path = Path::new(&worktree_path_str);
execute_worktree_command(
ctx.terminal,
&command,
worktree_path,
ctx.ai_runner,
ctx.app,
)
.await
}
pub async fn handle_plus_key(ctx: &mut KeyEventContext<'_>) -> Result<()> {
use crate::tui::types::ViewMode;
if ctx.app.view_mode != ViewMode::Worktrees {
return Ok(());
}
let Some(template) = ctx.config.get_worktree_command().map(str::to_string) else {
return Ok(());
};
let is_git_repo = match crate::vcs::git::commands::check_git_repo(ctx.repo_root).await {
Ok(is_repo) => is_repo,
Err(err) => {
ctx.app.add_log(LogEntry::error(format!(
"Failed to check git repo: {}",
err
)));
return Ok(());
}
};
if !super::worktrees::should_trigger_worktree_command(ctx.config, is_git_repo) {
return Ok(());
}
if let Err(err) = std::fs::create_dir_all(ctx.worktree_base_dir) {
ctx.app.add_log(LogEntry::error(format!(
"Failed to prepare worktree base dir: {}",
err
)));
return Ok(());
}
let worktree_path = super::worktrees::build_worktree_path(ctx.worktree_base_dir);
let Some(worktree_path_str) = worktree_path.to_str() else {
ctx.app.add_log(LogEntry::error(
"Failed to resolve worktree path".to_string(),
));
return Ok(());
};
let Some(repo_root_str) = ctx.repo_root.to_str() else {
ctx.app.add_log(LogEntry::error(
"Failed to resolve repo root path".to_string(),
));
return Ok(());
};
let branch_name = match crate::vcs::git::commands::generate_unique_branch_name(
ctx.repo_root,
"ws-session",
10,
)
.await
{
Ok(name) => name,
Err(err) => {
ctx.app.add_log(LogEntry::error(format!(
"Failed to generate unique branch name: {}",
err
)));
return Ok(());
}
};
if let Err(err) = crate::vcs::git::commands::worktree_add(
ctx.repo_root,
worktree_path_str,
&branch_name,
"HEAD",
)
.await
{
ctx.app.add_log(LogEntry::error(format!(
"Failed to create worktree: {}",
err
)));
return Ok(());
}
ctx.app.add_log(LogEntry::info(format!(
"Created worktree with branch '{}'",
branch_name
)));
if let Err(err) =
crate::vcs::git::commands::run_worktree_setup(ctx.repo_root, &worktree_path).await
{
ctx.app.add_log(LogEntry::error(format!(
"Failed to run worktree setup: {}",
err
)));
if let Err(cleanup_err) =
crate::vcs::git::commands::worktree_remove(ctx.repo_root, worktree_path_str).await
{
ctx.app.add_log(LogEntry::error(format!(
"Failed to cleanup worktree after setup failure: {}",
cleanup_err
)));
}
return Ok(());
}
let command =
OrchestratorConfig::expand_worktree_command(&template, worktree_path_str, repo_root_str);
ctx.app.add_log(LogEntry::info(format!(
"Running worktree command in {}",
worktree_path_str
)));
execute_worktree_command(
ctx.terminal,
&command,
&worktree_path,
ctx.ai_runner,
ctx.app,
)
.await
}
pub async fn handle_key_event(
key: KeyEvent,
ctx: &mut KeyEventContext<'_>,
) -> Result<Option<TuiCommand>> {
let had_warning_message = ctx.app.warning_message.is_some();
let had_warning_popup = ctx.app.warning_popup.is_some();
if ctx.app.mode == AppMode::QrPopup {
ctx.app.hide_qr_popup();
return Ok(None);
}
if let AppMode::ConfirmForceKill { ref change_id } = ctx.app.mode {
let cid = change_id.clone();
match (key.code, key.modifiers) {
(KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => {
ctx.app.mode = AppMode::Running;
ctx.app
.add_log(LogEntry::info(format!("Force-kill confirmed: {}", cid)));
let _ = ctx.cmd_tx.send(TuiCommand::DequeueChange(cid)).await;
}
(KeyCode::Char('n'), _) | (KeyCode::Char('N'), _) | (KeyCode::Esc, _) => {
ctx.app.mode = AppMode::Running;
ctx.app
.add_log(LogEntry::info("Force-kill canceled".to_string()));
}
_ => {}
}
return Ok(None);
}
if ctx.app.mode == AppMode::ConfirmWorktreeDelete {
match (key.code, key.modifiers) {
(KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => {
if let Some(cmd) = ctx.app.confirm_worktree_action_delete() {
let _ = ctx.cmd_tx.send(cmd).await;
}
}
(KeyCode::Char('n'), _) | (KeyCode::Char('N'), _) | (KeyCode::Esc, _) => {
ctx.app.cancel_worktree_action();
}
_ => {}
}
return Ok(None);
}
let mut cmd_to_start: Option<TuiCommand> = None;
match (key.code, key.modifiers) {
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
ctx.app.should_quit = true;
}
(KeyCode::Tab, _) => {
handle_tab_key(ctx).await?;
}
(KeyCode::Up, _) | (KeyCode::Char('k'), _) => {
handle_cursor_movement(ctx.app, true);
}
(KeyCode::Down, _) | (KeyCode::Char('j'), _) => {
handle_cursor_movement(ctx.app, false);
}
(KeyCode::Char(' '), _) => {
if let Some(cmd) = ctx.app.toggle_selection() {
let _ = ctx.cmd_tx.send(cmd).await;
}
}
(KeyCode::Char('x'), _) => {
use crate::tui::types::ViewMode;
if ctx.app.view_mode == ViewMode::Changes {
let commands = ctx.app.toggle_all_marks();
for cmd in commands {
let _ = ctx.cmd_tx.send(cmd).await;
}
}
}
(KeyCode::Char('e'), _) => {
handle_editor_launch(ctx).await?;
}
(KeyCode::Char('m'), _) | (KeyCode::Char('M'), _) => {
handle_merge_key(ctx).await?;
}
(KeyCode::Char('d'), _) | (KeyCode::Char('D'), _) => {
use crate::tui::types::ViewMode;
if ctx.app.view_mode == ViewMode::Worktrees {
ctx.app.request_worktree_delete_from_list();
}
}
(KeyCode::Esc, _) => {
handle_esc_key(ctx);
}
(KeyCode::F(5), _) => {
cmd_to_start = handle_f5_key(ctx);
}
(KeyCode::PageUp, _) => {
ctx.app.scroll_logs_up(5);
}
(KeyCode::PageDown, _) => {
ctx.app.scroll_logs_down(5);
}
(KeyCode::Home, _) => {
ctx.app.scroll_logs_to_top();
}
(KeyCode::End, _) => {
ctx.app.scroll_logs_to_bottom();
}
(KeyCode::Char('='), _) => {
ctx.app.toggle_parallel_mode();
}
(KeyCode::Enter, _) => {
handle_enter_key(ctx).await?;
}
(KeyCode::Char('+'), _) => {
handle_plus_key(ctx).await?;
}
(KeyCode::Char('w'), _) if ctx.app.web_url.is_some() => {
ctx.app.show_qr_popup();
}
(KeyCode::Char('l'), _) => {
use crate::tui::types::ViewMode;
if ctx.app.view_mode == ViewMode::Changes {
ctx.app.toggle_logs_panel();
}
}
(KeyCode::Char('K'), _) => {
use crate::tui::types::ViewMode;
if ctx.app.view_mode == ViewMode::Changes
&& ctx.app.mode == AppMode::Running
&& !ctx.app.changes.is_empty()
&& ctx.app.cursor_index < ctx.app.changes.len()
{
let change = &ctx.app.changes[ctx.app.cursor_index];
if matches!(
change.display_status_cache.as_str(),
"applying" | "accepting" | "archiving" | "resolving"
) {
let cid = change.id.clone();
ctx.app.mode = AppMode::ConfirmForceKill {
change_id: cid.clone(),
};
ctx.app.add_log(LogEntry::warn(format!(
"Confirm force-kill for '{}': press Y to confirm, N/Esc to cancel",
cid
)));
}
}
}
_ => {}
}
if had_warning_message || had_warning_popup {
ctx.app.warning_message = None;
ctx.app.warning_popup = None;
}
Ok(cmd_to_start)
}