cargo-port 0.0.3

A TUI for inspecting and managing Rust projects
mod async_tasks;
mod ci;
mod construct;
mod dismiss;
mod focus;
mod lint;
mod navigation;
mod query;
mod snapshots;
mod types;

use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc;
use std::time::Instant;

use ratatui::layout::Position;

use crate::ci::CiRun;
use crate::ci::OwnerRepo;
use crate::config::CargoPortConfig;
use crate::http::HttpClient;
use crate::http::ServiceKind;
use crate::keymap::ResolvedKeymap;
use crate::lint::CacheUsage;
use crate::lint::LintRuns;
use crate::lint::RuntimeHandle;
use crate::project::AbsolutePath;
use crate::project::ProjectCiData;
use crate::project_list::ProjectList;
use crate::scan;
use crate::scan::BackgroundMsg;
use crate::scan::RepoCache;
use crate::watcher::WatcherMsg;

#[cfg(test)]
#[allow(
    clippy::expect_used,
    reason = "tests should panic on unexpected values"
)]
#[allow(
    clippy::unwrap_used,
    reason = "tests should panic on unexpected values"
)]
#[allow(clippy::panic, reason = "tests should panic on unexpected values")]
mod tests;

pub(super) use dismiss::DismissTarget;
pub(super) use types::CiFetchTracker;
pub(super) use types::ConfirmAction;
pub(super) use types::DiscoveryRowKind;
pub(super) use types::ExpandKey;
pub(super) use types::HoveredPaneRow;
pub(super) use types::PendingClean;
pub(super) use types::PollBackgroundStats;
pub(super) use types::VisibleRow;

pub(super) use super::columns::ResolvedWidths;
use super::detail::PendingCiFetch;
use super::detail::PendingExampleRun;
use super::panes::PaneManager;
use super::terminal::CiFetchMsg;
use super::terminal::CleanMsg;
use super::terminal::ExampleMsg;
use super::toasts::ToastManager;
use super::toasts::ToastTaskId;
use super::types::LayoutCache;
use super::types::PaneId;

pub(super) struct App {
    current_config:           CargoPortConfig,
    http_client:              HttpClient,
    repo_fetch_cache:         RepoCache,
    projects:                 ProjectList,
    ci_fetch_tracker:         CiFetchTracker,
    ci_display_modes:         HashMap<AbsolutePath, types::CiRunDisplayMode>,
    lint_cache_usage:         CacheUsage,
    cargo_active_paths:       HashSet<AbsolutePath>,
    discovery_shimmers:       HashMap<AbsolutePath, types::DiscoveryShimmer>,
    pending_git_first_commit: HashMap<AbsolutePath, String>,
    bg_tx:                    mpsc::Sender<BackgroundMsg>,
    bg_rx:                    mpsc::Receiver<BackgroundMsg>,
    priority_fetch_path:      Option<AbsolutePath>,
    expanded:                 HashSet<ExpandKey>,
    pane_manager:             PaneManager,
    settings_edit_buf:        String,
    settings_edit_cursor:     usize,
    focused_pane:             PaneId,
    return_focus:             Option<PaneId>,
    visited_panes:            HashSet<PaneId>,
    pending_example_run:      Option<PendingExampleRun>,
    pending_ci_fetch:         Option<PendingCiFetch>,
    pending_cleans:           VecDeque<PendingClean>,
    confirm:                  Option<ConfirmAction>,
    animation_started:        Instant,
    ci_fetch_tx:              mpsc::Sender<CiFetchMsg>,
    ci_fetch_rx:              mpsc::Receiver<CiFetchMsg>,
    clean_tx:                 mpsc::Sender<CleanMsg>,
    clean_rx:                 mpsc::Receiver<CleanMsg>,
    example_running:          Option<String>,
    example_child:            Arc<Mutex<Option<u32>>>,
    example_output:           Vec<String>,
    example_tx:               mpsc::Sender<ExampleMsg>,
    example_rx:               mpsc::Receiver<ExampleMsg>,
    running_clean_paths:      HashSet<AbsolutePath>,
    clean_toast:              Option<ToastTaskId>,
    running_lint_paths:       HashMap<AbsolutePath, Instant>,
    lint_toast:               Option<ToastTaskId>,
    ci_fetch_toast:           Option<ToastTaskId>,
    watch_tx:                 mpsc::Sender<WatcherMsg>,
    lint_runtime:             Option<RuntimeHandle>,
    unreachable_services:     HashSet<ServiceKind>,
    service_retry_active:     HashSet<ServiceKind>,
    selection_paths:          types::SelectionPaths,
    finder:                   types::FinderState,
    cached_visible_rows:      Vec<VisibleRow>,
    cached_root_sorted:       Vec<u64>,
    cached_child_sorted:      HashMap<usize, Vec<u64>>,
    cached_fit_widths:        ResolvedWidths,
    builds:                   types::AsyncBuildState,
    data_generation:          u64,
    detail_generation:        u64,
    detail_cache_key:         Option<types::DetailCacheKey>,
    mouse_pos:                Option<Position>,
    hovered_pane_row:         Option<types::HoveredPaneRow>,
    layout_cache:             LayoutCache,
    status_flash:             Option<(String, std::time::Instant)>,
    toasts:                   ToastManager,
    config_path:              Option<AbsolutePath>,
    config_last_seen:         Option<types::ConfigFileStamp>,
    current_keymap:           ResolvedKeymap,
    keymap_path:              Option<AbsolutePath>,
    keymap_last_seen:         Option<types::ConfigFileStamp>,
    keymap_diagnostics_id:    Option<u64>,
    inline_error:             Option<String>,
    ui_modes:                 types::UiModes,
    dirty:                    types::DirtyState,
    scan:                     types::ScanState,
    selection:                types::SelectionSync,
    #[cfg(test)]
    retry_spawn_mode:         types::RetrySpawnMode,
}

impl App {
    pub(super) const fn current_config(&self) -> &CargoPortConfig { &self.current_config }

    pub(super) const fn current_keymap(&self) -> &ResolvedKeymap { &self.current_keymap }

    pub(super) const fn current_keymap_mut(&mut self) -> &mut ResolvedKeymap {
        &mut self.current_keymap
    }

    pub(super) fn resolved_dirs(&self) -> Vec<AbsolutePath> {
        scan::resolve_include_dirs(&self.current_config.tui.include_dirs)
    }

    pub(super) const fn projects(&self) -> &ProjectList { &self.projects }

    #[cfg(test)]
    pub(super) const fn projects_mut(&mut self) -> &mut ProjectList { &mut self.projects }

    pub(super) const fn repo_fetch_cache(&self) -> &RepoCache { &self.repo_fetch_cache }

    pub(in super::super) fn complete_ci_fetch_for(&mut self, path: &Path) -> bool {
        self.ci_fetch_tracker.complete(path)
    }

    pub(in super::super) fn replace_ci_data_for_path(
        &mut self,
        path: &Path,
        ci_data: ProjectCiData,
    ) {
        if let Some(project) = self.projects.at_path_mut(path) {
            project.ci_data = ci_data;
        }
    }

    pub(in super::super) fn start_ci_fetch_for(&mut self, path: AbsolutePath) {
        self.ci_fetch_tracker.start(path);
    }

    pub(super) const fn lint_cache_usage(&self) -> &CacheUsage { &self.lint_cache_usage }

    pub(super) fn lint_at_path(&self, path: &Path) -> Option<&LintRuns> {
        self.projects.lint_at_path(path)
    }

    pub(super) fn lint_at_path_mut(&mut self, path: &Path) -> Option<&mut LintRuns> {
        self.projects.lint_at_path_mut(path)
    }

    pub(super) fn clear_all_lint_state(&mut self) {
        let mut paths = Vec::new();
        self.projects.for_each_leaf_path(|path, is_rust| {
            if is_rust {
                paths.push(path.to_path_buf());
            }
        });
        for path in &paths {
            if let Some(lr) = self.projects.lint_at_path_mut(path) {
                lr.clear_runs();
            }
        }
    }

    pub(super) const fn layout_cache(&self) -> &LayoutCache { &self.layout_cache }

    pub(super) const fn layout_cache_mut(&mut self) -> &mut LayoutCache { &mut self.layout_cache }

    pub(super) const fn mouse_pos(&self) -> Option<Position> { self.mouse_pos }

    pub(super) const fn set_mouse_pos(&mut self, pos: Option<Position>) { self.mouse_pos = pos; }

    pub(super) const fn set_hovered_pane_row(
        &mut self,
        hovered_pane_row: Option<types::HoveredPaneRow>,
    ) {
        self.hovered_pane_row = hovered_pane_row;
    }

    pub(super) fn apply_hovered_pane_row(&mut self) {
        self.pane_manager.clear_hover();
        let Some(hovered) = self.hovered_pane_row else {
            return;
        };
        self.pane_manager
            .pane_mut(hovered.pane)
            .set_hovered(Some(hovered.row));
    }

    pub(super) const fn cached_fit_widths(&self) -> &ResolvedWidths { &self.cached_fit_widths }

    pub(super) fn cached_root_sorted(&self) -> &[u64] { &self.cached_root_sorted }

    pub(super) const fn cached_child_sorted(&self) -> &HashMap<usize, Vec<u64>> {
        &self.cached_child_sorted
    }

    pub(super) const fn focused_pane(&self) -> PaneId { self.focused_pane }

    pub(super) const fn expanded(&self) -> &HashSet<ExpandKey> { &self.expanded }

    #[cfg(test)]
    pub(super) const fn expanded_mut(&mut self) -> &mut HashSet<ExpandKey> { &mut self.expanded }

    pub(super) const fn dirty(&self) -> &types::DirtyState { &self.dirty }

    pub(super) const fn dirty_mut(&mut self) -> &mut types::DirtyState { &mut self.dirty }

    pub(super) const fn pane_manager(&self) -> &PaneManager { &self.pane_manager }

    pub(super) const fn pane_manager_mut(&mut self) -> &mut PaneManager { &mut self.pane_manager }

    pub(super) const fn finder(&self) -> &types::FinderState { &self.finder }

    pub(super) const fn finder_mut(&mut self) -> &mut types::FinderState { &mut self.finder }

    pub(super) const fn last_selected_path(&self) -> Option<&AbsolutePath> {
        self.selection_paths.last_selected.as_ref()
    }

    pub(super) fn set_pending_example_run(&mut self, run: PendingExampleRun) {
        self.pending_example_run = Some(run);
    }

    pub(super) const fn take_pending_example_run(&mut self) -> Option<PendingExampleRun> {
        self.pending_example_run.take()
    }

    pub(super) fn set_pending_ci_fetch(&mut self, fetch: PendingCiFetch) {
        self.pending_ci_fetch = Some(fetch);
    }

    pub(super) const fn set_ci_fetch_toast(&mut self, task_id: ToastTaskId) {
        self.ci_fetch_toast = Some(task_id);
    }

    pub(super) const fn take_pending_ci_fetch(&mut self) -> Option<PendingCiFetch> {
        self.pending_ci_fetch.take()
    }

    pub(super) const fn pending_cleans_mut(&mut self) -> &mut VecDeque<PendingClean> {
        &mut self.pending_cleans
    }

    pub(super) fn set_confirm(&mut self, action: ConfirmAction) { self.confirm = Some(action); }

    pub(super) const fn confirm(&self) -> Option<&ConfirmAction> { self.confirm.as_ref() }

    pub(super) fn settings_edit_buf(&self) -> &str { &self.settings_edit_buf }

    pub(super) const fn settings_edit_cursor(&self) -> usize { self.settings_edit_cursor }

    pub(super) const fn settings_edit_parts_mut(&mut self) -> (&mut String, &mut usize) {
        (&mut self.settings_edit_buf, &mut self.settings_edit_cursor)
    }

    pub(super) fn set_settings_edit_state(&mut self, value: String, cursor: usize) {
        self.settings_edit_buf = value;
        self.settings_edit_cursor = cursor;
    }

    pub(super) const fn inline_error(&self) -> Option<&String> { self.inline_error.as_ref() }

    pub(super) fn set_inline_error(&mut self, error: impl Into<String>) {
        self.inline_error = Some(error.into());
    }

    pub(super) fn clear_inline_error(&mut self) { self.inline_error = None; }

    pub(super) fn bg_tx(&self) -> mpsc::Sender<BackgroundMsg> { self.bg_tx.clone() }

    pub(super) fn http_client(&self) -> HttpClient { self.http_client.clone() }

    pub(super) fn ci_fetch_tx(&self) -> mpsc::Sender<CiFetchMsg> { self.ci_fetch_tx.clone() }

    pub(super) fn clean_tx(&self) -> mpsc::Sender<CleanMsg> { self.clean_tx.clone() }

    pub(super) fn example_tx(&self) -> mpsc::Sender<ExampleMsg> { self.example_tx.clone() }

    pub(super) fn example_child(&self) -> Arc<Mutex<Option<u32>>> {
        Arc::clone(&self.example_child)
    }

    pub(super) fn example_output(&self) -> &[String] { &self.example_output }

    pub(super) fn set_example_output(&mut self, output: Vec<String>) {
        let was_empty = self.example_output.is_empty();
        self.example_output = output;
        if was_empty && !self.example_output.is_empty() {
            self.focus_pane(PaneId::Output);
        }
    }

    pub(super) const fn example_output_mut(&mut self) -> &mut Vec<String> {
        &mut self.example_output
    }

    pub(super) fn example_running(&self) -> Option<&str> { self.example_running.as_deref() }

    pub(super) fn set_example_running(&mut self, running: Option<String>) {
        self.example_running = running;
    }

    pub(super) const fn increment_data_generation(&mut self) { self.data_generation += 1; }

    pub(super) const fn increment_detail_generation(&mut self) { self.detail_generation += 1; }

    pub(super) const fn config_path(&self) -> Option<&AbsolutePath> { self.config_path.as_ref() }

    pub(super) const fn keymap_path(&self) -> Option<&AbsolutePath> { self.keymap_path.as_ref() }

    pub(super) const fn ui_modes(&self) -> &types::UiModes { &self.ui_modes }

    pub(super) const fn take_confirm(&mut self) -> Option<ConfirmAction> { self.confirm.take() }

    #[cfg(test)]
    pub(super) fn set_projects(&mut self, projects: ProjectList) { self.projects = projects; }

    #[cfg(test)]
    pub(super) const fn toasts_mut(&mut self) -> &mut ToastManager { &mut self.toasts }

    pub(super) fn dismiss_target_for_row(&self, row: VisibleRow) -> Option<DismissTarget> {
        self.dismiss_target_for_row_inner(row)
    }

    pub(super) fn owner_repo_for_path(&self, path: &std::path::Path) -> Option<OwnerRepo> {
        self.owner_repo_for_path_inner(path)
    }

    pub(super) fn owner_paths_for_repo(&self, repo: &OwnerRepo) -> Vec<AbsolutePath> {
        self.owner_paths_for_repo_inner(repo)
    }

    pub(super) fn ci_owner_path_for(&self, path: &std::path::Path) -> Option<AbsolutePath> {
        self.ci_owner_path_for_inner(path)
    }

    pub(super) fn ci_display_mode_label_for(&self, path: &std::path::Path) -> &'static str {
        self.ci_display_mode_label_for_inner(path)
    }

    pub(super) fn ci_toggle_available_for(&self, path: &std::path::Path) -> bool {
        self.ci_toggle_available_for_inner(path)
    }

    pub(super) fn toggle_ci_display_mode_for(&mut self, path: &std::path::Path) {
        self.toggle_ci_display_mode_for_inner(path);
    }

    pub(super) fn ci_runs_for_display(&self, path: &std::path::Path) -> Vec<CiRun> {
        self.ci_runs_for_display_inner(path)
    }
}