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::style;
|
||||
use crate::cli::render::{self, Theme};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -606,65 +606,37 @@ fn get_mr_discussions(conn: &Connection, mr_id: i64) -> Result<Vec<MrDiscussionD
|
||||
}
|
||||
|
||||
fn format_date(ms: i64) -> String {
|
||||
let iso = ms_to_iso(ms);
|
||||
iso.split('T').next().unwrap_or(&iso).to_string()
|
||||
render::format_date(ms)
|
||||
}
|
||||
|
||||
fn wrap_text(text: &str, width: usize, indent: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut current_line = String::new();
|
||||
|
||||
for word in text.split_whitespace() {
|
||||
if current_line.is_empty() {
|
||||
current_line = word.to_string();
|
||||
} else if current_line.len() + 1 + word.len() <= width {
|
||||
current_line.push(' ');
|
||||
current_line.push_str(word);
|
||||
} else {
|
||||
if !result.is_empty() {
|
||||
result.push('\n');
|
||||
result.push_str(indent);
|
||||
}
|
||||
result.push_str(¤t_line);
|
||||
current_line = word.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
if !result.is_empty() {
|
||||
result.push('\n');
|
||||
result.push_str(indent);
|
||||
}
|
||||
result.push_str(¤t_line);
|
||||
}
|
||||
|
||||
result
|
||||
render::wrap_indent(text, width, indent)
|
||||
}
|
||||
|
||||
pub fn print_show_issue(issue: &IssueDetail) {
|
||||
let header = format!("Issue #{}: {}", issue.iid, issue.title);
|
||||
println!("{}", style(&header).bold());
|
||||
println!("{}", "━".repeat(header.len().min(80)));
|
||||
println!("{}", Theme::bold().render(&header));
|
||||
println!("{}", "\u{2501}".repeat(header.len().min(80)));
|
||||
println!();
|
||||
|
||||
println!("Ref: {}", style(&issue.references_full).dim());
|
||||
println!("Project: {}", style(&issue.project_path).cyan());
|
||||
println!("Ref: {}", Theme::dim().render(&issue.references_full));
|
||||
println!("Project: {}", Theme::info().render(&issue.project_path));
|
||||
|
||||
let state_styled = if issue.state == "opened" {
|
||||
style(&issue.state).green()
|
||||
Theme::success().render(&issue.state)
|
||||
} else {
|
||||
style(&issue.state).dim()
|
||||
Theme::dim().render(&issue.state)
|
||||
};
|
||||
println!("State: {}", state_styled);
|
||||
|
||||
if issue.confidential {
|
||||
println!(" {}", style("CONFIDENTIAL").red().bold());
|
||||
println!(" {}", Theme::error().bold().render("CONFIDENTIAL"));
|
||||
}
|
||||
|
||||
if let Some(status) = &issue.status_name {
|
||||
println!(
|
||||
"Status: {}",
|
||||
style_with_hex(status, issue.status_color.as_deref())
|
||||
render::style_with_hex(status, issue.status_color.as_deref())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -705,37 +677,37 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
||||
}
|
||||
|
||||
if issue.labels.is_empty() {
|
||||
println!("Labels: {}", style("(none)").dim());
|
||||
println!("Labels: {}", Theme::dim().render("(none)"));
|
||||
} else {
|
||||
println!("Labels: {}", issue.labels.join(", "));
|
||||
}
|
||||
|
||||
if !issue.closing_merge_requests.is_empty() {
|
||||
println!();
|
||||
println!("{}", style("Development:").bold());
|
||||
println!("{}", Theme::bold().render("Development:"));
|
||||
for mr in &issue.closing_merge_requests {
|
||||
let state_indicator = match mr.state.as_str() {
|
||||
"merged" => style(&mr.state).green(),
|
||||
"opened" => style(&mr.state).cyan(),
|
||||
"closed" => style(&mr.state).red(),
|
||||
_ => style(&mr.state).dim(),
|
||||
"merged" => Theme::success().render(&mr.state),
|
||||
"opened" => Theme::info().render(&mr.state),
|
||||
"closed" => Theme::error().render(&mr.state),
|
||||
_ => Theme::dim().render(&mr.state),
|
||||
};
|
||||
println!(" !{} {} ({})", mr.iid, mr.title, state_indicator);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(url) = &issue.web_url {
|
||||
println!("URL: {}", style(url).dim());
|
||||
println!("URL: {}", Theme::dim().render(url));
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
println!("{}", style("Description:").bold());
|
||||
println!("{}", Theme::bold().render("Description:"));
|
||||
if let Some(desc) = &issue.description {
|
||||
let wrapped = wrap_text(desc, 76, " ");
|
||||
println!(" {}", wrapped);
|
||||
} else {
|
||||
println!(" {}", style("(no description)").dim());
|
||||
println!(" {}", Theme::dim().render("(no description)"));
|
||||
}
|
||||
|
||||
println!();
|
||||
@@ -747,11 +719,11 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
||||
.collect();
|
||||
|
||||
if user_discussions.is_empty() {
|
||||
println!("{}", style("Discussions: (none)").dim());
|
||||
println!("{}", Theme::dim().render("Discussions: (none)"));
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
style(format!("Discussions ({}):", user_discussions.len())).bold()
|
||||
Theme::bold().render(&format!("Discussions ({}):", user_discussions.len()))
|
||||
);
|
||||
println!();
|
||||
|
||||
@@ -762,7 +734,7 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
||||
if let Some(first_note) = user_notes.first() {
|
||||
println!(
|
||||
" {} ({}):",
|
||||
style(format!("@{}", first_note.author_username)).cyan(),
|
||||
Theme::info().render(&format!("@{}", first_note.author_username)),
|
||||
format_date(first_note.created_at)
|
||||
);
|
||||
let wrapped = wrap_text(&first_note.body, 72, " ");
|
||||
@@ -772,7 +744,7 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
||||
for reply in user_notes.iter().skip(1) {
|
||||
println!(
|
||||
" {} ({}):",
|
||||
style(format!("@{}", reply.author_username)).cyan(),
|
||||
Theme::info().render(&format!("@{}", reply.author_username)),
|
||||
format_date(reply.created_at)
|
||||
);
|
||||
let wrapped = wrap_text(&reply.body, 68, " ");
|
||||
@@ -787,24 +759,24 @@ pub fn print_show_issue(issue: &IssueDetail) {
|
||||
pub fn print_show_mr(mr: &MrDetail) {
|
||||
let draft_prefix = if mr.draft { "[Draft] " } else { "" };
|
||||
let header = format!("MR !{}: {}{}", mr.iid, draft_prefix, mr.title);
|
||||
println!("{}", style(&header).bold());
|
||||
println!("{}", "━".repeat(header.len().min(80)));
|
||||
println!("{}", Theme::bold().render(&header));
|
||||
println!("{}", "\u{2501}".repeat(header.len().min(80)));
|
||||
println!();
|
||||
|
||||
println!("Project: {}", style(&mr.project_path).cyan());
|
||||
println!("Project: {}", Theme::info().render(&mr.project_path));
|
||||
|
||||
let state_styled = match mr.state.as_str() {
|
||||
"opened" => style(&mr.state).green(),
|
||||
"merged" => style(&mr.state).magenta(),
|
||||
"closed" => style(&mr.state).red(),
|
||||
_ => style(&mr.state).dim(),
|
||||
"opened" => Theme::success().render(&mr.state),
|
||||
"merged" => Theme::accent().render(&mr.state),
|
||||
"closed" => Theme::error().render(&mr.state),
|
||||
_ => Theme::dim().render(&mr.state),
|
||||
};
|
||||
println!("State: {}", state_styled);
|
||||
|
||||
println!(
|
||||
"Branches: {} -> {}",
|
||||
style(&mr.source_branch).cyan(),
|
||||
style(&mr.target_branch).yellow()
|
||||
Theme::info().render(&mr.source_branch),
|
||||
Theme::warning().render(&mr.target_branch)
|
||||
);
|
||||
|
||||
println!("Author: @{}", mr.author_username);
|
||||
@@ -843,23 +815,23 @@ pub fn print_show_mr(mr: &MrDetail) {
|
||||
}
|
||||
|
||||
if mr.labels.is_empty() {
|
||||
println!("Labels: {}", style("(none)").dim());
|
||||
println!("Labels: {}", Theme::dim().render("(none)"));
|
||||
} else {
|
||||
println!("Labels: {}", mr.labels.join(", "));
|
||||
}
|
||||
|
||||
if let Some(url) = &mr.web_url {
|
||||
println!("URL: {}", style(url).dim());
|
||||
println!("URL: {}", Theme::dim().render(url));
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
println!("{}", style("Description:").bold());
|
||||
println!("{}", Theme::bold().render("Description:"));
|
||||
if let Some(desc) = &mr.description {
|
||||
let wrapped = wrap_text(desc, 76, " ");
|
||||
println!(" {}", wrapped);
|
||||
} else {
|
||||
println!(" {}", style("(no description)").dim());
|
||||
println!(" {}", Theme::dim().render("(no description)"));
|
||||
}
|
||||
|
||||
println!();
|
||||
@@ -871,11 +843,11 @@ pub fn print_show_mr(mr: &MrDetail) {
|
||||
.collect();
|
||||
|
||||
if user_discussions.is_empty() {
|
||||
println!("{}", style("Discussions: (none)").dim());
|
||||
println!("{}", Theme::dim().render("Discussions: (none)"));
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
style(format!("Discussions ({}):", user_discussions.len())).bold()
|
||||
Theme::bold().render(&format!("Discussions ({}):", user_discussions.len()))
|
||||
);
|
||||
println!();
|
||||
|
||||
@@ -890,7 +862,7 @@ pub fn print_show_mr(mr: &MrDetail) {
|
||||
|
||||
println!(
|
||||
" {} ({}):",
|
||||
style(format!("@{}", first_note.author_username)).cyan(),
|
||||
Theme::info().render(&format!("@{}", first_note.author_username)),
|
||||
format_date(first_note.created_at)
|
||||
);
|
||||
let wrapped = wrap_text(&first_note.body, 72, " ");
|
||||
@@ -900,7 +872,7 @@ pub fn print_show_mr(mr: &MrDetail) {
|
||||
for reply in user_notes.iter().skip(1) {
|
||||
println!(
|
||||
" {} ({}):",
|
||||
style(format!("@{}", reply.author_username)).cyan(),
|
||||
Theme::info().render(&format!("@{}", reply.author_username)),
|
||||
format_date(reply.created_at)
|
||||
);
|
||||
let wrapped = wrap_text(&reply.body, 68, " ");
|
||||
@@ -926,39 +898,13 @@ fn print_diff_position(pos: &DiffNotePosition) {
|
||||
|
||||
println!(
|
||||
" {} {}{}",
|
||||
style("📍").dim(),
|
||||
style(file_path).yellow(),
|
||||
style(line_str).dim()
|
||||
Theme::dim().render("\u{1f4cd}"),
|
||||
Theme::warning().render(file_path),
|
||||
Theme::dim().render(&line_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn style_with_hex<'a>(text: &'a str, hex: Option<&str>) -> console::StyledObject<&'a str> {
|
||||
let styled = console::style(text);
|
||||
let Some(hex) = hex else { return styled };
|
||||
let hex = hex.trim_start_matches('#');
|
||||
if hex.len() != 6 {
|
||||
return styled;
|
||||
}
|
||||
let Ok(r) = u8::from_str_radix(&hex[0..2], 16) else {
|
||||
return styled;
|
||||
};
|
||||
let Ok(g) = u8::from_str_radix(&hex[2..4], 16) else {
|
||||
return styled;
|
||||
};
|
||||
let Ok(b) = u8::from_str_radix(&hex[4..6], 16) else {
|
||||
return styled;
|
||||
};
|
||||
styled.color256(ansi256_from_rgb(r, g, b))
|
||||
}
|
||||
|
||||
fn ansi256_from_rgb(r: u8, g: u8, b: u8) -> u8 {
|
||||
let ri = (u16::from(r) * 5 + 127) / 255;
|
||||
let gi = (u16::from(g) * 5 + 127) / 255;
|
||||
let bi = (u16::from(b) * 5 + 127) / 255;
|
||||
(16 + 36 * ri + 6 * gi + bi) as u8
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct IssueDetailJson {
|
||||
pub id: i64,
|
||||
@@ -1387,8 +1333,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_ansi256_from_rgb() {
|
||||
assert_eq!(ansi256_from_rgb(0, 0, 0), 16);
|
||||
assert_eq!(ansi256_from_rgb(255, 255, 255), 231);
|
||||
// Moved to render.rs — keeping basic hex sanity check
|
||||
let result = render::style_with_hex("test", Some("#ff0000"));
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user