use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::ffi::OsString;
use std::fmt::Write;
use std::hash::Hash;
use std::mem::size_of_val;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use ratatui::text::Line;
use crate::convert::{count, offset};
use crate::git::diff::{DiffDocument, DiffIndex, DiffLineCounts, DiffLineKind, PullRequestDetails};
use crate::git::github::{
CheckRunLog, GitHubRepository, PullRequest, PullRequestCheck, PullRequestCheckStatus,
PullRequestCommentMode, PullRequestConversation, PullRequestDiffIndex, PullRequestEdit,
PullRequestFile, PullRequestFileStatus, PullRequestLockReason, PullRequestMergeMethod,
PullRequestMergeMode, PullRequestOperation, PullRequestProgress, PullRequestReviewDecision,
PullRequestReviewKind, PullRequestReviewOperation, PullRequestReviewSide,
PullRequestReviewSnapshot, PullRequestReviewThread, PullRequestReviewThreadSubject,
PullRequestUpdateMethod, RecentPullRequest,
};
use crate::git::history::Commit;
use crate::git::status::{Change, ChangeArea, ChangeStatus, RepoStatus};
use crate::git::worker::{WorkerCommand, WorkerEvent};
use crate::git::{
Branch, ConflictChoice, GitOperation, HistoryBranch, LocalDiffRequest, ProjectGroup, Stash,
Worktree,
};
use crate::theme::{Appearance, AppearanceChoice, Theme, ThemeName};
const PREVIEW_DEBOUNCE: Duration = Duration::from_millis(45);
const RESIZE_DOUBLE_TAP_INTERVAL: Duration = Duration::from_millis(450);
const TOAST_DURATION: Duration = Duration::from_secs(4);
const HISTORY_PAGE_SIZE: usize = 300;
const PULL_REQUEST_PREFETCH_BATCH: usize = 32;
const PULL_REQUEST_PREFETCH_BYTE_BUDGET: usize = 6 * 1024 * 1024;
const PULL_REQUEST_PATCH_FALLBACK_ESTIMATE: usize = 512 * 1024;
const PULL_REQUEST_PATCH_LINE_ESTIMATE: usize = 80;
const MAX_PREFETCHED_PULL_REQUEST_FILES: usize = 4_096;
const MAX_PULL_REQUEST_DOCUMENT_BYTES: usize = 32 * 1024 * 1024;
const PULL_REQUEST_ACTIVE_POLL: Duration = Duration::from_secs(5);
const PULL_REQUEST_IDLE_POLL: Duration = Duration::from_secs(20);
const PULL_REQUEST_BACKGROUND_POLL: Duration = Duration::from_secs(120);
const PULL_REQUEST_DETAIL_POLL: Duration = Duration::from_secs(20);
const PULL_REQUEST_LOG_POLL: Duration = Duration::from_secs(8);
const MAX_PULL_REQUEST_NUMBER_DIGITS: usize = 20;
const DEFAULT_SIDEBAR_WIDTH: u16 = 42;
const MIN_SIDEBAR_WIDTH: u16 = 22;
const MIN_CONTENT_WIDTH: u16 = 32;
const DEFAULT_DIFF_SPLIT_PERCENT: u16 = 50;
const MIN_DIFF_SPLIT_PERCENT: u16 = 20;
const MAX_DIFF_SPLIT_PERCENT: u16 = 80;
const OPERATION_SPINNER: [&str; 4] = ["◐", "◓", "◑", "◒"];
const fn initial_recent_pull_requests() -> Vec<RecentPullRequest> {
Vec::new()
}
fn recent_pull_requests_for(
recent: Vec<RecentPullRequest>,
repository: &GitHubRepository,
) -> Vec<RecentPullRequest> {
recent
.into_iter()
.filter(|entry| entry.repository.url.eq_ignore_ascii_case(&repository.url))
.collect()
}
fn toggle_membership<T: Eq + Hash>(items: &mut HashSet<T>, item: T) {
if !items.remove(&item) {
items.extend([item]);
}
}
#[cfg(not(test))]
fn cached_recent_pull_requests_for(repository: &GitHubRepository) -> Vec<RecentPullRequest> {
recent_pull_requests_for(crate::git::github::recent_pull_requests(), repository)
}
#[cfg(test)]
const fn cached_recent_pull_requests_for(_repository: &GitHubRepository) -> Vec<RecentPullRequest> {
Vec::new()
}
#[cfg(not(test))]
fn updated_recent_pull_requests(
_existing: &[RecentPullRequest],
pull_request: &PullRequest,
) -> Vec<RecentPullRequest> {
recent_pull_requests_for(
crate::git::github::record_recent_pull_request(pull_request),
&pull_request.base_repository,
)
}
#[cfg(test)]
fn updated_recent_pull_requests(
existing: &[RecentPullRequest],
pull_request: &PullRequest,
) -> Vec<RecentPullRequest> {
let current = RecentPullRequest::from(pull_request);
let mut recent = existing.to_vec();
recent.retain(|entry| {
entry
.repository
.url
.eq_ignore_ascii_case(¤t.repository.url)
&& entry.number != current.number
});
recent.insert(0, current);
recent_pull_requests_for(recent, &pull_request.base_repository)
}
mod changes;
mod dialogs;
mod geometry;
mod initialization;
mod interaction;
mod keyboard;
mod links;
mod live;
mod local_diff;
mod modal;
mod modal_actions;
mod modal_events;
mod modal_forms;
mod modal_pickers;
mod modal_review;
mod mouse;
mod operations;
mod palette;
mod pull_request_actions;
mod pull_request_checks;
mod pull_request_diff;
mod pull_request_review;
mod refresh;
mod scm;
mod selection;
mod support;
mod view;
mod worker_content;
mod worker_events;
mod worker_repository;
pub(crate) use geometry::*;
pub(crate) use modal::*;
pub(crate) use pull_request_actions::*;
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use support::*;
pub(crate) use view::*;
#[derive(Debug)]
pub(crate) enum AppEffect {
Git(Box<WorkerCommand>),
Copy(String),
SetMouseCapture(bool),
Open(OpenTarget),
OpenRepository(PathBuf),
Quit,
}
#[expect(
clippy::struct_excessive_bools,
reason = "each flag is an independent piece of interface state"
)]
pub(crate) struct App {
pub repository_root: PathBuf,
pub repository_name: String,
pub view: View,
pub focus: Focus,
pub diff_layout: DiffLayout,
pub theme: Theme,
pub theme_name: ThemeName,
pub appearance_choice: AppearanceChoice,
pub appearance: Appearance,
pub status: RepoStatus,
pub history: Vec<Commit>,
pub worktrees: Vec<Worktree>,
pub project_groups: Vec<ProjectGroup>,
pub history_branch: Option<HistoryBranch>,
pub pull_request: Option<PullRequest>,
pub github_repositories: Vec<GitHubRepository>,
pub local_github_repository: Option<GitHubRepository>,
pub pull_request_repository: Option<GitHubRepository>,
pub pull_request_warnings: Vec<String>,
pub pull_request_error: Option<String>,
pub pull_request_exact_number: Option<u64>,
pub pull_request_from_cache: bool,
pub history_branches: Vec<HistoryBranch>,
pub history_branches_loading: bool,
pub history_branches_loaded: bool,
pub pull_request_lookup: TextBuffer,
pub pull_request_lookup_active: bool,
pub recent_pull_requests: Vec<RecentPullRequest>,
pub recent_pull_request_cursor: usize,
pub pull_request_section: PullRequestSection,
pub pull_request_file_view: PullRequestFileView,
pub pull_request_files: Vec<PullRequestFile>,
pub pull_request_tree: Vec<PullRequestTreeEntry>,
pub pull_request_total_files: usize,
pub pull_request_files_truncated: bool,
pub pull_request_file_cursor: usize,
pub pull_request_tree_cursor: usize,
pub collapsed_pull_request_directories: HashSet<PathBuf>,
pub pull_request_checks: Vec<PullRequestCheck>,
pub pull_request_check_cursor: Option<usize>,
pub selected_check_section: Option<CheckStatusSection>,
pub collapsed_check_sections: HashSet<CheckStatusSection>,
pub pull_request_checks_loading: bool,
pub pull_request_checks_error: Option<String>,
pub pull_request_checks_from_cache: bool,
pub pull_request_prefetched_logs: HashSet<String>,
pub pull_request_conversation: PullRequestConversation,
pub pull_request_conversation_loading: bool,
pub pull_request_conversation_refresh_again: bool,
pub pull_request_conversation_error: Option<String>,
pub pull_request_review: PullRequestReviewSnapshot,
pub pull_request_review_loading: bool,
pub pull_request_review_mutating: bool,
pub pull_request_review_error: Option<String>,
pub pull_request_review_cursor: Option<PullRequestReviewAnchor>,
pub pull_request_review_line_threads: HashMap<usize, String>,
pub pull_request_check_log: Option<CheckRunLog>,
pub pull_request_check_log_loading: bool,
pub pull_request_check_log_error: Option<String>,
pub expanded_check_steps: HashSet<usize>,
pub pull_request_step_cursor: usize,
pub pull_request_step_reveal: bool,
pub pull_request_content_rows: Vec<PullRequestContentRow>,
pub pull_request_content_rows_key: Option<(bool, usize, u64)>,
pub pull_request_content_width: usize,
pub pull_request_content_links: Vec<PullRequestContentLink>,
pub pull_request_content_generation: u64,
pub content_at_bottom: bool,
pub pull_request_progress: Option<PullRequestProgress>,
pub auxiliary_preview: Option<AuxiliaryPreview>,
pub document: DiffDocument,
pub document_layout_generation: u64,
pub unified_diff_rows: Vec<usize>,
pub side_by_side_diff_rows: Vec<SideBySideRow>,
pub diff_rows_key: Option<(u64, bool)>,
pub selected_change_section: Option<ChangeSection>,
pub collapsed_change_sections: HashSet<ChangeSection>,
pub checked_change_paths: HashSet<PathBuf>,
pub scm_menu_open: bool,
pub scm_menu_selected: usize,
pub pr_menu_open: bool,
pub pr_menu_selected: usize,
pub preferred_merge_method: PullRequestMergeMethod,
pub selected_preview_file: Option<PathBuf>,
pub preview_file_cursor: usize,
pub collapsed_preview_files: HashSet<PathBuf>,
pub expanded_preview_files: HashSet<PathBuf>,
pub change_cursor: usize,
pub history_cursor: usize,
pub sidebar_offset: usize,
pub sidebar_free_scroll: bool,
pub sidebar_last_cursor: Option<usize>,
pub content_scroll: usize,
pub horizontal_scroll: usize,
pub sidebar_width: u16,
pub sidebar_hidden: bool,
pub diff_split_percent: u16,
pub expanded_diff: bool,
pub files_collapsed: bool,
pub collapse_preference_set: bool,
pub resize_target: Option<ResizeTarget>,
pub filter: String,
pub modal: Option<Modal>,
pub toast: Option<Toast>,
pub mouse_capture: bool,
pub mouse_capture_preference: bool,
pub link_hover: Option<(u16, u16)>,
pub text_selection: Option<TextSelection>,
pub rendered_cells: Vec<Vec<char>>,
pub webhooks_listening: bool,
pub busy: Option<String>,
pub operation_frame: usize,
pub refreshing: bool,
pub document_loading: bool,
pub history_loading: bool,
pub history_complete: bool,
pub pull_request_loading: bool,
pub last_refresh: Option<Instant>,
pub geometry: UiGeometry,
pub status_generation: u64,
pub changes_diff_version: u64,
pub diff_generation: u64,
pub history_generation: u64,
pub pull_request_generation: u64,
pub repository_generation: u64,
pub pull_request_workspace_generation: Option<u64>,
pub pull_request_documents: HashMap<PathBuf, DiffDocument>,
pub pull_request_document_order: VecDeque<PathBuf>,
pub pull_request_document_bytes: usize,
pub pull_request_prefetched_paths: HashSet<PathBuf>,
pub pull_request_loading_path: Option<PathBuf>,
pub pull_request_single_file: Option<PathBuf>,
pub pull_request_prefetching: bool,
pub pull_request_prefetch_retrying: bool,
pub pull_request_checks_generation: u64,
pub pull_request_conversation_generation: u64,
pub pull_request_review_generation: u64,
pub pull_request_check_log_generation: u64,
pub pull_request_check_log_target: Option<String>,
pub local_diff_request: Option<LocalDiffRequest>,
pub local_diff_workspace_generation: Option<u64>,
pub local_diff_index: Option<DiffIndex>,
pub local_diff_documents: HashMap<PathBuf, DiffDocument>,
pub local_diff_loading_path: Option<PathBuf>,
pub local_diff_pending_paths: VecDeque<PathBuf>,
pub local_diff_single_loaded: bool,
pub branch_generation: u64,
pub history_branch_generation: u64,
pub stash_generation: u64,
pub worktree_generation: u64,
pub project_generation: u64,
pub operation_id: u64,
pub refresh_again: bool,
pub history_refresh_again: bool,
pub preview_due: Option<Instant>,
pub pull_request_poll_due: Option<Instant>,
pub pull_request_checks_read_at: Option<Instant>,
pub pull_request_detail_read_at: Option<Instant>,
pub pull_request_log_read_at: Option<Instant>,
pub pending_g: Option<Instant>,
pub last_resize_tap: Option<(ResizeTarget, Instant)>,
}
#[cfg(test)]
#[expect(
unused_results,
reason = "test helpers return values the assertions do not use"
)]
mod tests;