From 5c524099e682dbed3cfd168fcc7b0fa419459894 Mon Sep 17 00:00:00 2001 From: Marco Berger Date: Sun, 1 Feb 2026 12:01:05 +0100 Subject: [PATCH 1/2] Fix 100% CPU/GPU usage in file dialog view The event loop ignored Event::Update when showing the file dialog, causing next_repo_refresh to never be updated after expiration. This resulted in poll() returning immediately with zero timeout, creating a busy loop. Changed if-let to match statements that handle both Event::Input and Event::Update, updating the refresh timer in both branches. --- src/main.rs | 116 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8ab9664..d0772de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -793,8 +793,8 @@ fn run( let mut app = None; if file_dialog.error_message.is_some() { - if let Event::Input(event) = next_event() { - match event.code { + match next_event() { + Event::Input(event) => match event.code { KeyCode::Enter | KeyCode::Esc => { file_dialog.clear_error(); } @@ -805,61 +805,77 @@ fn run( break; } _ => {} + }, + Event::Update => { + let now = Instant::now(); + if next_repo_refresh.get() <= now { + next_repo_refresh.set(now + repo_refresh_interval); + } } } - } else if let Event::Input(event) = next_event() { - match event.code { - KeyCode::Char('q') => { - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - break; - } - KeyCode::Char('o') if event.modifiers.contains(KeyModifiers::CONTROL) => { - if let Some(prev_app) = file_dialog.previous_app.take() { - app = Some(prev_app); - } else { - file_dialog.set_error("No repository to return to.\nSelect a Git rrpository or quit with Q.".to_string()) + } else { + match next_event() { + Event::Input(event) => match event.code { + KeyCode::Char('q') => { + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + break; } - } - KeyCode::Esc => { - if let Some(prev_app) = file_dialog.previous_app.take() { - app = Some(prev_app); - } else { - file_dialog.set_error("No repository to return to.\nSelect a Git rrpository or quit with Q.".to_string()) + KeyCode::Char('o') if event.modifiers.contains(KeyModifiers::CONTROL) => { + if let Some(prev_app) = file_dialog.previous_app.take() { + app = Some(prev_app); + } else { + file_dialog.set_error("No repository to return to.\nSelect a Git rrpository or quit with Q.".to_string()) + } } - } - KeyCode::Up => file_dialog.on_up(event.modifiers.contains(KeyModifiers::SHIFT)), - KeyCode::Down => { - file_dialog.on_down(event.modifiers.contains(KeyModifiers::SHIFT)) - } - KeyCode::Left => file_dialog.on_left()?, - KeyCode::Right => file_dialog.on_right()?, - KeyCode::Enter => { - file_dialog.on_enter(); - if let Some(path) = &file_dialog.selection { - match get_repo(path) { - Ok(repo) => { - if repo.is_shallow() { - file_dialog.set_error(format!("{} is a shallow clone. Shallow clones are not supported due to a missing feature in the underlying libgit2 library.", repo.path().parent().unwrap().display())); - } else { - app = Some(create_app( - repo, - &mut settings, - &app_settings, - model, - max_commits, - )?) + KeyCode::Esc => { + if let Some(prev_app) = file_dialog.previous_app.take() { + app = Some(prev_app); + } else { + file_dialog.set_error("No repository to return to.\nSelect a Git rrpository or quit with Q.".to_string()) + } + } + KeyCode::Up => { + file_dialog.on_up(event.modifiers.contains(KeyModifiers::SHIFT)) + } + KeyCode::Down => { + file_dialog.on_down(event.modifiers.contains(KeyModifiers::SHIFT)) + } + KeyCode::Left => file_dialog.on_left()?, + KeyCode::Right => file_dialog.on_right()?, + KeyCode::Enter => { + file_dialog.on_enter(); + if let Some(path) = &file_dialog.selection { + match get_repo(path) { + Ok(repo) => { + if repo.is_shallow() { + file_dialog.set_error(format!("{} is a shallow clone. Shallow clones are not supported due to a missing feature in the underlying libgit2 library.", repo.path().parent().unwrap().display())); + } else { + app = Some(create_app( + repo, + &mut settings, + &app_settings, + model, + max_commits, + )?) + } } - } - Err(_) => { - file_dialog.on_right()?; - } - }; + Err(_) => { + file_dialog.on_right()?; + } + }; + } + } + _ => {} + }, + Event::Update => { + let now = Instant::now(); + if next_repo_refresh.get() <= now { + next_repo_refresh.set(now + repo_refresh_interval); } } - _ => {} - }; + } } app }; From 6f9f1fb58f691798a7a00e564b63313861276f7b Mon Sep 17 00:00:00 2001 From: Marco Berger Date: Sun, 1 Feb 2026 12:01:15 +0100 Subject: [PATCH 2/2] Fix crash with single-commit repositories Multiple places used len() - 1 which causes unsigned integer underflow when length is 0, wrapping to usize::MAX. This caused invalid comparisons in bounds checks and subsequent out-of-bounds index access. Changes: - Added guard for empty indices in graph_view render function - Changed len() - 1 to len().saturating_sub(1) throughout - Used safe .get(idx).copied() instead of direct indexing - Pre-computed max index values to avoid repeated calculations --- src/dialogs.rs | 2 +- src/widgets/graph_view.rs | 53 ++++++++++++++++++++++++-------------- src/widgets/models_view.rs | 2 +- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/dialogs.rs b/src/dialogs.rs index 5105a8c..73f142a 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -31,7 +31,7 @@ impl<'a> FileDialog<'a> { pub fn fwd(&mut self, steps: usize) { let i = match self.state.selected() { - Some(i) => std::cmp::min(i.saturating_add(steps), self.dirs.len() - 1), + Some(i) => std::cmp::min(i.saturating_add(steps), self.dirs.len().saturating_sub(1)), None => 0, }; self.state.select(Some(i)); diff --git a/src/widgets/graph_view.rs b/src/widgets/graph_view.rs index f701d6b..910467b 100644 --- a/src/widgets/graph_view.rs +++ b/src/widgets/graph_view.rs @@ -29,9 +29,12 @@ impl GraphViewState { pub fn move_selection(&mut self, steps: usize, down: bool) -> bool { let changed = if let Some(sel) = self.selected { let new_idx = if down { - std::cmp::min(sel.saturating_add(steps), self.indices.len() - 1) + std::cmp::min( + sel.saturating_add(steps), + self.indices.len().saturating_sub(1), + ) } else { - std::cmp::max(sel.saturating_sub(steps), 0) + sel.saturating_sub(steps) }; self.selected = Some(new_idx); new_idx != sel @@ -49,18 +52,24 @@ impl GraphViewState { pub fn move_secondary_selection(&mut self, steps: usize, down: bool) -> bool { let changed = if let Some(sel) = self.secondary_selected { let new_idx = if down { - std::cmp::min(sel.saturating_add(steps), self.indices.len() - 1) + std::cmp::min( + sel.saturating_add(steps), + self.indices.len().saturating_sub(1), + ) } else { - std::cmp::max(sel.saturating_sub(steps), 0) + sel.saturating_sub(steps) }; self.secondary_selected = Some(new_idx); new_idx != sel } else if !self.graph_lines.is_empty() { if let Some(sel) = self.selected { let new_idx = if down { - std::cmp::min(sel.saturating_add(steps), self.indices.len() - 1) + std::cmp::min( + sel.saturating_add(steps), + self.indices.len().saturating_sub(1), + ) } else { - std::cmp::max(sel.saturating_sub(steps), 0) + sel.saturating_sub(steps) }; self.secondary_selected = Some(new_idx); new_idx != sel @@ -131,7 +140,7 @@ impl StatefulWidget for GraphView<'_> { return; } - if state.graph_lines.is_empty() { + if state.graph_lines.is_empty() || state.indices.is_empty() { return; } let list_height = list_area.height as usize; @@ -144,18 +153,23 @@ impl StatefulWidget for GraphView<'_> { ); let mut end = start + height; - let selected_row = state.selected.map(|idx| state.indices[idx]); - let selected = selected_row.unwrap_or(0).min(state.graph_lines.len() - 1); + let max_graph_idx = state.graph_lines.len().saturating_sub(1); + let max_indices_idx = state.indices.len().saturating_sub(1); - let secondary_selected_row = state.secondary_selected.map(|idx| state.indices[idx]); - let secondary_selected = secondary_selected_row - .unwrap_or(0) - .min(state.graph_lines.len() - 1); + let selected_row = state + .selected + .and_then(|idx| state.indices.get(idx).copied()); + let selected = selected_row.unwrap_or(0).min(max_graph_idx); + + let secondary_selected_row = state + .secondary_selected + .and_then(|idx| state.indices.get(idx).copied()); + let secondary_selected = secondary_selected_row.unwrap_or(0).min(max_graph_idx); let selected_index = if state.secondary_changed { - state.secondary_selected.unwrap_or(0) + state.secondary_selected.unwrap_or(0).min(max_indices_idx) } else { - state.selected.unwrap_or(0) + state.selected.unwrap_or(0).min(max_indices_idx) }; let move_to_selected = if state.secondary_changed { secondary_selected @@ -163,12 +177,13 @@ impl StatefulWidget for GraphView<'_> { selected }; - let move_to_end = if selected_index >= state.indices.len() - 1 { - state.graph_lines.len() - 1 + let move_to_end = if selected_index >= max_indices_idx { + max_graph_idx } else { - (state.indices[selected_index + 1] - 1) + state.indices[selected_index + 1] + .saturating_sub(1) .max(move_to_selected + SCROLL_MARGIN) - .min(state.graph_lines.len() - 1) + .min(max_graph_idx) }; let move_to_start = move_to_selected.saturating_sub(SCROLL_MARGIN); diff --git a/src/widgets/models_view.rs b/src/widgets/models_view.rs index bf31979..848325d 100644 --- a/src/widgets/models_view.rs +++ b/src/widgets/models_view.rs @@ -17,7 +17,7 @@ impl ModelListState { pub fn fwd(&mut self, steps: usize) { let i = match self.state.selected() { - Some(i) => std::cmp::min(i.saturating_add(steps), self.models.len() - 1), + Some(i) => std::cmp::min(i.saturating_add(steps), self.models.len().saturating_sub(1)), None => 0, }; self.state.select(Some(i));