quinjet 0.0.47

A fast, live, keyboard-first Git source-control interface for the terminal
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::ssh::SshContext;
use crate::tabs::{TabId, TabInfo};
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;
#[doc = " Poll cadences for an open pull request. A run in progress changes state in"]
#[doc = " seconds and is worth watching closely; a settled pull request only needs to"]
#[doc = " notice new comments; a pull request nobody is looking at needs less again."]
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);
#[doc = " Each live stream costs its own GitHub requests, so the tick cadence is a"]
#[doc = " ceiling rather than a schedule: check state is the only thing worth reading"]
#[doc = " as often as the tick fires. Metadata, the conversation and a growing log all"]
#[doc = " change on human or build timescales and hold their own floor."]
const PULL_REQUEST_DETAIL_POLL: Duration = Duration::from_secs(20);
#[doc = " A running job's log grows continuously, so this is a tail interval rather"]
#[doc = " than a staleness bound."]
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(&current.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 projects;
mod pull_request_actions;
mod pull_request_checks;
mod pull_request_diff;
mod pull_request_review;
mod refresh;
mod repository_tabs;
mod scm;
mod selection;
mod support;
mod view;
mod view_state;
mod worker_content;
mod worker_events;
mod worker_repository;

pub(crate) use geometry::*;
pub(crate) use modal::*;
pub(crate) use projects::{ProjectOpenMode, ProjectRow};
pub(crate) use pull_request_actions::*;
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use support::*;
pub(crate) use view::*;
pub(crate) use view_state::*;

#[derive(Debug)]
pub(crate) enum AppEffect {
    Git(Box<WorkerCommand>),
    Copy(String),
    SetMouseCapture(bool),
    Open(OpenTarget),
    SwitchRepository(PathBuf),
    OpenRepositoryTab(PathBuf),
    SwitchSshMachine(crate::ssh::SshSwitch),
    ActivateRepositoryTab(TabId),
    ReorderRepositoryTab { source: TabId, target: TabId },
    CloseRepositoryTab(TabId),
    CloseOtherRepositoryTabs(TabId),
    CloseAllRepositoryTabs,
    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 view_states: ViewStates,
    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 collapsed_project_groups: HashSet<PathBuf>,
    pub ssh_context: Option<SshContext>,
    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>,
    #[doc = " Why the last lookup failed. The pull-request pane renders app state"]
    #[doc = " rather than a document, so a failure needs somewhere to live that"]
    #[doc = " outlasts the toast announcing it."]
    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>,
    #[doc = " `None` keeps the content pane on the pull request itself; selecting a"]
    #[doc = " check replaces it with that run's steps and log."]
    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,
    #[doc = " Set when the step selection moves, and cleared by the draw that acts on"]
    #[doc = " it. Scrolling the selection into view on every frame instead would pin"]
    #[doc = " the pane to the selected step and make its own output unreadable."]
    pub pull_request_step_reveal: bool,
    pub pull_request_content_rows: Vec<PullRequestContentRow>,
    pub pull_request_content_rows_key: Option<(bool, usize, u64, i64)>,
    pub pull_request_content_width: usize,
    pub pull_request_content_links: Vec<PullRequestContentLink>,
    pub pull_request_content_generation: u64,
    #[doc = " Whether the last draw left the content pane scrolled to its end. The"]
    #[doc = " renderer owns the row count, so it reports this back for the one decision"]
    #[doc = " that needs it: whether a growing log should keep following."]
    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 modal_scroll: usize,
    pub modal_free_scroll: bool,
    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 repository_tabs: Vec<TabInfo>,
    pub repository_tab_drag: Option<RepositoryTabDrag>,
    pub repository_tab_menu: Option<RepositoryTabMenu>,
    pub tab_active: bool,
    pub status_generation: u64,
    pub changes_diff_version: u64,
    pub diff_generation: u64,
    pub history_generation: u64,
    pub pull_request_generation: u64,
    #[doc = " Repository discovery answers on its own counter. Sharing the lookup's"]
    #[doc = " would let opening the picker discard a pull request already on its way,"]
    #[doc = " leaving its loading flag set with no reply ever able to clear it."]
    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>,
    #[doc = " The path whose patch currently occupies `document` in single-file view."]
    #[doc = " Tracking it explicitly keeps the cache authoritative about which files"]
    #[doc = " already have a patch, wherever that patch happens to be held."]
    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_change_section: Option<ChangeSection>,
    pub local_diff_preserving_document: bool,
    pub local_diff_preserved_paths: HashSet<PathBuf>,
    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;