mod actions;
mod key_handlers;
use std::io;
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver};
use std::thread;
use std::time::{Duration, Instant, SystemTime};
use anyhow::Result;
use crossterm::{
event::{
self as crossterm_event, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind,
MouseButton, MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use git2::Repository;
use ratatui::{backend::CrosstermBackend, Terminal};
use gitstack::{
calculate_file_heatmap, calculate_project_health, classify_intent, detect_sessions,
fetch_remote_at_path, get_commit_diff, get_head_hash_cached, get_index_mtime_cached,
get_repo_info_cached, get_status_cached, get_working_file_diff, list_branches_cached,
load_events, load_events_fast, parse_cli_args, parse_tui_options, pull, push, run_cli_mode,
tui, ActivityTimeline, App, BlameLine, ChangeCouplingAnalysis, CliCommand, CodeOwnership,
CommitDiff, CommitImpactAnalysis, CommitQualityAnalysis, GitEvent, InputMode, ProjectHealth,
SidebarPanel, TuiFocusTarget, TuiOptions,
};
use key_handlers::{
handle_blame_view_keys, handle_branch_compare_keys, handle_branch_create_keys,
handle_branch_select_keys, handle_change_coupling_keys, handle_commit_input_keys,
handle_detail_keys, handle_file_history_keys, handle_filter_keys, handle_handoff_view_keys,
handle_heatmap_view_keys, handle_help_keys, handle_impact_score_keys,
handle_next_actions_view_keys, handle_normal_keys, handle_ownership_view_keys,
handle_patch_view_keys, handle_pr_create_keys, handle_preset_save_keys,
handle_quality_score_keys, handle_quick_action_keys, handle_related_files_keys,
handle_review_pack_view_keys, handle_review_queue_keys, handle_stash_view_keys,
handle_stats_view_keys, handle_status_view_keys, handle_timeline_view_keys,
handle_topology_view_keys,
};
const INITIAL_LOAD_COUNT: usize = 10;
const MAX_LOAD_COUNT: usize = 2000;
pub(crate) struct RemoteFetchState {
result_rx: Option<Receiver<Result<(), String>>>,
quiet_mode: bool,
last_fetch: Instant,
}
impl RemoteFetchState {
fn new() -> Self {
Self {
result_rx: None,
quiet_mode: false,
last_fetch: Instant::now(),
}
}
fn is_running(&self) -> bool {
self.result_rx.is_some()
}
fn start(&mut self, repo_path: PathBuf, quiet: bool) {
if self.is_running() {
return; }
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = fetch_remote_at_path(&repo_path).map_err(|e| e.to_string());
let _ = tx.send(result);
});
self.result_rx = Some(rx);
self.quiet_mode = quiet;
}
fn check_result(&mut self) -> Option<Result<(), String>> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(result) => {
self.result_rx = None;
self.last_fetch = Instant::now();
Some(result)
}
Err(mpsc::TryRecvError::Empty) => None, Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
Some(Err("Fetch thread crashed".to_string()))
}
}
}
}
pub(crate) struct BackgroundLoadState {
result_rx: Option<Receiver<Vec<GitEvent>>>,
}
impl BackgroundLoadState {
fn new() -> Self {
Self { result_rx: None }
}
fn is_running(&self) -> bool {
self.result_rx.is_some()
}
pub(crate) fn start(&mut self, skip: usize, limit: usize) {
if self.is_running() {
return;
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
if let Ok(all_events) = load_events(skip + limit) {
let additional: Vec<GitEvent> = all_events.into_iter().skip(skip).collect();
let _ = tx.send(additional);
} else {
let _ = tx.send(Vec::new());
}
});
self.result_rx = Some(rx);
}
fn check_result(&mut self) -> Option<Vec<GitEvent>> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(events) => {
self.result_rx = None;
Some(events)
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
Some(Vec::new())
}
}
}
}
struct BackgroundHealthState {
result_rx: Option<Receiver<ProjectHealth>>,
}
impl BackgroundHealthState {
fn new() -> Self {
Self { result_rx: None }
}
fn is_running(&self) -> bool {
self.result_rx.is_some()
}
fn start(&mut self, events: Vec<GitEvent>) {
if self.is_running() {
return;
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let repo = match git2::Repository::discover(".") {
Ok(r) => r,
Err(_) => return,
};
let mut file_cache: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let event_refs: Vec<&GitEvent> = events.iter().collect();
for event in &event_refs {
if !file_cache.contains_key(&event.short_hash) {
if let Ok(files) =
gitstack::get_commit_files_from_repo(&repo, &event.short_hash)
{
file_cache.insert(event.short_hash.clone(), files);
}
}
}
let file_cache_ref = &file_cache;
let heatmap =
calculate_file_heatmap(&event_refs, |hash| file_cache_ref.get(hash).cloned());
let health = calculate_project_health(
&event_refs,
|hash| file_cache_ref.get(hash).cloned(),
None,
None,
None,
&heatmap,
);
let _ = tx.send(health);
});
self.result_rx = Some(rx);
}
fn check_result(&mut self) -> Option<ProjectHealth> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(health) => {
self.result_rx = None;
Some(health)
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
None
}
}
}
}
struct BackgroundRiskState {
result_rx: Option<Receiver<(f64, gitstack::RiskLevel)>>,
}
impl BackgroundRiskState {
fn new() -> Self {
Self { result_rx: None }
}
fn is_running(&self) -> bool {
self.result_rx.is_some()
}
fn start(&mut self, events: Vec<GitEvent>, statuses: Vec<gitstack::git::FileStatus>) {
if self.is_running() {
return;
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let repo = match git2::Repository::discover(".") {
Ok(r) => r,
Err(_) => return,
};
let event_refs: Vec<&GitEvent> = events.iter().collect();
let mut file_cache: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for event in &event_refs {
if !file_cache.contains_key(&event.short_hash) {
if let Ok(files) =
gitstack::get_commit_files_from_repo(&repo, &event.short_hash)
{
file_cache.insert(event.short_hash.clone(), files);
}
}
}
let file_cache_ref = &file_cache;
let heatmap =
calculate_file_heatmap(&event_refs, |hash| file_cache_ref.get(hash).cloned());
let ownership = gitstack::stats::CodeOwnership {
entries: vec![],
total_files: 0,
};
let result = gitstack::calculate_staged_risk(&statuses, &heatmap, &ownership);
let _ = tx.send(result);
});
self.result_rx = Some(rx);
}
fn check_result(&mut self) -> Option<(f64, gitstack::RiskLevel)> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(result) => {
self.result_rx = None;
Some(result)
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
None
}
}
}
}
pub(crate) struct BackgroundDiffState {
result_rx: Option<Receiver<(String, CommitDiff)>>,
pending_hash: Option<String>,
}
impl BackgroundDiffState {
fn new() -> Self {
Self {
result_rx: None,
pending_hash: None,
}
}
pub(crate) fn is_running(&self) -> bool {
self.result_rx.is_some()
}
pub(crate) fn start(&mut self, hash: String) {
if self.is_running() {
return;
}
let hash_clone = hash.clone();
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
if let Ok(diff) = get_commit_diff(&hash_clone) {
let _ = tx.send((hash_clone, diff));
}
});
self.pending_hash = Some(hash);
self.result_rx = Some(rx);
}
fn check_result(&mut self) -> Option<(String, CommitDiff)> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(result) => {
self.result_rx = None;
self.pending_hash = None;
Some(result)
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
self.pending_hash = None;
None
}
}
}
}
pub(crate) enum GitOp {
Pull,
Push,
}
pub(crate) struct BackgroundGitOpState {
result_rx: Option<Receiver<Result<(), String>>>,
operation: Option<GitOp>,
}
impl BackgroundGitOpState {
fn new() -> Self {
Self {
result_rx: None,
operation: None,
}
}
pub(crate) fn is_running(&self) -> bool {
self.result_rx.is_some()
}
pub(crate) fn start_pull(&mut self) {
if self.is_running() {
return;
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = pull().map_err(|e| e.to_string());
let _ = tx.send(result);
});
self.result_rx = Some(rx);
self.operation = Some(GitOp::Pull);
}
pub(crate) fn start_push(&mut self) {
if self.is_running() {
return;
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = push().map_err(|e| e.to_string());
let _ = tx.send(result);
});
self.result_rx = Some(rx);
self.operation = Some(GitOp::Push);
}
fn check_result(&mut self) -> Option<(GitOp, Result<(), String>)> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(result) => {
let op = self.operation.take().unwrap_or(GitOp::Pull);
self.result_rx = None;
Some((op, result))
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
let op = self.operation.take().unwrap_or(GitOp::Pull);
self.result_rx = None;
Some((op, Err("Git operation thread crashed".to_string())))
}
}
}
}
pub(crate) enum AnalysisResult {
Timeline(Box<ActivityTimeline>),
Ownership(CodeOwnership),
ImpactScore(CommitImpactAnalysis),
ChangeCoupling(ChangeCouplingAnalysis),
QualityScore(CommitQualityAnalysis),
}
pub(crate) struct BackgroundAnalysisState {
pub(crate) result_rx: Option<Receiver<AnalysisResult>>,
}
impl BackgroundAnalysisState {
fn new() -> Self {
Self { result_rx: None }
}
pub(crate) fn is_running(&self) -> bool {
self.result_rx.is_some()
}
fn check_result(&mut self) -> Option<AnalysisResult> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(result) => {
self.result_rx = None;
Some(result)
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
None
}
}
}
}
pub(crate) struct BackgroundBlameState {
result_rx: Option<Receiver<(String, Vec<BlameLine>)>>,
}
impl BackgroundBlameState {
fn new() -> Self {
Self { result_rx: None }
}
pub(crate) fn is_running(&self) -> bool {
self.result_rx.is_some()
}
pub(crate) fn start(&mut self, path: String) {
if self.is_running() {
return;
}
let path_clone = path.clone();
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
if let Ok(blame) = gitstack::get_blame(&path_clone) {
let _ = tx.send((path_clone, blame));
}
});
self.result_rx = Some(rx);
}
fn check_result(&mut self) -> Option<(String, Vec<BlameLine>)> {
let rx = self.result_rx.as_ref()?;
match rx.try_recv() {
Ok(result) => {
self.result_rx = None;
Some(result)
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.result_rx = None;
None
}
}
}
}
struct BackgroundStates {
load: BackgroundLoadState,
health: BackgroundHealthState,
risk: BackgroundRiskState,
diff: BackgroundDiffState,
git_op: BackgroundGitOpState,
analysis: BackgroundAnalysisState,
blame: BackgroundBlameState,
}
fn check_input(timeout_ms: u64) -> Result<Option<Event>> {
if crossterm_event::poll(Duration::from_millis(timeout_ms))? {
Ok(Some(crossterm_event::read()?))
} else {
Ok(None)
}
}
fn apply_tui_options(app: &mut App, options: &TuiOptions) {
if let Some(TuiFocusTarget::Files) = options.focus {
app.input_mode = InputMode::StatusView;
}
}
fn apply_post_repo_focus(app: &mut App, options: &TuiOptions) {
if options.focus == Some(TuiFocusTarget::Files) {
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.file_statuses = statuses;
if app.status_selected_index >= app.file_statuses.len() {
app.status_selected_index = 0;
}
}
}
}
fn adjust_scroll_for_resize(app: &mut App, term_height: u16) {
let list_lines = term_height.saturating_sub(8) as usize;
let detail_lines = term_height.saturating_sub(10) as usize;
if app.show_detail {
app.detail_adjust_scroll(detail_lines);
}
match app.input_mode {
InputMode::TopologyView => app.topology_adjust_scroll(list_lines),
InputMode::StatsView => app.stats_adjust_scroll(list_lines),
InputMode::HeatmapView => app.heatmap_adjust_scroll(list_lines),
InputMode::FileHistoryView => app.file_history_adjust_scroll(list_lines),
InputMode::BlameView => app.blame_adjust_scroll(list_lines),
InputMode::OwnershipView => app.ownership_adjust_scroll(list_lines),
InputMode::StashView => app.stash_adjust_scroll(list_lines),
InputMode::BranchCompareView => app.branch_compare_adjust_scroll(list_lines),
InputMode::RelatedFilesView => app.related_files_adjust_scroll(list_lines),
InputMode::ImpactScoreView => app.impact_score_adjust_scroll(list_lines),
InputMode::ChangeCouplingView => app.change_coupling_adjust_scroll(list_lines),
InputMode::QualityScoreView => app.quality_score_adjust_scroll(list_lines),
InputMode::ReviewPackView => app.review_pack_adjust_scroll(list_lines),
InputMode::NextActionsView => app.next_actions_adjust_scroll(list_lines),
InputMode::HandoffView => app.handoff_adjust_scroll(list_lines),
_ => {}
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let tui_options = parse_tui_options(&args);
if let Some(command) = parse_cli_args() {
if command != CliCommand::Benchmark {
return run_cli_mode(command);
}
}
if args.iter().any(|a| a == "--benchmark") {
return run_benchmark_mode(&tui_options);
}
run_tui_mode(&tui_options)
}
fn run_benchmark_mode(tui_options: &TuiOptions) -> Result<()> {
let start = Instant::now();
let mut app = App::new();
apply_tui_options(&mut app, tui_options);
if let Ok(repo) = Repository::discover(".") {
app.set_repo(repo);
apply_post_repo_focus(&mut app, tui_options);
}
if let Ok(repo_info) = get_repo_info_cached(app.get_repo()) {
let initial_events = load_events_fast(INITIAL_LOAD_COUNT).unwrap_or_default();
app.load(repo_info, initial_events);
if let Ok(head_hash) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(head_hash);
}
}
let elapsed = start.elapsed();
println!(
"Initialization time: {:.2}ms",
elapsed.as_secs_f64() * 1000.0
);
println!("Events loaded: {}", app.event_count());
Ok(())
}
fn run_tui_mode(tui_options: &TuiOptions) -> Result<()> {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
original_hook(panic_info);
}));
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
apply_tui_options(&mut app, tui_options);
let mut bg_load_state = BackgroundLoadState::new();
if let Ok(repo) = Repository::discover(".") {
app.set_repo(repo);
apply_post_repo_focus(&mut app, tui_options);
}
initialize_repo_data(&mut app, &mut bg_load_state);
let mut bg = BackgroundStates {
load: bg_load_state,
health: BackgroundHealthState::new(),
risk: BackgroundRiskState::new(),
diff: BackgroundDiffState::new(),
git_op: BackgroundGitOpState::new(),
analysis: BackgroundAnalysisState::new(),
blame: BackgroundBlameState::new(),
};
let result = run_app(&mut terminal, &mut app, &mut bg);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
fn initialize_repo_data(app: &mut App, bg_load_state: &mut BackgroundLoadState) {
if let Ok(repo_info) = get_repo_info_cached(app.get_repo()) {
let initial_events = load_events_fast(INITIAL_LOAD_COUNT).unwrap_or_default();
let initial_count = initial_events.len();
app.load(repo_info, initial_events);
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);
}
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.update_file_statuses(statuses);
}
annotate_events_with_ai(app);
if initial_count >= INITIAL_LOAD_COUNT {
app.start_loading();
bg_load_state.start(0, MAX_LOAD_COUNT);
}
}
}
fn annotate_events_with_ai(app: &mut App) {
let events: Vec<GitEvent> = app.events().cloned().collect();
let sessions = detect_sessions(&events);
if !sessions.is_empty() {
let session_map: std::collections::HashMap<String, u32> = sessions
.iter()
.flat_map(|s| s.commits.iter().map(move |hash| (hash.clone(), s.id)))
.collect();
app.annotate_events(|event| {
if let Some(&sid) = session_map.get(&event.short_hash) {
event.session_id = Some(sid);
}
if event.inferred_intent.is_none() {
event.inferred_intent = Some(classify_intent(&event.message, &[]));
}
});
} else {
app.annotate_events(|event| {
if event.inferred_intent.is_none() {
event.inferred_intent = Some(classify_intent(&event.message, &[]));
}
});
}
app.session_cache = if sessions.is_empty() {
None
} else {
Some(sessions)
};
}
const LOCAL_CHECK_MIN_INTERVAL: u64 = 1;
const LOCAL_CHECK_MAX_INTERVAL: u64 = 5;
const REMOTE_FETCH_INTERVAL: u64 = 60;
struct AdaptiveCheckInterval {
current_interval: u64,
last_check: Instant,
}
impl AdaptiveCheckInterval {
fn new() -> Self {
Self {
current_interval: LOCAL_CHECK_MIN_INTERVAL,
last_check: Instant::now(),
}
}
fn should_check(&self) -> bool {
self.last_check.elapsed().as_secs() >= self.current_interval
}
fn on_no_change(&mut self) {
self.current_interval = (self.current_interval * 2).min(LOCAL_CHECK_MAX_INTERVAL);
self.last_check = Instant::now();
}
fn on_change(&mut self) {
self.current_interval = LOCAL_CHECK_MIN_INTERVAL;
self.last_check = Instant::now();
}
}
fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
bg: &mut BackgroundStates,
) -> Result<()> {
let mut tracked_head = get_head_hash_cached(app.get_repo()).ok();
let mut tracked_index_mtime: Option<SystemTime> = get_index_mtime_cached(app.get_repo()).ok();
let mut adaptive_check = AdaptiveCheckInterval::new();
let mut fetch_state = RemoteFetchState::new();
let repo_path = std::env::current_dir().ok();
let mut needs_redraw = true;
loop {
if app.active_sidebar_panel == SidebarPanel::Commits
&& !app.show_detail
&& app.detail_diff_cache.is_none()
&& !bg.diff.is_running()
{
if let Some(event) = app.selected_event() {
let hash = event.short_hash.clone();
if bg.diff.pending_hash.as_deref() != Some(&hash) {
bg.diff.start(hash);
}
}
}
if app.active_sidebar_panel == SidebarPanel::Files {
if let Some(status) = app.file_statuses.get(app.status_selected_index) {
let current_path = status.path.clone();
let is_staged = status.kind.is_staged();
if app.file_diff_cache_path() != Some(current_path.as_str()) {
if let Ok(patch) = get_working_file_diff(¤t_path, is_staged) {
app.set_file_diff(patch, current_path);
needs_redraw = true;
}
}
}
}
if app.status_message.is_some() {
let was_some = app.status_message.is_some();
app.clear_expired_status_message();
if was_some && app.status_message.is_none() {
needs_redraw = true;
}
}
if needs_redraw {
terminal.draw(|frame| tui::render(frame, app))?;
needs_redraw = false;
}
if let Some(full_events) = bg.load.check_result() {
if !full_events.is_empty() {
let count = full_events.len();
app.all_events_replace(full_events);
app.filtered_indices_reset(count);
}
app.finish_loading();
annotate_events_with_ai(app);
let events_for_health: Vec<GitEvent> = app.events().cloned().collect();
bg.health.start(events_for_health);
needs_redraw = true;
}
if let Some((_hash, diff)) = bg.diff.check_result() {
app.set_detail_diff(diff);
needs_redraw = true;
}
if let Some((op, result)) = bg.git_op.check_result() {
match (&op, &result) {
(GitOp::Pull, Ok(())) => {
app.set_status_message(app.language.status_pulled().to_string());
key_handlers::reload_events_bg(&mut bg.load);
if let Ok(head_hash) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(head_hash);
}
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.update_file_statuses(statuses);
}
}
(GitOp::Push, Ok(())) => {
app.set_status_message(app.language.status_pushed().to_string());
}
(GitOp::Pull, Err(e)) => {
app.set_status_message(format!("{}: {}", app.language.status_pull_failed(), e));
}
(GitOp::Push, Err(e)) => {
app.set_status_message(format!("{}: {}", app.language.status_push_failed(), e));
}
}
needs_redraw = true;
}
if let Some(result) = bg.analysis.check_result() {
match result {
AnalysisResult::Timeline(timeline) => app.start_timeline_view(*timeline),
AnalysisResult::Ownership(ownership) => app.start_ownership_view(ownership),
AnalysisResult::ImpactScore(analysis) => app.start_impact_score_view(analysis),
AnalysisResult::ChangeCoupling(analysis) => {
app.start_change_coupling_view(analysis)
}
AnalysisResult::QualityScore(analysis) => app.start_quality_score_view(analysis),
}
needs_redraw = true;
}
if let Some((path, blame)) = bg.blame.check_result() {
app.close_detail();
app.start_blame_view(path, blame);
needs_redraw = true;
}
if let Some(result) = fetch_state.check_result() {
match result {
Ok(()) => {
app.set_status_message(app.language.status_fetched().to_string());
key_handlers::reload_events_bg(&mut bg.load);
if let Ok(current_head) = get_head_hash_cached(app.get_repo()) {
app.set_head_hash(current_head.clone());
tracked_head = Some(current_head);
}
}
Err(e) if !fetch_state.quiet_mode => {
app.set_status_message(format!(
"{}: {}",
app.language.status_fetch_failed(),
e
));
}
Err(_) => {} }
needs_redraw = true;
}
if let Some(health) = bg.health.check_result() {
app.health_view.cache = Some(health);
needs_redraw = true;
}
if let Some((score, level)) = bg.risk.check_result() {
app.staged_risk_score = Some(score);
app.staged_risk_level = Some(level);
needs_redraw = true;
}
if !fetch_state.is_running()
&& fetch_state.last_fetch.elapsed().as_secs() >= REMOTE_FETCH_INTERVAL
{
if let Some(ref path) = repo_path {
fetch_state.start(path.clone(), true); }
}
let should_check = if app.watch_mode {
adaptive_check.last_check.elapsed().as_millis() >= 500
} else {
adaptive_check.should_check()
};
if should_check {
let mut change_detected = false;
if let Ok(current_head) = get_head_hash_cached(app.get_repo()) {
if tracked_head.as_ref() != Some(¤t_head) {
key_handlers::reload_events_bg(&mut bg.load);
if let Ok(repo_info) = get_repo_info_cached(app.get_repo()) {
app.update_branch(repo_info.branch);
}
if let Ok(branches) = list_branches_cached(app.get_repo()) {
app.update_branches(branches);
}
app.set_head_hash(current_head.clone());
tracked_head = Some(current_head);
let events_for_health: Vec<GitEvent> = app.events().cloned().collect();
bg.health.start(events_for_health);
change_detected = true;
needs_redraw = true;
}
}
if let Ok(current_mtime) = get_index_mtime_cached(app.get_repo()) {
if tracked_index_mtime.as_ref() != Some(¤t_mtime) {
if let Ok(statuses) = get_status_cached(app.get_repo()) {
app.update_file_statuses(statuses.clone());
if !bg.risk.is_running() {
let events_for_risk: Vec<GitEvent> = app.events().cloned().collect();
bg.risk.start(events_for_risk, statuses);
}
}
needs_redraw = true;
tracked_index_mtime = Some(current_mtime);
change_detected = true;
}
}
if change_detected {
adaptive_check.on_change();
} else {
adaptive_check.on_no_change();
}
}
let poll_timeout = if needs_redraw { 100 } else { 500 };
if let Some(event) = check_input(poll_timeout)? {
if let Event::Resize(_, rows) = event {
adjust_scroll_for_resize(app, rows);
needs_redraw = true;
} else if let Event::Mouse(mouse) = event {
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
let click_col = mouse.column;
let click_row = mouse.row;
let area: ratatui::layout::Rect = terminal.size().unwrap_or_default().into();
let layout = tui::calculate_layout_areas(area, app);
for (i, panel_area) in layout.sidebar_panels.iter().enumerate() {
if click_col >= panel_area.x
&& click_col < panel_area.x + panel_area.width
&& click_row >= panel_area.y
&& click_row < panel_area.y + panel_area.height
{
let panels = SidebarPanel::all();
app.active_sidebar_panel = panels[i];
app.sidebar_focused = true;
needs_redraw = true;
break;
}
}
}
} else if let Event::Key(key) = event {
if key.kind != KeyEventKind::Press {
continue;
}
needs_redraw = true;
match app.input_mode {
InputMode::Filter => handle_filter_keys(app, key),
InputMode::PresetSave => handle_preset_save_keys(app, key),
InputMode::BranchSelect => handle_branch_select_keys(app, key, &mut bg.load),
InputMode::BranchCreate => handle_branch_create_keys(app, key, &mut bg.load),
InputMode::QuickActionView => {
handle_quick_action_keys(app, key, &mut bg.analysis)
}
InputMode::Normal if app.show_help => handle_help_keys(app, key),
InputMode::Normal if app.show_detail => {
handle_detail_keys(app, key, terminal, &mut bg.diff, &mut bg.blame)
}
InputMode::StatusView => handle_status_view_keys(app, key, &mut bg.git_op),
InputMode::CommitInput => {
if handle_commit_input_keys(app, key, &mut bg.load) {
continue;
}
}
InputMode::TopologyView => {
handle_topology_view_keys(app, key, terminal, &mut bg.load)
}
InputMode::BranchCompareView => handle_branch_compare_keys(app, key, terminal),
InputMode::RelatedFilesView => handle_related_files_keys(app, key, terminal),
InputMode::StatsView => handle_stats_view_keys(app, key, terminal),
InputMode::HeatmapView => handle_heatmap_view_keys(app, key, terminal),
InputMode::FileHistoryView => handle_file_history_keys(app, key, terminal),
InputMode::TimelineView => handle_timeline_view_keys(app, key),
InputMode::BlameView => handle_blame_view_keys(app, key, terminal),
InputMode::OwnershipView => handle_ownership_view_keys(app, key, terminal),
InputMode::ImpactScoreView => handle_impact_score_keys(app, key, terminal),
InputMode::ChangeCouplingView => {
handle_change_coupling_keys(app, key, terminal)
}
InputMode::QualityScoreView => handle_quality_score_keys(app, key, terminal),
InputMode::StashView => handle_stash_view_keys(app, key, terminal),
InputMode::PatchView => handle_patch_view_keys(app, key, terminal),
InputMode::ReviewQueueView => handle_review_queue_keys(app, key),
InputMode::PrCreate => handle_pr_create_keys(app, key),
InputMode::ReviewPackView => handle_review_pack_view_keys(app, key, terminal),
InputMode::NextActionsView => handle_next_actions_view_keys(app, key, terminal),
InputMode::HandoffView => handle_handoff_view_keys(app, key, terminal),
InputMode::Normal => handle_normal_keys(
app,
key,
terminal,
&mut fetch_state,
&repo_path,
&mut bg.load,
),
}
}
}
if app.should_quit {
return Ok(());
}
}
}