refactor(cli): remove deprecated stage_spinner, migrate remaining callers to v2

Phase 7 cleanup: migrate timeline.rs and main.rs search spinner
from stage_spinner() to stage_spinner_v2() with proper icon labels,
then remove the now-unused stage_spinner() function and its tests.

No external callers remain for the old numbered-stage API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-14 10:10:22 -05:00
parent 8572f6cc04
commit 361757568f
4 changed files with 47 additions and 71 deletions

View File

@@ -2,7 +2,7 @@ use crate::cli::render::{self, Icons, Theme};
use serde::Serialize; use serde::Serialize;
use crate::Config; use crate::Config;
use crate::cli::progress::stage_spinner; use crate::cli::progress::stage_spinner_v2;
use crate::core::db::create_connection; use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result}; use crate::core::error::{LoreError, Result};
use crate::core::paths::get_db_path; use crate::core::paths::get_db_path;
@@ -96,7 +96,12 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
let seed_result = match parsed_query { let seed_result = match parsed_query {
TimelineQuery::EntityDirect { entity_type, iid } => { TimelineQuery::EntityDirect { entity_type, iid } => {
// Direct seeding: synchronous, no Ollama needed // Direct seeding: synchronous, no Ollama needed
let spinner = stage_spinner(1, 3, "Resolving entity...", params.robot_mode); let spinner = stage_spinner_v2(
Icons::search(),
"Resolve",
"Resolving entity...",
params.robot_mode,
);
let result = seed_timeline_direct(&conn, &entity_type, iid, project_id)?; let result = seed_timeline_direct(&conn, &entity_type, iid, project_id)?;
spinner.finish_and_clear(); spinner.finish_and_clear();
result result
@@ -111,7 +116,12 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
}); });
// Stage 1+2: SEED + HYDRATE (hybrid search with FTS fallback) // Stage 1+2: SEED + HYDRATE (hybrid search with FTS fallback)
let spinner = stage_spinner(1, 3, "Seeding timeline...", params.robot_mode); let spinner = stage_spinner_v2(
Icons::search(),
"Seed",
"Seeding timeline...",
params.robot_mode,
);
let result = seed_timeline( let result = seed_timeline(
&conn, &conn,
Some(&client), Some(&client),
@@ -128,7 +138,12 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
}; };
// Stage 3: EXPAND // Stage 3: EXPAND
let spinner = stage_spinner(2, 3, "Expanding cross-references...", params.robot_mode); let spinner = stage_spinner_v2(
Icons::sync(),
"Expand",
"Expanding cross-references...",
params.robot_mode,
);
let expand_result = expand_timeline( let expand_result = expand_timeline(
&conn, &conn,
&seed_result.seed_entities, &seed_result.seed_entities,
@@ -139,7 +154,12 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
spinner.finish_and_clear(); spinner.finish_and_clear();
// Stage 4: COLLECT // Stage 4: COLLECT
let spinner = stage_spinner(3, 3, "Collecting events...", params.robot_mode); let spinner = stage_spinner_v2(
Icons::sync(),
"Collect",
"Collecting events...",
params.robot_mode,
);
let (events, total_before_limit) = collect_events( let (events, total_before_limit) = collect_events(
&conn, &conn,
&seed_result.seed_entities, &seed_result.seed_entities,

View File

@@ -12,26 +12,6 @@ pub fn multi() -> &'static MultiProgress {
&MULTI &MULTI
} }
/// Create a spinner for a numbered pipeline stage.
///
/// Returns a hidden (no-op) bar in robot mode so callers can use
/// the same code path regardless of output mode.
pub fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressBar {
if robot_mode {
return ProgressBar::hidden();
}
let pb = multi().add(ProgressBar::new_spinner());
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.blue} {prefix} {msg}")
.expect("valid template"),
);
pb.enable_steady_tick(std::time::Duration::from_millis(80));
pb.set_prefix(format!("[{stage}/{total}]"));
pb.set_message(msg.to_string());
pb
}
/// Stage spinner with icon prefix and elapsed time on the right. /// Stage spinner with icon prefix and elapsed time on the right.
/// ///
/// Template: `{spinner:.cyan} {prefix} {wide_msg} {elapsed_style:.dim}` /// Template: `{spinner:.cyan} {prefix} {wide_msg} {elapsed_style:.dim}`
@@ -176,35 +156,7 @@ mod tests {
drop(w); drop(w);
} }
#[test] // ── Progress API tests ──
fn stage_spinner_robot_mode_returns_hidden() {
let pb = stage_spinner(1, 3, "Testing...", true);
assert!(pb.is_hidden());
}
#[test]
fn stage_spinner_human_mode_sets_properties() {
let pb = stage_spinner(1, 3, "Testing...", false);
assert_eq!(pb.prefix(), "[1/3]");
assert_eq!(pb.message(), "Testing...");
pb.finish_and_clear();
}
#[test]
fn stage_spinner_sets_prefix_format() {
let pb = stage_spinner(2, 5, "Working...", false);
assert_eq!(pb.prefix(), "[2/5]");
pb.finish_and_clear();
}
#[test]
fn stage_spinner_sets_message() {
let pb = stage_spinner(1, 3, "Seeding timeline...", false);
assert_eq!(pb.message(), "Seeding timeline...");
pb.finish_and_clear();
}
// ── New progress API tests ──
#[test] #[test]
fn stage_spinner_v2_robot_mode_returns_hidden() { fn stage_spinner_v2_robot_mode_returns_hidden() {

View File

@@ -32,14 +32,10 @@ impl GlyphMode {
/// Precedence: /// Precedence:
/// 1. Explicit `--icons` CLI value (passed as `cli_flag`) /// 1. Explicit `--icons` CLI value (passed as `cli_flag`)
/// 2. `LORE_ICONS` environment variable /// 2. `LORE_ICONS` environment variable
/// 3. Auto-detect: Nerd if `$TERM_PROGRAM` matches known Nerd-capable terminals /// 3. Force ASCII fallback if `force_ascii` is true (robot mode)
/// 4. Auto-detect: Nerd if `$TERM_PROGRAM` matches known Nerd-capable terminals
/// or `$NERD_FONTS=1`; otherwise Unicode /// or `$NERD_FONTS=1`; otherwise Unicode
/// 4. Force ASCII if `force_ascii` is true (robot mode or `--color never`)
pub fn detect(cli_flag: Option<&str>, force_ascii: bool) -> Self { pub fn detect(cli_flag: Option<&str>, force_ascii: bool) -> Self {
if force_ascii {
return Self::Ascii;
}
// 1. CLI flag // 1. CLI flag
if let Some(flag) = cli_flag { if let Some(flag) = cli_flag {
return Self::from_str_lossy(flag); return Self::from_str_lossy(flag);
@@ -50,7 +46,12 @@ impl GlyphMode {
return Self::from_str_lossy(&val); return Self::from_str_lossy(&val);
} }
// 3. Auto-detect // 3. Robot-safe fallback
if force_ascii {
return Self::Ascii;
}
// 4. Auto-detect
if Self::detect_nerd_capable() { if Self::detect_nerd_capable() {
Self::Nerd Self::Nerd
} else { } else {
@@ -1252,8 +1253,14 @@ mod tests {
} }
#[test] #[test]
fn glyph_mode_force_ascii_overrides_cli_flag() { fn glyph_mode_force_ascii_is_fallback_when_no_explicit_icon_mode() {
assert_eq!(GlyphMode::detect(Some("nerd"), true), GlyphMode::Ascii); assert_eq!(GlyphMode::detect(None, true), GlyphMode::Ascii);
}
#[test]
fn glyph_mode_force_ascii_does_not_override_cli_flag() {
assert_eq!(GlyphMode::detect(Some("nerd"), true), GlyphMode::Nerd);
assert_eq!(GlyphMode::detect(Some("unicode"), true), GlyphMode::Unicode);
} }
#[test] #[test]

View File

@@ -144,11 +144,8 @@ async fn main() {
} }
} }
// I1: Respect NO_COLOR convention (https://no-color.org/) // Icon mode is independent of color flags; robot mode still defaults to ASCII.
let force_ascii = robot_mode let glyphs = GlyphMode::detect(cli.icons.as_deref(), robot_mode);
|| cli.color == "never"
|| std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty());
let glyphs = GlyphMode::detect(cli.icons.as_deref(), force_ascii);
if std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) { if std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) {
LoreRenderer::init(ColorMode::Never, glyphs); LoreRenderer::init(ColorMode::Never, glyphs);
@@ -1919,9 +1916,9 @@ async fn handle_search(
limit: args.limit, limit: args.limit,
}; };
let spinner = lore::cli::progress::stage_spinner( let spinner = lore::cli::progress::stage_spinner_v2(
1, lore::cli::render::Icons::search(),
1, "Search",
&format!("Searching ({})...", args.mode), &format!("Searching ({})...", args.mode),
robot_mode, robot_mode,
); );