mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35:36 +00:00
fix(cli): keep idx's own colors; sanitize remaining provider text; no UTF-8 panic
From the Codex re-gate of the 0.3.3 fixes: - Colored signal cells (Bullish/Bearish, ratings, ownership change and flow labels) were painted before the new cell sanitizer, which turned their escape codes into U+FFFD on every color terminal; the goldens run without color and missed it. Labels idx produces itself go into cells untouched (`painted`), with the label made safe before styling. - Statement dates and currency, and the entity name in `ownership entity`, still reached the terminal unsanitized. - The JSON-mode argument handler read std::env::args(), which panics on a non-UTF-8 argument (exit 101 instead of 2); use args_os. - A missing subcommand printed help as plain text in JSON mode; it is an error (exit 2), so it is a JSON error there too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
ec2e870c6a
commit
e3df327f31
4 changed files with 56 additions and 33 deletions
|
|
@ -8,7 +8,7 @@ use crate::config::IdxConfig;
|
|||
use crate::output::json;
|
||||
use crate::output::style::Paint;
|
||||
use crate::output::table::format_idr;
|
||||
use crate::output::text::{cell, terminal_safe};
|
||||
use crate::output::text::{cell, painted, terminal_safe};
|
||||
use idx_core::error::IdxError;
|
||||
use idx_core::investor::{
|
||||
ConcentrationRank, HolderSource, ImportSource, Investor, parse_report_family,
|
||||
|
|
@ -349,7 +349,7 @@ fn handle_ticker(
|
|||
}),
|
||||
cell(format_idr(h.shares)),
|
||||
cell(format_bps(h.percentage_bps)),
|
||||
cell(format_signal(h.signal)),
|
||||
painted(format_signal(h.signal)),
|
||||
]);
|
||||
}
|
||||
println!("{table}");
|
||||
|
|
@ -381,7 +381,7 @@ fn handle_entity(
|
|||
println!(
|
||||
"{} {}",
|
||||
"Entity:".bold(),
|
||||
data.entity.canonical_name.as_str().bold()
|
||||
terminal_safe(&data.entity.canonical_name).bold()
|
||||
);
|
||||
let mut table = Table::new();
|
||||
table
|
||||
|
|
@ -561,7 +561,7 @@ fn print_flow_section(title: &str, rows: &[HolderRow]) {
|
|||
cell(&row.name),
|
||||
cell(format_idr(row.shares)),
|
||||
cell(format_bps(row.percentage_bps)),
|
||||
cell(format_signal(row.signal)),
|
||||
painted(format_signal(row.signal)),
|
||||
]);
|
||||
}
|
||||
println!("{table}");
|
||||
|
|
@ -630,7 +630,7 @@ fn handle_changes(
|
|||
table.add_row(vec![
|
||||
cell(row.ticker_code),
|
||||
cell(row.entity_name),
|
||||
cell(format_change_type(row.change_type)),
|
||||
painted(format_change_type(row.change_type)),
|
||||
cell(
|
||||
row.old_bps
|
||||
.map(format_bps)
|
||||
|
|
|
|||
|
|
@ -91,11 +91,11 @@ fn run() -> Result<(), IdxError> {
|
|||
/// `--version` print as usual.
|
||||
fn argument_error(err: clap::Error) -> IdxError {
|
||||
use clap::error::ErrorKind;
|
||||
// A missing subcommand prints help but is an error (exit 2), so in JSON
|
||||
// mode it is a JSON error too.
|
||||
if matches!(
|
||||
err.kind(),
|
||||
ErrorKind::DisplayHelp
|
||||
| ErrorKind::DisplayVersion
|
||||
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
|
||||
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
|
||||
) || !requested_output_from_args().is_json()
|
||||
{
|
||||
err.exit();
|
||||
|
|
@ -114,7 +114,12 @@ fn argument_error(err: clap::Error) -> IdxError {
|
|||
/// The output format the raw arguments ask for, when they did not parse:
|
||||
/// `-o`/`--output` (any spelling), then `IDX_OUTPUT`, then auto.
|
||||
fn requested_output_from_args() -> OutputFormat {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
// args_os: an argument that is not UTF-8 (itself the error being
|
||||
// reported) must not panic here.
|
||||
let args: Vec<String> = std::env::args_os()
|
||||
.skip(1)
|
||||
.map(|arg| arg.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
let flag = args.iter().enumerate().find_map(|(i, arg)| {
|
||||
if arg == "-o" || arg == "--output" {
|
||||
args.get(i + 1).cloned()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::output::style::Paint;
|
||||
use crate::output::text::{cell, terminal_safe, truncate_chars};
|
||||
use crate::output::text::{cell, painted, terminal_safe, truncate_chars};
|
||||
use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
|
||||
|
||||
use idx_core::analysis::fundamental::{
|
||||
|
|
@ -166,7 +166,7 @@ pub fn print_technical(report: &TechnicalReport, no_color: bool) -> Result<(), I
|
|||
table.add_row(vec![
|
||||
cell("RSI (14)"),
|
||||
cell(format_float(report.rsi14, 2)),
|
||||
cell(format_signal(report.signals.rsi, no_color, false)),
|
||||
painted(format_signal(report.signals.rsi, no_color, false)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("MACD (12,26,9)"),
|
||||
|
|
@ -176,12 +176,12 @@ pub fn print_technical(report: &TechnicalReport, no_color: bool) -> Result<(), I
|
|||
format_float(report.macd.signal, 2),
|
||||
format_float(report.macd.histogram, 2)
|
||||
)),
|
||||
cell(format_signal(report.signals.macd, no_color, false)),
|
||||
painted(format_signal(report.signals.macd, no_color, false)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Trend"),
|
||||
cell(trend_context(report)),
|
||||
cell(format_signal(report.signals.trend, no_color, false)),
|
||||
painted(format_signal(report.signals.trend, no_color, false)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Volume Ratio (20)"),
|
||||
|
|
@ -191,7 +191,7 @@ pub fn print_technical(report: &TechnicalReport, no_color: bool) -> Result<(), I
|
|||
table.add_row(vec![
|
||||
cell("Overall Signal"),
|
||||
cell("-"),
|
||||
cell(format_signal(report.signals.overall, no_color, true)),
|
||||
painted(format_signal(report.signals.overall, no_color, true)),
|
||||
]);
|
||||
|
||||
println!("{table}");
|
||||
|
|
@ -210,17 +210,17 @@ pub fn print_growth(symbol: &str, report: &GrowthReport, no_color: bool) -> Resu
|
|||
table.add_row(vec![
|
||||
cell("Revenue Growth"),
|
||||
cell(format_pct(report.revenue_growth_pct)),
|
||||
cell(format_growth_signal(&report.revenue_signal, no_color)),
|
||||
painted(format_growth_signal(&report.revenue_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Earnings Growth"),
|
||||
cell(format_pct(report.earnings_growth_pct)),
|
||||
cell(format_growth_signal(&report.earnings_signal, no_color)),
|
||||
painted(format_growth_signal(&report.earnings_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Overall"),
|
||||
cell("-"),
|
||||
cell(format_growth_signal(&report.overall_signal, no_color)),
|
||||
painted(format_growth_signal(&report.overall_signal, no_color)),
|
||||
]);
|
||||
|
||||
println!("{table}");
|
||||
|
|
@ -243,7 +243,7 @@ pub fn print_valuation(
|
|||
table.add_row(vec![
|
||||
cell("P/E (Trailing)"),
|
||||
cell(format_opt_f64(report.pe_trailing, 2)),
|
||||
cell(format_valuation_signal(&report.pe_signal, no_color)),
|
||||
painted(format_valuation_signal(&report.pe_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("P/E (Forward)"),
|
||||
|
|
@ -253,27 +253,27 @@ pub fn print_valuation(
|
|||
table.add_row(vec![
|
||||
cell("Price/Book"),
|
||||
cell(format_opt_f64(report.pb, 2)),
|
||||
cell(format_valuation_signal(&report.pb_signal, no_color)),
|
||||
painted(format_valuation_signal(&report.pb_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("ROE"),
|
||||
cell(format_pct(report.roe_pct)),
|
||||
cell(format_valuation_signal(&report.roe_signal, no_color)),
|
||||
painted(format_valuation_signal(&report.roe_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Net Margin"),
|
||||
cell(format_pct(report.net_margin_pct)),
|
||||
cell(format_valuation_signal(&report.margin_signal, no_color)),
|
||||
painted(format_valuation_signal(&report.margin_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("EV/EBITDA"),
|
||||
cell(format_opt_f64(report.ev_ebitda, 2)),
|
||||
cell(format_valuation_signal(&report.ev_ebitda_signal, no_color)),
|
||||
painted(format_valuation_signal(&report.ev_ebitda_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Overall"),
|
||||
cell("-"),
|
||||
cell(format_valuation_signal(&report.overall_signal, no_color)),
|
||||
painted(format_valuation_signal(&report.overall_signal, no_color)),
|
||||
]);
|
||||
|
||||
println!("{table}");
|
||||
|
|
@ -292,12 +292,12 @@ pub fn print_risk(symbol: &str, report: &RiskReport, no_color: bool) -> Result<(
|
|||
table.add_row(vec![
|
||||
cell("Debt/Equity"),
|
||||
cell(format_opt_f64(report.debt_to_equity, 2)),
|
||||
cell(format_risk_signal(&report.de_signal, no_color)),
|
||||
painted(format_risk_signal(&report.de_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("Current Ratio"),
|
||||
cell(format_opt_f64(report.current_ratio, 2)),
|
||||
cell(format_risk_signal(&report.current_ratio_signal, no_color)),
|
||||
painted(format_risk_signal(&report.current_ratio_signal, no_color)),
|
||||
]);
|
||||
table.add_row(vec![
|
||||
cell("ROA"),
|
||||
|
|
@ -307,7 +307,7 @@ pub fn print_risk(symbol: &str, report: &RiskReport, no_color: bool) -> Result<(
|
|||
table.add_row(vec![
|
||||
cell("Overall"),
|
||||
cell("-"),
|
||||
cell(format_risk_signal(&report.overall_signal, no_color)),
|
||||
painted(format_risk_signal(&report.overall_signal, no_color)),
|
||||
]);
|
||||
|
||||
println!("{table}");
|
||||
|
|
@ -632,16 +632,18 @@ fn format_text_signal(
|
|||
positive: &[&str],
|
||||
negative: &[&str],
|
||||
) -> String {
|
||||
// The label comes from the analysis report; make it safe before styling.
|
||||
let label = terminal_safe(signal);
|
||||
if no_color {
|
||||
return signal.to_string();
|
||||
return label;
|
||||
}
|
||||
|
||||
if positive.contains(&signal) {
|
||||
signal.green().to_string()
|
||||
label.green().to_string()
|
||||
} else if negative.contains(&signal) {
|
||||
signal.red().to_string()
|
||||
label.red().to_string()
|
||||
} else {
|
||||
signal.yellow().to_string()
|
||||
label.yellow().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -706,9 +708,12 @@ pub fn print_profile(profile: &CompanyProfile) -> Result<(), IdxError> {
|
|||
|
||||
pub fn print_financials(fin: &FinancialStatements) -> Result<(), IdxError> {
|
||||
let print_section = |label: &str, section: &idx_core::api::types::StatementSection| {
|
||||
println!("\n── {label} ({}) ──", format_table_date(§ion.end_date));
|
||||
println!(
|
||||
"\n── {label} ({}) ──",
|
||||
terminal_safe(&format_table_date(§ion.end_date))
|
||||
);
|
||||
let mut t = Table::new();
|
||||
let value_header = format!("VALUE ({})", section.currency);
|
||||
let value_header = format!("VALUE ({})", terminal_safe(§ion.currency));
|
||||
t.load_preset(UTF8_FULL)
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec!["LINE ITEM", value_header.as_str()]);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ pub fn cell(value: impl ToString) -> Cell {
|
|||
Cell::new(terminal_safe(&value.to_string()))
|
||||
}
|
||||
|
||||
/// A cell for text idx produced itself (labels, numbers) that may carry its
|
||||
/// own color codes, which [`cell`] would strip. Never for provider text.
|
||||
pub fn painted(styled: String) -> Cell {
|
||||
Cell::new(styled)
|
||||
}
|
||||
|
||||
/// At most `max` characters of `text` (never splitting one), with `...`
|
||||
/// appended when it was cut.
|
||||
pub fn truncate_chars(text: &str, max: usize) -> String {
|
||||
|
|
@ -36,7 +42,7 @@ pub fn truncate_chars(text: &str, max: usize) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{terminal_safe, truncate_chars};
|
||||
use super::{painted, terminal_safe, truncate_chars};
|
||||
|
||||
#[test]
|
||||
fn escape_sequences_and_bidi_overrides_are_neutralized() {
|
||||
|
|
@ -57,6 +63,13 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn painted_cells_keep_idx_color_codes() {
|
||||
// comfy-table appends a reset; the color codes themselves survive.
|
||||
let green = "\u{1b}[32mBullish\u{1b}[39m".to_string();
|
||||
assert!(painted(green.clone()).content().starts_with(&green));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_never_splits_a_character() {
|
||||
let text = format!("{}é tail", "a".repeat(199));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue