cargo-port 0.0.3

A TUI for inspecting and managing Rust projects
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::time::Instant;

use super::App;
use super::CiFetchTracker;
use super::types::AsyncBuildState;
use super::types::BuildChannels;
use super::types::ConfigFileStamp;
use super::types::DirtyState;
use super::types::FinderState;
#[cfg(test)]
use super::types::RetrySpawnMode;
use super::types::ScanState;
use super::types::SelectionPaths;
use super::types::SelectionSync;
use super::types::UiModes;
use crate::config::CargoPortConfig;
use crate::http::HttpClient;
use crate::keymap;
use crate::lint;
use crate::lint::RuntimeHandle;
use crate::project::AbsolutePath;
use crate::project::RootItem;
use crate::project_list::ProjectList;
use crate::scan;
use crate::scan::BackgroundMsg;
use crate::tui::columns::ResolvedWidths;
use crate::tui::terminal::CiFetchMsg;
use crate::tui::terminal::CleanMsg;
use crate::tui::terminal::ExampleMsg;
use crate::tui::toasts::ToastManager;
use crate::tui::types::LayoutCache;
use crate::tui::types::PaneId;
use crate::watcher;
use crate::watcher::WatcherMsg;

pub(super) struct AppChannels {
    example_tx:  mpsc::Sender<ExampleMsg>,
    example_rx:  mpsc::Receiver<ExampleMsg>,
    ci_fetch_tx: mpsc::Sender<CiFetchMsg>,
    ci_fetch_rx: mpsc::Receiver<CiFetchMsg>,
    clean_tx:    mpsc::Sender<CleanMsg>,
    clean_rx:    mpsc::Receiver<CleanMsg>,
}

impl AppChannels {
    fn new() -> Self {
        let (example_tx, example_rx) = mpsc::channel();
        let (ci_fetch_tx, ci_fetch_rx) = mpsc::channel();
        let (clean_tx, clean_rx) = mpsc::channel();
        Self {
            example_tx,
            example_rx,
            ci_fetch_tx,
            ci_fetch_rx,
            clean_tx,
            clean_rx,
        }
    }
}

struct AppInit {
    config_path:      Option<AbsolutePath>,
    config_last_seen: Option<ConfigFileStamp>,
    lint_warning:     Option<String>,
    lint_runtime:     Option<RuntimeHandle>,
    watch_tx:         mpsc::Sender<WatcherMsg>,
    projects:         ProjectList,
}

impl AppInit {
    fn new(
        projects: &[RootItem],
        bg_tx: &mpsc::Sender<BackgroundMsg>,
        cfg: &CargoPortConfig,
        http_client: &HttpClient,
    ) -> Self {
        crate::config::set_active_config(cfg);
        let config_path = crate::config::config_path();
        let config_last_seen = config_path.as_deref().and_then(App::config_file_stamp);
        let lint_spawn = lint::spawn(cfg, bg_tx.clone());
        let watch_roots = scan::resolve_include_dirs(&cfg.tui.include_dirs);
        let watch_tx = watcher::spawn_watcher(
            watch_roots,
            bg_tx.clone(),
            cfg.tui.ci_run_count,
            cfg.tui.include_non_rust,
            http_client.clone(),
            lint_spawn.handle.clone(),
        );
        let built = scan::build_tree(projects, &cfg.tui.inline_dirs);
        let projects = crate::project_list::ProjectList::new(built);

        Self {
            config_path,
            config_last_seen,
            lint_warning: lint_spawn.warning,
            lint_runtime: lint_spawn.handle,
            watch_tx,
            projects,
        }
    }
}

struct CoreInputs {
    http_client:     HttpClient,
    bg_tx:           mpsc::Sender<BackgroundMsg>,
    bg_rx:           Receiver<BackgroundMsg>,
    cfg:             CargoPortConfig,
    scan_started_at: Instant,
    builds:          AsyncBuildState,
    channels:        AppChannels,
    init:            AppInit,
    status_flash:    Option<(String, Instant)>,
}

impl App {
    pub(in super::super) fn has_cached_non_rust_projects(&self) -> bool {
        let mut found = false;
        self.projects.for_each_leaf(|item| {
            if !item.is_rust() {
                found = true;
            }
        });
        found
    }

    pub(in super::super) fn new(
        projects: &[RootItem],
        bg_tx: mpsc::Sender<BackgroundMsg>,
        bg_rx: Receiver<BackgroundMsg>,
        cfg: &CargoPortConfig,
        http_client: HttpClient,
        scan_started_at: Instant,
    ) -> Self {
        let channels = AppChannels::new();
        let builds = AsyncBuildState::new(BuildChannels::new());
        let init = AppInit::new(projects, &bg_tx, cfg, &http_client);
        let status_flash = init.lint_warning.clone().map(|w| (w, Instant::now()));
        let mut app = Self::build_core(CoreInputs {
            http_client,
            bg_tx,
            bg_rx,
            cfg: cfg.clone(),
            scan_started_at,
            builds,
            channels,
            init,
            status_flash,
        });
        app.finish_new();
        app
    }

    fn build_core(inputs: CoreInputs) -> Self {
        let init = inputs.init;
        let channels = inputs.channels;
        let cached_fit_widths = ResolvedWidths::new(inputs.cfg.lint.enabled);
        Self {
            current_config: inputs.cfg,
            http_client: inputs.http_client,
            repo_fetch_cache: crate::scan::new_repo_cache(),
            projects: init.projects,
            ci_fetch_tracker: CiFetchTracker::default(),
            ci_display_modes: HashMap::new(),
            lint_cache_usage: crate::lint::CacheUsage::default(),
            cargo_active_paths: HashSet::new(),
            discovery_shimmers: HashMap::new(),
            pending_git_first_commit: HashMap::new(),
            bg_tx: inputs.bg_tx,
            bg_rx: inputs.bg_rx,
            priority_fetch_path: None,
            expanded: HashSet::new(),
            pane_manager: crate::tui::panes::PaneManager::new(),
            settings_edit_buf: String::new(),
            settings_edit_cursor: 0,
            focused_pane: PaneId::ProjectList,
            return_focus: None,
            visited_panes: std::iter::once(PaneId::ProjectList).collect(),
            pending_example_run: None,
            pending_ci_fetch: None,
            pending_cleans: VecDeque::new(),
            confirm: None,
            animation_started: Instant::now(),
            ci_fetch_tx: channels.ci_fetch_tx,
            ci_fetch_rx: channels.ci_fetch_rx,
            clean_tx: channels.clean_tx,
            clean_rx: channels.clean_rx,
            example_running: None,
            example_child: Arc::new(Mutex::new(None)),
            example_output: Vec::new(),
            example_tx: channels.example_tx,
            example_rx: channels.example_rx,
            running_clean_paths: HashSet::new(),
            clean_toast: None,
            running_lint_paths: HashMap::new(),
            lint_toast: None,
            ci_fetch_toast: None,
            watch_tx: init.watch_tx,
            lint_runtime: init.lint_runtime,
            unreachable_services: HashSet::new(),
            service_retry_active: HashSet::new(),
            #[cfg(test)]
            retry_spawn_mode: RetrySpawnMode::Enabled,
            selection_paths: SelectionPaths::new(),
            finder: FinderState::new(),
            cached_visible_rows: Vec::new(),
            cached_root_sorted: Vec::new(),
            cached_child_sorted: HashMap::new(),
            cached_fit_widths,
            builds: inputs.builds,
            data_generation: 0,
            detail_generation: 0,
            detail_cache_key: None,
            mouse_pos: None,
            hovered_pane_row: None,
            layout_cache: LayoutCache::default(),
            status_flash: inputs.status_flash,
            toasts: ToastManager::default(),
            config_path: init.config_path,
            config_last_seen: init.config_last_seen,
            current_keymap: keymap::ResolvedKeymap::defaults(),
            keymap_path: keymap::keymap_path(),
            keymap_last_seen: None,
            keymap_diagnostics_id: None,
            inline_error: None,
            ui_modes: UiModes::default(),
            dirty: DirtyState::initial(),
            scan: ScanState::new(inputs.scan_started_at),
            selection: SelectionSync::Stable,
        }
    }

    fn finish_new(&mut self) {
        self.load_initial_keymap();
        if let Some(warning) = self
            .status_flash
            .as_ref()
            .map(|(warning, _)| warning.clone())
        {
            self.show_timed_toast("Lint runtime", warning);
        }
        self.force_settings_if_unconfigured();
        self.recompute_cargo_active_paths();
        self.prune_inactive_project_state();
        self.register_existing_projects();
        if !self.projects.is_empty() {
            self.finish_watcher_registration_batch();
        }
        self.refresh_lint_runs_from_disk();
    }
}