a73x

src/tui/events.rs

Ref:   Size: 13.0 KiB

use std::io::{self, stdout};
use std::time::Duration;

use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{self, LeaveAlternateScreen};
use crossterm::ExecutableCommand;
use git2::Repository;
use ratatui::prelude::*;
use ratatui::widgets::ListState;

use crate::error::Error;
use crate::issue as issue_mod;
use crate::patch as patch_mod;

use super::state::{App, InputMode, KeyAction, ViewMode};
use super::widgets::ui;

pub(crate) fn run_loop(
    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
    app: &mut App,
    repo: &Repository,
) -> Result<(), Error> {
    loop {
        terminal.draw(|frame| ui(frame, app, Some(repo)))?;

        if event::poll(Duration::from_millis(100))? {
            if let Event::Key(key) = event::read()? {
                app.status_msg = None; // clear status on any keypress

                // Input mode intercept — handle keys before normal bindings
                match app.input_mode {
                    InputMode::Search => {
                        match key.code {
                            KeyCode::Esc => {
                                app.input_mode = InputMode::Normal;
                                app.search_query.clear();
                                let count = app.visible_count();
                                app.list_state
                                    .select(if count > 0 { Some(0) } else { None });
                            }
                            KeyCode::Backspace => {
                                app.search_query.pop();
                                let count = app.visible_count();
                                app.list_state
                                    .select(if count > 0 { Some(0) } else { None });
                            }
                            KeyCode::Char(c) => {
                                app.search_query.push(c);
                                let count = app.visible_count();
                                app.list_state
                                    .select(if count > 0 { Some(0) } else { None });
                            }
                            _ => {}
                        }
                        continue;
                    }
                    InputMode::CreateTitle => {
                        match key.code {
                            KeyCode::Esc => {
                                app.input_mode = InputMode::Normal;
                                app.input_buf.clear();
                            }
                            KeyCode::Enter => {
                                let title = app.input_buf.trim().to_string();
                                if title.is_empty() {
                                    app.input_mode = InputMode::Normal;
                                    app.input_buf.clear();
                                } else {
                                    app.create_title = title;
                                    app.input_buf.clear();
                                    app.input_mode = InputMode::CreateBody;
                                }
                            }
                            KeyCode::Backspace => {
                                app.input_buf.pop();
                            }
                            KeyCode::Char(c) => {
                                app.input_buf.push(c);
                            }
                            _ => {}
                        }
                        continue;
                    }
                    InputMode::CreateBody => {
                        match key.code {
                            KeyCode::Esc => {
                                // Submit with title only, no body
                                let title = app.create_title.clone();
                                match issue_mod::open(repo, &title, "", None) {
                                    Ok(id) => {
                                        app.reload(repo);
                                        app.status_msg =
                                            Some(format!("Issue created: {:.8}", id));
                                    }
                                    Err(e) => {
                                        app.status_msg =
                                            Some(format!("Error creating issue: {}", e));
                                    }
                                }
                                app.input_mode = InputMode::Normal;
                                app.input_buf.clear();
                                app.create_title.clear();
                            }
                            KeyCode::Enter => {
                                let title = app.create_title.clone();
                                let body = app.input_buf.clone();
                                match issue_mod::open(repo, &title, &body, None) {
                                    Ok(id) => {
                                        app.reload(repo);
                                        app.status_msg =
                                            Some(format!("Issue created: {:.8}", id));
                                    }
                                    Err(e) => {
                                        app.status_msg =
                                            Some(format!("Error creating issue: {}", e));
                                    }
                                }
                                app.input_mode = InputMode::Normal;
                                app.input_buf.clear();
                                app.create_title.clear();
                            }
                            KeyCode::Backspace => {
                                app.input_buf.pop();
                            }
                            KeyCode::Char(c) => {
                                app.input_buf.push(c);
                            }
                            _ => {}
                        }
                        continue;
                    }
                    InputMode::Normal => {}
                }

                // Handle keys that need repo access or are run_loop-specific
                // before delegating to handle_key
                if app.mode == ViewMode::Details {
                    match key.code {
                        KeyCode::Char('/') => {
                            app.input_mode = InputMode::Search;
                            app.search_query.clear();
                            continue;
                        }
                        KeyCode::Char('n') => {
                            app.input_mode = InputMode::CreateTitle;
                            app.input_buf.clear();
                            app.create_title.clear();
                            continue;
                        }
                        KeyCode::Char('o') => {
                            // Check out the relevant commit for local browsing
                            let checkout_target = {
                                let visible = app.visible_issues();
                                app.list_state
                                    .selected()
                                    .and_then(|idx| visible.get(idx))
                                    .and_then(|issue| {
                                        // Try linked patch first
                                        app.patches
                                            .iter()
                                            .find(|p| p.fixes.as_deref() == Some(&issue.id))
                                            .map(|p| p.branch.clone())
                                            // Fall back to closing commit
                                            .or_else(|| {
                                                issue.closed_by.map(|oid| oid.to_string())
                                            })
                                    })
                            };
                            if let Some(head) = checkout_target {
                                // Exit TUI, checkout, and return
                                terminal::disable_raw_mode()?;
                                stdout().execute(LeaveAlternateScreen)?;
                                let status = std::process::Command::new("git")
                                    .args(["checkout", &head])
                                    .status();
                                match status {
                                    Ok(s) if s.success() => {
                                        println!("Checked out commit: {:.8}", head);
                                        println!("Use 'git checkout -' to return.");
                                    }
                                    Ok(s) => {
                                        eprintln!("git checkout exited with {}", s);
                                    }
                                    Err(e) => {
                                        eprintln!("Failed to run git checkout: {}", e);
                                    }
                                }
                                return Ok(());
                            } else {
                                app.status_msg =
                                    Some("No linked patch to check out".to_string());
                            }
                            continue;
                        }
                        _ => {}
                    }
                }

                match app.handle_key(key.code, key.modifiers) {
                    KeyAction::Quit => return Ok(()),
                    KeyAction::Reload if app.mode == ViewMode::PatchDetail => {
                        // Regenerate diff for current revision/interdiff state
                        regenerate_patch_diff(app, repo);
                    }
                    KeyAction::Reload => app.reload(repo),
                    KeyAction::OpenPatchDetail => {
                        if let Some(patch) = app.linked_patch_for_selected().cloned() {
                            open_patch_detail(app, repo, patch);
                        }
                    }
                    KeyAction::OpenPatchDetailDirect(idx) => {
                        let patch = app.visible_patches().get(idx).cloned().cloned();
                        if let Some(patch) = patch {
                            open_patch_detail(app, repo, patch);
                        }
                    }
                    KeyAction::OpenCommitBrowser => {
                        if let Some(ref_name) = app.selected_ref_name() {
                            match crate::dag::walk_events(repo, &ref_name) {
                                Ok(events) => {
                                    app.event_history = events;
                                    app.event_list_state = ListState::default();
                                    if !app.event_history.is_empty() {
                                        app.event_list_state.select(Some(0));
                                    }
                                    app.mode = ViewMode::CommitList;
                                    app.scroll = 0;
                                }
                                Err(e) => {
                                    app.status_msg =
                                        Some(format!("Error loading events: {}", e));
                                }
                            }
                        }
                    }
                    KeyAction::Continue => {}
                }
            }
        }
    }
}

fn generate_patch_diff_for(
    repo: &Repository,
    patch: &crate::state::PatchState,
    rev_idx: usize,
    interdiff_mode: bool,
) -> String {
    if patch.revisions.is_empty() {
        return "(no revisions)".to_string();
    }

    let rev = &patch.revisions[rev_idx];

    if interdiff_mode && rev_idx > 0 {
        let from_rev = patch.revisions[rev_idx - 1].number;
        let to_rev = rev.number;
        match patch_mod::interdiff(repo, patch, from_rev, to_rev) {
            Ok(d) => d,
            Err(e) => format!("(error generating interdiff: {})", e),
        }
    } else {
        // Diff at specific revision vs base
        match patch_mod::diff(repo, &patch.id, Some(rev.number), None) {
            Ok(d) => d,
            Err(e) => format!("(error generating diff: {})", e),
        }
    }
}

fn open_patch_detail(app: &mut App, repo: &Repository, patch: crate::state::PatchState) {
    let warning = crate::staleness_warning(repo, &patch);
    app.patch_revision_idx = patch.revisions.len().saturating_sub(1);
    app.patch_interdiff_mode = false;
    app.patch_scroll = 0;

    let diff = generate_patch_diff_for(repo, &patch, app.patch_revision_idx, false);
    app.patch_diff = diff;
    app.current_patch = Some(patch);
    app.mode = ViewMode::PatchDetail;

    if let Some(w) = warning {
        app.status_msg = Some(w);
    }
}

fn regenerate_patch_diff(app: &mut App, repo: &Repository) {
    if let Some(ref patch) = app.current_patch {
        let diff = generate_patch_diff_for(
            repo,
            patch,
            app.patch_revision_idx,
            app.patch_interdiff_mode,
        );
        app.patch_diff = diff;
    }
}