diff --git a/captures/ghzinga-issue-88499/large/manifest.json b/captures/ghzinga-issue-88499/large/manifest.json index 3faec0b6..f264b5af 100644 --- a/captures/ghzinga-issue-88499/large/manifest.json +++ b/captures/ghzinga-issue-88499/large/manifest.json @@ -4,7 +4,7 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "requested_columns": 160, "requested_rows": 50, @@ -113,5 +113,5 @@ } ], "actual_tmux_size": "160x50", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-issue-88499/manifest.json b/captures/ghzinga-issue-88499/manifest.json index 05817bd5..4cc2f9f8 100644 --- a/captures/ghzinga-issue-88499/manifest.json +++ b/captures/ghzinga-issue-88499/manifest.json @@ -3,7 +3,7 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "offline_fixture": "fixtures/issue-88499.json", "offline_resource_fixtures": [], @@ -24,5 +24,5 @@ "rows": 50 } ], - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-issue-88499/medium/manifest.json b/captures/ghzinga-issue-88499/medium/manifest.json index 878528ff..0041beaf 100644 --- a/captures/ghzinga-issue-88499/medium/manifest.json +++ b/captures/ghzinga-issue-88499/medium/manifest.json @@ -4,7 +4,7 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "requested_columns": 120, "requested_rows": 36, @@ -113,5 +113,5 @@ } ], "actual_tmux_size": "120x36", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-issue-88499/mouse-smoke/manifest.json b/captures/ghzinga-issue-88499/mouse-smoke/manifest.json index d499d094..fb45ca83 100644 --- a/captures/ghzinga-issue-88499/mouse-smoke/manifest.json +++ b/captures/ghzinga-issue-88499/mouse-smoke/manifest.json @@ -5,7 +5,7 @@ "fixtures/issue-66943.json" ], "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-issue-88499/mouse-smoke/capture-empty-config.toml", "command": "cd . && TERM=xterm-256color GZG_CONFIG_PATH=./captures/ghzinga-issue-88499/mouse-smoke/capture-empty-config.toml GZG_STATE_HOME=./captures/ghzinga-issue-88499/mouse-smoke/.capture-state GZG_CACHE_HOME=./captures/ghzinga-issue-88499/mouse-smoke/.capture-cache BROWSER=./captures/ghzinga-issue-88499/mouse-smoke/capture-open-url.sh GZG_COPY_COMMAND=./captures/ghzinga-issue-88499/mouse-smoke/capture-copy-url.sh ./target/debug/gzg https://github.com/openclaw/openclaw/issues/88499 --offline-fixture ./captures/ghzinga-issue-88499/mouse-smoke/navigation-fixture.json --offline-resource-fixture ./fixtures/issue-66943.json --no-restore --refresh-seconds 0", "actual_tmux_size": "120x36", @@ -120,5 +120,5 @@ "ansi": "70_mouse_quit_confirm.ansi" } ], - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-issue-88499/narrow/manifest.json b/captures/ghzinga-issue-88499/narrow/manifest.json index dc524792..77fa920c 100644 --- a/captures/ghzinga-issue-88499/narrow/manifest.json +++ b/captures/ghzinga-issue-88499/narrow/manifest.json @@ -4,7 +4,7 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "requested_columns": 80, "requested_rows": 24, @@ -113,5 +113,5 @@ } ], "actual_tmux_size": "80x24", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-pr-81834/large/manifest.json b/captures/ghzinga-pr-81834/large/manifest.json index e4f305cf..abf20f75 100644 --- a/captures/ghzinga-pr-81834/large/manifest.json +++ b/captures/ghzinga-pr-81834/large/manifest.json @@ -4,7 +4,7 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "requested_columns": 160, "requested_rows": 50, @@ -183,5 +183,5 @@ } ], "actual_tmux_size": "160x50", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-pr-81834/manifest.json b/captures/ghzinga-pr-81834/manifest.json index 51649a2c..ab9ff300 100644 --- a/captures/ghzinga-pr-81834/manifest.json +++ b/captures/ghzinga-pr-81834/manifest.json @@ -3,7 +3,7 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "offline_fixture": null, "offline_resource_fixtures": [], @@ -24,5 +24,5 @@ "rows": 50 } ], - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-pr-81834/medium/manifest.json b/captures/ghzinga-pr-81834/medium/manifest.json index 75b56a08..4e10b556 100644 --- a/captures/ghzinga-pr-81834/medium/manifest.json +++ b/captures/ghzinga-pr-81834/medium/manifest.json @@ -4,7 +4,7 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "requested_columns": 120, "requested_rows": 36, @@ -183,5 +183,5 @@ } ], "actual_tmux_size": "120x36", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-pr-81834/mouse-smoke/manifest.json b/captures/ghzinga-pr-81834/mouse-smoke/manifest.json index 3b40d5f6..1fb45df5 100644 --- a/captures/ghzinga-pr-81834/mouse-smoke/manifest.json +++ b/captures/ghzinga-pr-81834/mouse-smoke/manifest.json @@ -5,7 +5,7 @@ "fixtures/issue-66943.json" ], "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-pr-81834/mouse-smoke/capture-empty-config.toml", "command": "cd . && TERM=xterm-256color GZG_CONFIG_PATH=./captures/ghzinga-pr-81834/mouse-smoke/capture-empty-config.toml GZG_STATE_HOME=./captures/ghzinga-pr-81834/mouse-smoke/.capture-state GZG_CACHE_HOME=./captures/ghzinga-pr-81834/mouse-smoke/.capture-cache BROWSER=./captures/ghzinga-pr-81834/mouse-smoke/capture-open-url.sh GZG_COPY_COMMAND=./captures/ghzinga-pr-81834/mouse-smoke/capture-copy-url.sh ./target/debug/gzg 'openclaw/openclaw#81834' --offline-fixture ./captures/ghzinga-pr-81834/mouse-smoke/navigation-fixture.json --offline-resource-fixture ./fixtures/issue-66943.json --no-restore --refresh-seconds 0", "actual_tmux_size": "120x36", @@ -240,5 +240,5 @@ ], "load_full_fixture": "captures/ghzinga-pr-81834/mouse-smoke/load-full-fixture.json", "load_full_command": "cd . && TERM=xterm-256color GZG_CONFIG_PATH=./captures/ghzinga-pr-81834/mouse-smoke/capture-empty-config.toml GZG_STATE_HOME=./captures/ghzinga-pr-81834/mouse-smoke/.capture-state GZG_CACHE_HOME=./captures/ghzinga-pr-81834/mouse-smoke/.capture-cache ./target/debug/gzg 'openclaw/openclaw#81834' --offline-fixture ./captures/ghzinga-pr-81834/mouse-smoke/load-full-fixture.json --no-restore --refresh-seconds 0", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/captures/ghzinga-pr-81834/narrow/manifest.json b/captures/ghzinga-pr-81834/narrow/manifest.json index abc8cbf2..4cfa69d8 100644 --- a/captures/ghzinga-pr-81834/narrow/manifest.json +++ b/captures/ghzinga-pr-81834/narrow/manifest.json @@ -4,7 +4,7 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "1f9c11df009706d744e6c810d50bb886d8bebc9a", + "git_commit": "79b504ea6e2ab4d768a3abf0aed9ab4903057f46", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "requested_columns": 80, "requested_rows": 24, @@ -183,5 +183,5 @@ } ], "actual_tmux_size": "80x24", - "app_tree_hash": "0bb55716541e219b88eda9e1305244258debfd2a90d5e6d65b0958aafbe4840d" + "app_tree_hash": "3d0ff248f59a801cfca8d8049b68091d5ade6f392d024eb3cb07ec1ffd4fd358" } diff --git a/src/domain/resource.rs b/src/domain/resource.rs index ae21f684..b73e39d2 100644 --- a/src/domain/resource.rs +++ b/src/domain/resource.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt}; +use std::{collections::HashMap, fmt, sync::LazyLock}; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -6,6 +6,14 @@ use thiserror::Error; pub const FULL_DEPTH_WARNING_HINT: &str = "set --api-depth full or GZG_API_DEPTH=full"; +static GITHUB_RESOURCE_URL_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^https://github\.com/([^/\s]+)/([^/\s]+)/(pull|issues)/([0-9]+)(?:[/?#].*)?$") + .expect("valid GitHub URL regex") +}); +static OWNER_REPO_HASH_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"^([^/\s#]+)/([^/\s#]+)#([0-9]+)$").expect("valid owner repo hash regex") +}); + #[derive(Debug, Error, PartialEq, Eq)] pub enum ResourceIdError { #[error("expected a GitHub PR/issue URL, owner/repo#number, or owner/repo number")] @@ -107,11 +115,7 @@ impl ResourceId { } fn parse_url(input: &str) -> Result, ResourceIdError> { - let re = Regex::new( - r"^https://github\.com/([^/\s]+)/([^/\s]+)/(pull|issues)/([0-9]+)(?:[/?#].*)?$", - ) - .expect("valid GitHub URL regex"); - let Some(caps) = re.captures(input) else { + let Some(caps) = GITHUB_RESOURCE_URL_RE.captures(input) else { return Ok(None); }; let kind_hint = match &caps[3] { @@ -128,9 +132,7 @@ impl ResourceId { } fn parse_owner_repo_hash(input: &str) -> Result, ResourceIdError> { - let re = - Regex::new(r"^([^/\s#]+)/([^/\s#]+)#([0-9]+)$").expect("valid owner repo hash regex"); - let Some(caps) = re.captures(input) else { + let Some(caps) = OWNER_REPO_HASH_RE.captures(input) else { return Ok(None); }; Ok(Some(Self { diff --git a/src/runner.rs b/src/runner.rs index 976b7f7e..16926f22 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -655,19 +655,22 @@ async fn run_tui( last_refresh = Instant::now(); } } + let mut needs_redraw = true; loop { + let mut state_changed = false; if apply_completed_fetches(state, &mut fetch_rx) { + state_changed = true; if let Some(runtime) = &mut session_runtime { persist_session_now(state, runtime); } } - maybe_load_file_patches_for_active_files_tab( + state_changed |= maybe_load_file_patches_for_active_files_tab( state, fetch_source.clone(), &fetch_tx, &mut last_refresh, ); - handle_pending_control_requests( + state_changed |= handle_pending_control_requests( state, fetch_source.clone(), &fetch_tx, @@ -675,21 +678,31 @@ async fn run_tui( &mut session_runtime, &mut last_refresh, ); - state.advance_loading_frame(); - terminal.draw(|frame| render_app(frame, state))?; + if should_advance_loading_frame(state) { + state.advance_loading_frame(); + state_changed = true; + } + let scrollbar_was_fading = should_advance_scrollbar_fade(state); + if scrollbar_was_fading { + state_changed = true; + } + if needs_redraw || state_changed { + terminal.draw(|frame| render_app(frame, state))?; + needs_redraw = should_redraw_after_scrollbar_frame(scrollbar_was_fading, state); + } if state.should_quit { if let Some(runtime) = &mut session_runtime { persist_session_now(state, runtime); } return Ok(()); } - maybe_refresh_loading_active_resource( + needs_redraw |= maybe_refresh_loading_active_resource( state, fetch_source.clone(), &fetch_tx, &mut last_refresh, ); - maybe_auto_refresh( + needs_redraw |= maybe_auto_refresh( state, fetch_source.is_live_github(), refresh_interval, @@ -698,8 +711,17 @@ async fn run_tui( fetch_source.clone(), &fetch_tx, ); - for app_event in read_pending_app_events()? { - let intent = apply_event(state, app_event); + let app_events = read_pending_app_events()?; + if !app_events.events.is_empty() { + needs_redraw = true; + } + for pending_event in app_events.events { + if pending_event.requires_pre_event_redraw { + let scrollbar_was_fading = should_advance_scrollbar_fade(state); + terminal.draw(|frame| render_app(frame, state))?; + needs_redraw |= should_redraw_after_scrollbar_frame(scrollbar_was_fading, state); + } + let intent = apply_event(state, pending_event.event); if handle_intent( state, intent, @@ -714,13 +736,13 @@ async fn run_tui( } return Ok(()); } - maybe_refresh_loading_active_resource( + needs_redraw |= maybe_refresh_loading_active_resource( state, fetch_source.clone(), &fetch_tx, &mut last_refresh, ); - maybe_load_file_patches_for_active_files_tab( + needs_redraw |= maybe_load_file_patches_for_active_files_tab( state, fetch_source.clone(), &fetch_tx, @@ -731,11 +753,40 @@ async fn run_tui( } } if let Some(runtime) = &mut session_runtime { - persist_session_when_due(state, runtime, Instant::now()); + needs_redraw |= persist_session_when_due(state, runtime, Instant::now()); } } } +fn should_advance_loading_frame(state: &AppState) -> bool { + if state.loading_message().is_some() || state.file_patch_loading_message().is_some() { + return true; + } + if is_animating_loading_resource(&state.resource, state.last_error.as_deref()) { + return true; + } + let active_tab_id = state.active_resource_tab_id(); + state.resource_tabs.iter().any(|tab| { + tab.id != active_tab_id + && is_animating_loading_resource(&tab.resource, tab.last_error.as_deref()) + }) +} + +fn is_animating_loading_resource( + resource: &crate::domain::Resource, + last_error: Option<&str>, +) -> bool { + last_error.is_none() && is_loading_resource(resource) +} + +fn should_advance_scrollbar_fade(state: &AppState) -> bool { + state.scrollbar_visible_frames > 0 +} + +fn should_redraw_after_scrollbar_frame(scrollbar_was_fading: bool, state: &AppState) -> bool { + scrollbar_was_fading || should_advance_scrollbar_fade(state) +} + fn handle_pending_control_requests( state: &mut AppState, fetch_source: FetchSource, @@ -743,7 +794,8 @@ fn handle_pending_control_requests( control_rx: &mut UnboundedReceiver, session_runtime: &mut Option, last_refresh: &mut Instant, -) { +) -> bool { + let mut handled = false; while let Ok(request) = control_rx.try_recv() { let reply = handle_control_command( state, @@ -760,7 +812,9 @@ fn handle_pending_control_requests( persist_session_now(state, runtime); } } + handled = true; } + handled } fn handle_control_command( @@ -863,9 +917,9 @@ fn apply_control_setting(state: &mut AppState, key: &str, value: &str) -> Result } } -fn persist_session_now(state: &mut AppState, runtime: &mut SessionRuntime) { +fn persist_session_now(state: &mut AppState, runtime: &mut SessionRuntime) -> bool { if !session_state_persistable(state) { - return; + return false; } match session::save_session( &runtime.handle, @@ -878,23 +932,32 @@ fn persist_session_now(state: &mut AppState, runtime: &mut SessionRuntime) { runtime.snapshot = Some(snapshot); runtime.dirty = false; runtime.dirty_since = None; + false } Err(error) => { - state.last_error = Some(format!("failed to save ghzinga session: {error}")); + let message = format!("failed to save ghzinga session: {error}"); + let changed = state.last_error.as_deref() != Some(message.as_str()); + state.last_error = Some(message); + changed } } } -fn persist_session_when_due(state: &mut AppState, runtime: &mut SessionRuntime, now: Instant) { +fn persist_session_when_due( + state: &mut AppState, + runtime: &mut SessionRuntime, + now: Instant, +) -> bool { if !runtime.dirty { - return; + return false; } let Some(dirty_since) = runtime.dirty_since else { - return; + return false; }; if now.duration_since(dirty_since) >= SESSION_SAVE_DEBOUNCE { - persist_session_now(state, runtime); + return persist_session_now(state, runtime); } + false } fn session_state_persistable(state: &AppState) -> bool { @@ -975,9 +1038,18 @@ enum OpenResourceMode { ReplaceCurrent, } -fn read_pending_app_events() -> anyhow::Result> { +struct PendingAppEvents { + events: Vec, +} + +struct PendingAppEvent { + event: AppEvent, + requires_pre_event_redraw: bool, +} + +fn read_pending_app_events() -> anyhow::Result { if !event::poll(EVENT_POLL_TIMEOUT)? { - return Ok(Vec::new()); + return Ok(PendingAppEvents { events: Vec::new() }); } let mut events = Vec::with_capacity(MAX_PENDING_EVENTS_PER_FRAME); @@ -985,13 +1057,24 @@ fn read_pending_app_events() -> anyhow::Result> { while events.len() < MAX_PENDING_EVENTS_PER_FRAME && event::poll(Duration::ZERO)? { events.push(event_to_app_event(event::read()?)); } - Ok(events.into_iter().flatten().collect()) + let events = events.into_iter().flatten().collect(); + Ok(PendingAppEvents { events }) } -fn event_to_app_event(event: Event) -> Option { +fn event_to_app_event(event: Event) -> Option { match event { - Event::Key(key) => Some(AppEvent::Key(key)), - Event::Mouse(mouse) => Some(AppEvent::Mouse(mouse)), + Event::Key(key) => Some(PendingAppEvent { + event: AppEvent::Key(key), + requires_pre_event_redraw: false, + }), + Event::Mouse(mouse) => Some(PendingAppEvent { + event: AppEvent::Mouse(mouse), + requires_pre_event_redraw: false, + }), + Event::Resize(_, _) => Some(PendingAppEvent { + event: AppEvent::Tick, + requires_pre_event_redraw: true, + }), _ => None, } } @@ -1586,7 +1669,7 @@ mod tests { }; use crate::{ - app::{loading_resource_placeholder, AppIntent, AppState, Tab}, + app::{loading_resource_placeholder, AppEvent, AppIntent, AppState, Tab}, domain::{ Resource, ResourceId, ResourceKind, FILE_PATCH_CONTEXT_UNAVAILABLE_WARNING, FULL_DEPTH_WARNING_HINT, @@ -1605,10 +1688,12 @@ mod tests { use super::{ apply_control_setting, auto_refresh_due, clipboard_command, empty_launch_resource, - handle_control_open, handle_intent, has_command_help_arg, maybe_auto_refresh_with_start, - maybe_load_file_patches_with_start, maybe_refresh_loading_active_resource, navigate_back, - navigate_to_resource, parse_resource_args, prepare_restored_initial_fetch, - resource_count_label, save_open_commands_to_session, session_state_persistable, + event_to_app_event, handle_control_open, handle_intent, has_command_help_arg, + maybe_auto_refresh_with_start, maybe_load_file_patches_with_start, + maybe_refresh_loading_active_resource, navigate_back, navigate_to_resource, + parse_resource_args, prepare_restored_initial_fetch, resource_count_label, + save_open_commands_to_session, session_state_persistable, should_advance_loading_frame, + should_advance_scrollbar_fade, should_redraw_after_scrollbar_frame, should_replace_empty_launch_tab, url_open_command, ClipboardPlatform, }; @@ -1902,6 +1987,69 @@ mod tests { ])); } + #[test] + fn idle_resource_does_not_advance_loading_frame() { + let state = AppState::new(issue_resource(1, "Idle issue")); + + assert!(!should_advance_loading_frame(&state)); + } + + #[test] + fn loading_resource_advances_loading_frame() { + let mut state = AppState::new(loading_resource_placeholder( + ResourceId::parse("owner/repo#1").unwrap(), + )); + + assert!(should_advance_loading_frame(&state)); + + state.replace_resource_reset_view(issue_resource(1, "Loaded issue")); + state.begin_loading( + ResourceId::parse("owner/repo#1").unwrap(), + "refreshing owner/repo#1 from GitHub", + ); + + assert!(should_advance_loading_frame(&state)); + } + + #[test] + fn failed_loading_resource_does_not_advance_loading_frame() { + let mut state = AppState::new(loading_resource_placeholder( + ResourceId::parse("owner/repo#1").unwrap(), + )); + state.last_error = Some("network down".into()); + + assert!(!should_advance_loading_frame(&state)); + } + + #[test] + fn scrollbar_fade_keeps_redrawing_until_hidden() { + let mut state = AppState::new(issue_resource(1, "Scrollable issue")); + + assert!(!should_advance_scrollbar_fade(&state)); + + state.set_scroll_limit(10); + state.scroll_down(1); + + assert!(should_advance_scrollbar_fade(&state)); + assert!(should_redraw_after_scrollbar_frame(false, &state)); + + while should_advance_scrollbar_fade(&state) { + state.advance_scrollbar_visibility(); + } + + assert!(!should_advance_scrollbar_fade(&state)); + assert!(should_redraw_after_scrollbar_frame(true, &state)); + assert!(!should_redraw_after_scrollbar_frame(false, &state)); + } + + #[test] + fn resize_event_requires_pre_event_redraw() { + let pending = event_to_app_event(crossterm::event::Event::Resize(120, 40)).unwrap(); + + assert!(matches!(pending.event, AppEvent::Tick)); + assert!(pending.requires_pre_event_redraw); + } + #[test] fn session_resource_count_label_uses_singular_and_plural() { assert_eq!(resource_count_label(1), "1 resource");