refactor(cli): migrate all command modules from console::style to Theme
Replace all console::style() calls in command modules with the centralized Theme API and render:: utility functions. This ensures consistent color behavior across the entire CLI, proper NO_COLOR/--color never support via the LoreRenderer singleton, and eliminates duplicated formatting code. Changes per module: - count.rs: Theme for table headers, render::format_number replacing local duplicate. Removed local format_number implementation. - doctor.rs: Theme::success/warning/error for check status symbols and messages. Unicode escapes for check/warning/cross symbols. - drift.rs: Theme::bold/error/success for drift detection headers and status messages. - embed.rs: Compact output format — headline with count, zero-suppressed detail lines, 'nothing to embed' short-circuit for no-op runs. - generate_docs.rs: Same compact pattern — headline + detail + hint for next step. No-op short-circuit when regenerated==0. - ingest.rs: Theme for project summaries, sync status, dry-run preview. All console::style -> Theme replacements. - list.rs: Replace comfy-table with render::LoreTable for issue/MR listing. Remove local colored_cell, colored_cell_hex, format_relative_time, truncate_with_ellipsis, and format_labels (all moved to render.rs). - list_tests.rs: Update test assertions to use render:: functions. - search.rs: Add render_snippet() for FTS5 <mark> tag highlighting via Theme::bold().underline(). Compact result layout with type badges. - show.rs: Theme for entity detail views, delegate format_date and wrap_text to render module. - stats.rs: Section-based layout using render::section_divider. Compact middle-dot format for document counts. Color-coded embedding coverage percentage (green >=95%, yellow >=50%, red <50%). - sync.rs: Compact sync summary — headline with counts and elapsed time, zero-suppressed detail lines, visually prominent error-only section. - sync_status.rs: Theme for run history headers, removed local format_number duplicate. - timeline.rs: Theme for headers/footers, render:: for date/truncate, standard format! padding replacing console::pad_str. - who.rs: Theme for all expert/workload/active/overlap/review output modes, render:: for relative time and truncation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use console::{Alignment, pad_str, style};
|
||||
use crate::cli::render::{self, Theme};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
@@ -22,7 +22,7 @@ pub struct TimelineParams {
|
||||
pub project: Option<String>,
|
||||
pub since: Option<String>,
|
||||
pub depth: u32,
|
||||
pub expand_mentions: bool,
|
||||
pub no_mentions: bool,
|
||||
pub limit: usize,
|
||||
pub max_seeds: usize,
|
||||
pub max_entities: usize,
|
||||
@@ -133,7 +133,7 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
|
||||
&conn,
|
||||
&seed_result.seed_entities,
|
||||
params.depth,
|
||||
params.expand_mentions,
|
||||
!params.no_mentions,
|
||||
params.max_entities,
|
||||
)?;
|
||||
spinner.finish_and_clear();
|
||||
@@ -171,19 +171,21 @@ pub fn print_timeline(result: &TimelineResult) {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
Theme::bold().render(&format!(
|
||||
"Timeline: \"{}\" ({} events across {} entities)",
|
||||
result.query,
|
||||
result.events.len(),
|
||||
entity_count,
|
||||
))
|
||||
.bold()
|
||||
);
|
||||
println!("{}", "─".repeat(60));
|
||||
println!("{}", "\u{2500}".repeat(60));
|
||||
println!();
|
||||
|
||||
if result.events.is_empty() {
|
||||
println!(" {}", style("No events found for this query.").dim());
|
||||
println!(
|
||||
" {}",
|
||||
Theme::dim().render("No events found for this query.")
|
||||
);
|
||||
println!();
|
||||
return;
|
||||
}
|
||||
@@ -193,12 +195,12 @@ pub fn print_timeline(result: &TimelineResult) {
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "─".repeat(60));
|
||||
println!("{}", "\u{2500}".repeat(60));
|
||||
print_timeline_footer(result);
|
||||
}
|
||||
|
||||
fn print_timeline_event(event: &TimelineEvent) {
|
||||
let date = format_date(event.timestamp);
|
||||
let date = render::format_date(event.timestamp);
|
||||
let tag = format_event_tag(&event.event_type);
|
||||
let entity_ref = format_entity_ref(&event.entity_type, event.entity_iid);
|
||||
let actor = event
|
||||
@@ -208,18 +210,20 @@ fn print_timeline_event(event: &TimelineEvent) {
|
||||
.unwrap_or_default();
|
||||
let expanded_marker = if event.is_seed { "" } else { " [expanded]" };
|
||||
|
||||
let summary = truncate_summary(&event.summary, 50);
|
||||
let tag_padded = pad_str(&tag, 12, Alignment::Left, None);
|
||||
let summary = render::truncate(&event.summary, 50);
|
||||
let tag_padded = format!("{:<12}", tag);
|
||||
println!("{date} {tag_padded} {entity_ref:7} {summary:50} {actor}{expanded_marker}");
|
||||
|
||||
// Show snippet for evidence notes
|
||||
if let TimelineEventType::NoteEvidence { snippet, .. } = &event.event_type
|
||||
&& !snippet.is_empty()
|
||||
{
|
||||
for line in wrap_snippet(snippet, 60) {
|
||||
let mut lines = render::wrap_lines(snippet, 60);
|
||||
lines.truncate(4);
|
||||
for line in lines {
|
||||
println!(
|
||||
" \"{}\"",
|
||||
style(line).dim()
|
||||
Theme::dim().render(&line)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -229,14 +233,14 @@ fn print_timeline_event(event: &TimelineEvent) {
|
||||
let bar = "\u{2500}".repeat(44);
|
||||
println!(" \u{2500}\u{2500} Discussion {bar}");
|
||||
for note in notes {
|
||||
let note_date = format_date(note.created_at);
|
||||
let note_date = render::format_date(note.created_at);
|
||||
let author = note
|
||||
.author
|
||||
.as_deref()
|
||||
.map(|a| format!("@{a}"))
|
||||
.unwrap_or_else(|| "unknown".to_owned());
|
||||
println!(" {} ({note_date}):", style(author).bold());
|
||||
for line in wrap_text(¬e.body, 60) {
|
||||
println!(" {} ({note_date}):", Theme::bold().render(&author));
|
||||
for line in render::wrap_lines(¬e.body, 60) {
|
||||
println!(" {line}");
|
||||
}
|
||||
}
|
||||
@@ -274,20 +278,20 @@ fn print_timeline_footer(result: &TimelineResult) {
|
||||
|
||||
fn format_event_tag(event_type: &TimelineEventType) -> String {
|
||||
match event_type {
|
||||
TimelineEventType::Created => style("CREATED").green().to_string(),
|
||||
TimelineEventType::Created => Theme::success().render("CREATED"),
|
||||
TimelineEventType::StateChanged { state } => match state.as_str() {
|
||||
"closed" => style("CLOSED").red().to_string(),
|
||||
"reopened" => style("REOPENED").yellow().to_string(),
|
||||
_ => style(state.to_uppercase()).dim().to_string(),
|
||||
"closed" => Theme::error().render("CLOSED"),
|
||||
"reopened" => Theme::warning().render("REOPENED"),
|
||||
_ => Theme::dim().render(&state.to_uppercase()),
|
||||
},
|
||||
TimelineEventType::LabelAdded { .. } => style("LABEL+").blue().to_string(),
|
||||
TimelineEventType::LabelRemoved { .. } => style("LABEL-").blue().to_string(),
|
||||
TimelineEventType::MilestoneSet { .. } => style("MILESTONE+").magenta().to_string(),
|
||||
TimelineEventType::MilestoneRemoved { .. } => style("MILESTONE-").magenta().to_string(),
|
||||
TimelineEventType::Merged => style("MERGED").cyan().to_string(),
|
||||
TimelineEventType::NoteEvidence { .. } => style("NOTE").dim().to_string(),
|
||||
TimelineEventType::DiscussionThread { .. } => style("THREAD").yellow().to_string(),
|
||||
TimelineEventType::CrossReferenced { .. } => style("REF").dim().to_string(),
|
||||
TimelineEventType::LabelAdded { .. } => Theme::info().render("LABEL+"),
|
||||
TimelineEventType::LabelRemoved { .. } => Theme::info().render("LABEL-"),
|
||||
TimelineEventType::MilestoneSet { .. } => Theme::accent().render("MILESTONE+"),
|
||||
TimelineEventType::MilestoneRemoved { .. } => Theme::accent().render("MILESTONE-"),
|
||||
TimelineEventType::Merged => Theme::info().render("MERGED"),
|
||||
TimelineEventType::NoteEvidence { .. } => Theme::dim().render("NOTE"),
|
||||
TimelineEventType::DiscussionThread { .. } => Theme::warning().render("THREAD"),
|
||||
TimelineEventType::CrossReferenced { .. } => Theme::dim().render("REF"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,48 +303,6 @@ fn format_entity_ref(entity_type: &str, iid: i64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_date(ms: i64) -> String {
|
||||
let iso = ms_to_iso(ms);
|
||||
iso.split('T').next().unwrap_or(&iso).to_string()
|
||||
}
|
||||
|
||||
fn truncate_summary(s: &str, max: usize) -> String {
|
||||
if s.chars().count() <= max {
|
||||
s.to_owned()
|
||||
} else {
|
||||
let truncated: String = s.chars().take(max - 3).collect();
|
||||
format!("{truncated}...")
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_text(text: &str, width: usize) -> Vec<String> {
|
||||
let mut lines = Vec::new();
|
||||
let mut current = String::new();
|
||||
|
||||
for word in text.split_whitespace() {
|
||||
if current.is_empty() {
|
||||
current = word.to_string();
|
||||
} else if current.len() + 1 + word.len() <= width {
|
||||
current.push(' ');
|
||||
current.push_str(word);
|
||||
} else {
|
||||
lines.push(current);
|
||||
current = word.to_string();
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
lines.push(current);
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn wrap_snippet(text: &str, width: usize) -> Vec<String> {
|
||||
let mut lines = wrap_text(text, width);
|
||||
lines.truncate(4);
|
||||
lines
|
||||
}
|
||||
|
||||
// ─── Robot JSON output ───────────────────────────────────────────────────────
|
||||
|
||||
/// Render timeline as robot-mode JSON in {ok, data, meta} envelope.
|
||||
@@ -348,7 +310,7 @@ pub fn print_timeline_json_with_meta(
|
||||
result: &TimelineResult,
|
||||
total_events_before_limit: usize,
|
||||
depth: u32,
|
||||
expand_mentions: bool,
|
||||
include_mentions: bool,
|
||||
fields: Option<&[String]>,
|
||||
) {
|
||||
let output = TimelineJsonEnvelope {
|
||||
@@ -357,7 +319,7 @@ pub fn print_timeline_json_with_meta(
|
||||
meta: TimelineMetaJson {
|
||||
search_mode: result.search_mode.clone(),
|
||||
expansion_depth: depth,
|
||||
expand_mentions,
|
||||
include_mentions,
|
||||
total_entities: result.seed_entities.len() + result.expanded_entities.len(),
|
||||
total_events: total_events_before_limit,
|
||||
evidence_notes_included: count_evidence_notes(&result.events),
|
||||
@@ -586,7 +548,7 @@ fn event_type_to_json(event_type: &TimelineEventType) -> (String, serde_json::Va
|
||||
struct TimelineMetaJson {
|
||||
search_mode: String,
|
||||
expansion_depth: u32,
|
||||
expand_mentions: bool,
|
||||
include_mentions: bool,
|
||||
total_entities: usize,
|
||||
total_events: usize,
|
||||
evidence_notes_included: usize,
|
||||
|
||||
Reference in New Issue
Block a user