use std::collections::HashSet;
use std::time::Instant;
use ratatui::style::Color;
use super::orchestrator::StartupPlan;
use super::orchestrator::StartupReadiness;
use super::orchestrator::StartupToast;
use super::toast_bodies;
use crate::project;
use crate::project::AbsolutePath;
use crate::project::LanguageStats;
use crate::project::TestCounts;
use crate::tui::app::App;
use crate::tui::app::Startup;
use crate::tui::app::phase_state::FailureReason;
use crate::tui::app::phase_state::PhaseCompletion;
use crate::tui::app::phase_state::ProgressRow;
use crate::tui::app::phase_state::ProgressState;
use crate::tui::app::startup;
use crate::tui::constants::STARTUP_PHASE_CRATES_IO;
use crate::tui::constants::STARTUP_PHASE_DISK;
use crate::tui::constants::STARTUP_PHASE_GIT;
use crate::tui::constants::STARTUP_PHASE_GITHUB;
use crate::tui::constants::STARTUP_PHASE_LANGUAGES;
use crate::tui::constants::STARTUP_PHASE_LINT;
use crate::tui::constants::STARTUP_PHASE_METADATA;
use crate::tui::constants::STARTUP_PHASE_TESTS;
use crate::tui::constants::STARTUP_ROW_DETAIL_DELAY;
use crate::tui::constants::STARTUP_ROW_MIN_VISIBLE;
use crate::tui::constants::STARTUP_ROW_TIMEOUT;
impl Startup {
pub(super) fn log_phase_plan(&self) {
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
disk_expected = self.disk.expected_len(),
git_expected = self.git.expected_len(),
repo_expected = self.repo.expected_len(),
crates_io_expected = self.crates_io.expected_len(),
detail_declaration_expected = self.details_declared.expected_len(),
lint_expected = self.lint_phase.expected_len(),
languages_expected = self.languages.expected_len(),
tests_expected = self.tests.expected_len(),
metadata_expected = self.metadata.expected_len(),
"startup_phase_plan"
);
}
pub(super) fn startup_panel_rows(
&self,
now: Instant,
github_detail: Option<&str>,
crates_io_detail: Option<&str>,
) -> Vec<ProgressRow> {
let phases: [(&'static str, &dyn PhaseCompletion); 8] = [
(STARTUP_PHASE_DISK, &self.disk),
(STARTUP_PHASE_GIT, &self.git),
(STARTUP_PHASE_GITHUB, &self.repo),
(STARTUP_PHASE_CRATES_IO, &self.crates_io),
(STARTUP_PHASE_METADATA, &self.metadata),
(STARTUP_PHASE_LINT, &self.lint_phase),
(STARTUP_PHASE_LANGUAGES, &self.languages),
(STARTUP_PHASE_TESTS, &self.tests),
];
let mut rows: Vec<ProgressRow> = phases
.into_iter()
.filter_map(|(label, phase)| {
let state = phase.progress_state(now, STARTUP_ROW_MIN_VISIBLE)?;
let detail = row_wants_detail(now, phase.first_seen(), state)
.then(|| self.row_detail(label, github_detail, crates_io_detail))
.flatten();
Some(ProgressRow {
label,
state,
detail,
})
})
.collect();
rows.sort_by(|a, b| {
a.label
.bytes()
.map(|byte| byte.to_ascii_lowercase())
.cmp(b.label.bytes().map(|byte| byte.to_ascii_lowercase()))
});
rows
}
fn row_detail(
&self,
label: &str,
github_detail: Option<&str>,
crates_io_detail: Option<&str>,
) -> Option<String> {
let home = |path: &AbsolutePath| project::home_relative_path(path.as_path());
match label {
STARTUP_PHASE_DISK => self.disk.pending_sample(home),
STARTUP_PHASE_GIT => self.git.pending_sample(home),
STARTUP_PHASE_GITHUB => github_detail
.map(ToString::to_string)
.or_else(|| self.repo.pending_sample(ToString::to_string)),
STARTUP_PHASE_CRATES_IO => crates_io_detail
.map(ToString::to_string)
.or_else(|| self.crates_io.pending_sample(Clone::clone)),
STARTUP_PHASE_METADATA => self.metadata.pending_sample(home),
STARTUP_PHASE_TESTS => self.tests.pending_sample(home),
_ => None,
}
}
pub(super) fn all_rows_gate_satisfied(&self, now: Instant) -> bool {
let phases: [&dyn PhaseCompletion; 8] = [
&self.disk,
&self.git,
&self.repo,
&self.crates_io,
&self.metadata,
&self.lint_phase,
&self.languages,
&self.tests,
];
phases
.iter()
.all(|phase| phase.gate_satisfied(now, STARTUP_ROW_MIN_VISIBLE))
}
}
impl App {
pub(super) fn begin_startup_phase_tracker(&mut self, lint_registered: usize) {
let crates_io_plan = self.collect_crates_io_fetch_plan();
let startup_plan = self.build_startup_plan(lint_registered, crates_io_plan.names());
self.initialize_startup_phase_tracker_with_plan(&startup_plan);
if lint_registered == 0 {
self.maybe_complete_startup_lint_cache();
}
self.schedule_startup_project_details(crates_io_plan);
self.schedule_git_first_commit_refreshes();
}
fn build_startup_plan(
&self,
lint_registered: usize,
crates_io_expected: HashSet<String>,
) -> StartupPlan {
let disk_expected = startup::initial_disk_roots(&self.project_list);
let git_expected = self
.project_list
.git_directories()
.into_iter()
.collect::<HashSet<_>>();
let git_seen = self
.project_list
.iter()
.filter(|entry| entry.root_item.git_info().is_some())
.filter_map(|entry| entry.root_item.git_directory())
.collect::<HashSet<_>>();
let metadata_expected = startup::initial_metadata_roots(&self.project_list);
let lint_history = self.lint_history_project_paths();
let mut detail_expected = HashSet::new();
self.project_list.for_each_leaf_path(|path, _| {
detail_expected.insert(AbsolutePath::from(path));
});
StartupPlan {
disk_expected,
git_expected,
git_seen,
metadata_expected,
lint_history,
lint_count_expected: lint_registered,
crates_io_expected,
detail_expected,
github_running: self.net.startup_github_running_repos(),
crates_io_running: self.net.startup_crates_io_running_names(),
}
}
fn initialize_startup_phase_tracker_with_plan(&mut self, startup_plan: &StartupPlan) {
self.reset_startup_phase_state(startup_plan);
self.start_startup_toast();
self.startup.log_phase_plan();
self.maybe_log_startup_phase_completions();
}
#[cfg(test)]
pub fn initialize_startup_phase_tracker(&mut self) {
let crates_io_plan = self.collect_crates_io_fetch_plan();
let mut startup_plan = self.build_startup_plan(0, crates_io_plan.names());
startup_plan.detail_expected.clear();
self.initialize_startup_phase_tracker_with_plan(&startup_plan);
}
pub(super) fn reset_startup_phase_state(&mut self, startup_plan: &StartupPlan) {
self.startup.reset();
self.startup.scan_complete_at = Some(Instant::now());
self.startup.toast = None;
if let Some(planning) = self.startup.take_planning() {
planning.install(&mut self.startup, startup_plan);
}
}
pub(super) fn start_startup_toast(&mut self) {
let now = Instant::now();
self.startup.disk.stamp_first_seen(now);
self.startup.git.stamp_first_seen(now);
self.startup.repo.stamp_first_seen(now);
self.startup.crates_io.stamp_first_seen(now);
self.startup.metadata.stamp_first_seen(now);
self.startup.lint_phase.stamp_first_seen(now);
self.startup.languages.stamp_first_seen(now);
self.startup.tests.stamp_first_seen(now);
let (lines, colors) = self.startup_panel_lines(now);
let toast_id = self
.framework
.toasts
.push_colored_persistent("Startup", lines, colors);
self.startup.toast = Some(StartupToast::new(toast_id));
}
fn startup_panel_lines(&self, now: Instant) -> (Vec<String>, Vec<Color>) {
let github_detail = self.in_flight_github_label();
let crates_io_detail = self.in_flight_crates_io_label();
let width = tui_pane::toast_body_width(self.framework.toast_settings());
let rows = self.startup.startup_panel_rows(
now,
github_detail.as_deref(),
crates_io_detail.as_deref(),
);
toast_bodies::startup_panel_body(&rows, width)
}
fn in_flight_github_label(&self) -> Option<String> {
self.net
.github_running()
.running
.iter()
.min_by_key(|(_, started)| **started)
.map(|(repo, _)| repo.to_string())
}
fn in_flight_crates_io_label(&self) -> Option<String> {
self.net
.crates_io_running()
.running
.iter()
.min_by_key(|(_, started)| **started)
.map(|(name, _)| name.clone())
}
pub fn maybe_log_startup_phase_completions(&mut self) {
let Some(scan_complete_at) = self.startup.scan_complete_at else {
return;
};
if !self.startup.is_collecting() {
return;
}
let now = Instant::now();
self.maybe_complete_startup_disk(now, scan_complete_at);
self.maybe_complete_startup_git(now, scan_complete_at);
self.maybe_complete_startup_repo(now, scan_complete_at);
self.maybe_complete_startup_metadata(now, scan_complete_at);
self.maybe_complete_startup_lint_history(now, scan_complete_at);
self.startup.crates_io.complete_once(now);
self.startup.languages.complete_once(now);
self.startup.tests.complete_once(now);
self.startup.details_declared.complete_once(now);
self.refresh_startup_panel(now);
self.maybe_complete_startup_ready(now, scan_complete_at);
}
pub(super) fn refresh_startup_panel(&mut self, now: Instant) {
let Some(toast) = self.startup.toast else {
return;
};
let (lines, colors) = self.startup_panel_lines(now);
self.framework
.toasts
.update_colored(toast.id(), lines, colors);
}
pub fn tick_startup_panel(&mut self) {
if !self.startup.is_collecting() {
return;
}
let Some(scan_complete_at) = self.startup.scan_complete_at else {
return;
};
let now = Instant::now();
self.sweep_startup_timeouts(now);
self.refresh_startup_panel(now);
self.maybe_complete_startup_ready(now, scan_complete_at);
}
pub(super) fn sweep_startup_timeouts(&mut self, now: Instant) {
let timeout = STARTUP_ROW_TIMEOUT;
let timed_out: [(bool, &'static str); 8] = [
(
self.startup.disk.time_out(now, timeout).is_some(),
STARTUP_PHASE_DISK,
),
(
self.startup.git.time_out(now, timeout).is_some(),
STARTUP_PHASE_GIT,
),
(
self.startup.repo.time_out(now, timeout).is_some(),
STARTUP_PHASE_GITHUB,
),
(
self.startup.crates_io.time_out(now, timeout).is_some(),
STARTUP_PHASE_CRATES_IO,
),
(
self.startup.metadata.time_out(now, timeout).is_some(),
STARTUP_PHASE_METADATA,
),
(
self.startup.lint_phase.time_out(now, timeout).is_some(),
STARTUP_PHASE_LINT,
),
(
self.startup.languages.time_out(now, timeout).is_some(),
STARTUP_PHASE_LANGUAGES,
),
(
self.startup.tests.time_out(now, timeout).is_some(),
STARTUP_PHASE_TESTS,
),
];
for (newly_failed, label) in timed_out {
if newly_failed {
self.show_timed_warning_toast(
"Startup timed out",
format!("{label} did not finish in time"),
);
}
}
}
pub fn fail_startup_repo_phase(&mut self, reason: FailureReason) {
if !self.startup.is_collecting() {
return;
}
let repo = &mut self.startup.repo;
if repo.failure.is_some() || repo.complete_at.is_some() || repo.expected.is_unknown() {
return;
}
repo.failure = Some(reason);
self.maybe_log_startup_phase_completions();
}
pub const fn mark_startup_languages_expected(&mut self, units: usize) {
self.startup.languages.add_work_expected(units);
}
pub fn mark_startup_languages_seen(&mut self, entries: &[(AbsolutePath, LanguageStats)]) {
self.startup.languages.add_work_seen(entries.len());
for (path, _) in entries {
self.startup.languages.seen.insert(path.clone());
}
if self.startup.languages.is_complete() {
self.maybe_log_startup_phase_completions();
}
}
pub fn mark_startup_tests_seen(&mut self, entries: &[(AbsolutePath, TestCounts)]) {
for (path, _) in entries {
self.startup.tests.seen.insert(path.clone());
}
self.maybe_log_startup_phase_completions();
}
pub fn mark_startup_project_details_declared(&mut self, path: AbsolutePath) {
self.startup.details_declared.seen.insert(path);
self.maybe_log_startup_phase_completions();
}
pub fn maybe_complete_startup_disk(&mut self, now: Instant, scan_complete_at: Instant) {
if !self.startup.disk.complete_once(now) {
return;
}
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
phase = "disk_applied",
since_scan_complete_ms =
tui_pane::perf_log_ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.startup.disk.seen.len(),
expected = self.startup.disk.expected_len(),
"startup_phase_complete"
);
}
pub fn maybe_complete_startup_git(&mut self, now: Instant, scan_complete_at: Instant) {
if !self.startup.git.complete_once(now) {
return;
}
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
phase = "git_local_applied",
since_scan_complete_ms =
tui_pane::perf_log_ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.startup.git.seen.len(),
expected = self.startup.git.expected_len(),
"startup_phase_complete"
);
}
pub fn maybe_complete_startup_repo(&mut self, now: Instant, scan_complete_at: Instant) {
if !self.startup.git.is_terminal() {
return;
}
self.startup.repo.expected.stabilize();
if self.net.github.has_repo_fetch_in_flight() {
return;
}
if !self.startup.repo.complete_once(now) {
return;
}
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
phase = "repo_fetch_applied",
since_scan_complete_ms =
tui_pane::perf_log_ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.startup.repo.seen.len(),
expected = self.startup.repo.expected_len(),
"startup_phase_complete"
);
}
pub(super) fn maybe_complete_startup_metadata(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if !self.startup.metadata.complete_once(now) {
return;
}
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
phase = "metadata_applied",
since_scan_complete_ms =
tui_pane::perf_log_ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.startup.metadata.seen.len(),
expected = self.startup.metadata.expected_len(),
"startup_phase_complete"
);
}
pub(super) fn maybe_complete_startup_lint_history(
&mut self,
now: Instant,
scan_complete_at: Instant,
) {
if !self.startup.lint_phase.complete_once(now) {
return;
}
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
phase = "lint_history_applied",
since_scan_complete_ms =
tui_pane::perf_log_ms(now.duration_since(scan_complete_at).as_millis()),
seen = self.startup.lint_phase.seen.len(),
expected = self.startup.lint_phase.expected_len(),
"startup_phase_complete"
);
}
pub fn maybe_complete_startup_ready(&mut self, now: Instant, scan_complete_at: Instant) {
let lint_seen = self.startup.lint_phase.seen.len();
let lint_expected = self.startup.lint_phase.expected_len();
if !self.startup.is_collecting() {
return;
}
let network = self.net.startup_network_readiness(
self.startup.repo.failure.is_some(),
self.startup.crates_io.failure.is_some(),
);
let Some(collecting) = self.startup.take_collecting() else {
return;
};
let ready = match collecting.try_ready(&self.startup, now, scan_complete_at, network) {
StartupReadiness::Ready(ready) => ready,
StartupReadiness::RowsPending(collecting)
| StartupReadiness::DeclarationsPending(collecting) => {
collecting.restore(&mut self.startup);
return;
},
StartupReadiness::NetworkPending {
pending,
collecting,
} => {
tracing::debug!(
github_running = pending.github,
crates_io_running = pending.crates_io,
"startup_waiting_for_network_work"
);
collecting.restore(&mut self.startup);
return;
},
};
self.refresh_startup_panel(now);
let closing = ready.begin_closing(&mut self.startup);
self.net.begin_steady_state_network_toasts(&closing.network);
if let Some(toast) = closing.toast {
self.finish_startup_toast_with_countdown(toast);
}
self.sync_running_crates_io_toast();
self.sync_running_repo_fetch_toast();
self.kick_off_startup_lints();
let since_scan_ms = tui_pane::perf_log_ms(
closing
.completed_at
.duration_since(closing.scan_complete_at)
.as_millis(),
);
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
since_scan_complete_ms = since_scan_ms,
disk_seen = self.startup.disk.seen.len(),
disk_expected = self.startup.disk.expected_len(),
git_seen = self.startup.git.seen.len(),
git_expected = self.startup.git.expected_len(),
repo_seen = self.startup.repo.seen.len(),
repo_expected = self.startup.repo.expected_len(),
lint_seen = lint_seen,
lint_expected = lint_expected,
metadata_seen = self.startup.metadata.seen.len(),
metadata_expected = self.startup.metadata.expected_len(),
"startup_complete"
);
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
since_scan_complete_ms = since_scan_ms,
"steady_state_begin"
);
}
fn finish_startup_toast_with_countdown(&mut self, toast: StartupToast) {
let linger = self.framework.toast_settings().finished_task_visible.get();
self.framework
.toasts
.start_colored_countdown(toast.id(), linger);
self.prune_toasts();
}
}
fn row_wants_detail(now: Instant, first_seen: Option<Instant>, state: ProgressState) -> bool {
let in_progress =
matches!(state, ProgressState::Progress(percentage) if percentage.get() < 100);
in_progress
&& first_seen.is_some_and(|first| now.duration_since(first) >= STARTUP_ROW_DETAIL_DELAY)
}