Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2063,27 +2063,47 @@ impl App {
}
}

pub fn poll_persisted_session_changes(&mut self) {
/// Returns `true` if visible state changed (external comments merged or a
/// warning was raised) so the main loop can schedule a redraw without an
/// input event.
pub fn poll_persisted_session_changes(&mut self) -> bool {
let Some(interval) = self.review_watch_interval else {
return;
return false;
};
let now = Instant::now();
if now < self.next_review_watch_at {
return;
return false;
}
self.next_review_watch_at = now + interval;

// Do not mutate the session while the user is composing or editing a
// comment. The next poll after the editor closes will merge changes.
if self.input_mode == InputMode::Comment {
return;
return false;
}

if let Err(err) = self.reload_persisted_session_if_changed(false) {
self.set_warning(format!("Review reload failed: {err}"));
match self.reload_persisted_session_if_changed(false) {
Ok(0) => false,
Ok(_) => true,
Err(err) => {
self.set_warning(format!("Review reload failed: {err}"));
true
}
}
}

/// True while any forge background fetch (PR list/open/reload/threads/
/// submit) is in flight. Used by the main loop to keep redrawing so
/// spinners animate and results land without waiting for input.
pub fn has_pending_pr_work(&self) -> bool {
self.pr_load_rx.is_some()
|| self.pr_open_rx.is_some()
|| self.pr_reload_rx.is_some()
|| self.pr_range_reload_rx.is_some()
|| self.pr_threads_rx.is_some()
|| self.pr_submit_rx.is_some()
}

pub fn reload_persisted_session_if_changed(&mut self, force: bool) -> Result<usize> {
let path = match self.session_path.clone() {
Some(path) => path,
Expand Down Expand Up @@ -4340,7 +4360,9 @@ impl App {
});
}

pub fn clear_expired_message(&mut self) {
/// Returns `true` if a message was cleared so the main loop can
/// schedule a redraw.
pub fn clear_expired_message(&mut self) -> bool {
let expired = self
.message
.as_ref()
Expand All @@ -4349,6 +4371,7 @@ impl App {
if expired {
self.message = None;
}
expired
}

pub fn cursor_down(&mut self, lines: usize) {
Expand Down
44 changes: 31 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ fn main() -> anyhow::Result<()> {
let mut pending_leader = false;
// Track pending Ctrl+C for "press twice to exit" (with timestamp for 2s timeout)
let mut pending_ctrl_c: Option<Instant> = None;
// Only re-render when state actually changed; the diff renderer rebuilds
// every line on each draw, so idle redraws are expensive on large diffs.
let mut needs_redraw = true;

// Main loop
loop {
Expand All @@ -308,6 +311,7 @@ fn main() -> anyhow::Result<()> {
) = rx.try_recv()
{
app.update_info = Some(info);
needs_redraw = true;
}

// Auto-clear expired pending Ctrl+C state and message
Expand All @@ -316,30 +320,44 @@ fn main() -> anyhow::Result<()> {
{
pending_ctrl_c = None;
app.message = None;
needs_redraw = true;
}

app.clear_expired_message();
needs_redraw |= app.clear_expired_message();

// Snapshot before polling so the tick that drains a channel (clearing
// its rx) still triggers a redraw with the applied result. While work
// is pending we redraw every tick anyway so spinners animate.
let pr_pending = app.has_pending_pr_work();
app.poll_pr_load_events();
app.poll_pr_open_events();
app.poll_pr_reload_events();
app.poll_pr_range_reload_events();
app.poll_pr_threads_events();
app.poll_pr_submit_events();
app.poll_persisted_session_changes();

// Render. Bracket the frame in a synchronized-output pair
// (CSI ?2026h/l) so terminals (and zellij) buffer the whole repaint
// and present it atomically. Without this, scrolling over a slow link
// visibly tears as escape sequences arrive in chunks. Terminals that
// do not support DEC 2026 ignore it.
queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
terminal.draw(|frame| {
ui::render(frame, &mut app);
})?;
execute!(terminal.backend_mut(), EndSynchronizedUpdate)?;
needs_redraw |= app.poll_persisted_session_changes();
needs_redraw |= pr_pending;

if needs_redraw {
// Bracket the frame in a synchronized-output pair (CSI ?2026h/l)
// so terminals (and zellij) buffer the whole repaint and present
// it atomically. Without this, scrolling over a slow link visibly
// tears as escape sequences arrive in chunks. Terminals that do
// not support DEC 2026 ignore it.
queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
terminal.draw(|frame| {
ui::render(frame, &mut app);
})?;
execute!(terminal.backend_mut(), EndSynchronizedUpdate)?;
needs_redraw = false;
}

// Handle events
if event::poll(Duration::from_millis(100))? {
// Set before reading so the many `continue` paths below (leader
// keys, zz/ZZ, dd, {count}G, …) still schedule a redraw on the
// next iteration even though they short-circuit past the match.
needs_redraw = true;
let event = event::read()?;
// Down/Up Release flips the `*_released_since_arm` flag so the
// primed two-press file walk in single-file view requires a
Expand Down
Loading