Fix cli-table ignoring NO_COLOR environment variable

cli-table defaults to ColorChoice::Always, emitting ANSI escape codes
regardless of NO_COLOR. Fix all 5 call sites to:

1. Pass use_color to title cell .bold() instead of hardcoding true
2. Set .color_choice(Never) when colors are disabled
3. Use .display() instead of the free print_stdout/print_stderr
   functions (which re-wrap with Always defaults)

Affected commands: model list, model test, ps list, system df, rewind.
The model test snapshots are now clean plaintext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-30 09:20:12 -04:00
parent e2a8463c2e
commit f01d74c692
No known key found for this signature in database
5 changed files with 146 additions and 103 deletions

View file

@ -1,7 +1,7 @@
use anyhow::Context;
use anyhow::Result;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table, print_stderr};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_config::FabroSettingsExt;
use fabro_git_storage::gitobj::Store;
use fabro_util::terminal::Styles;
@ -61,9 +61,9 @@ pub(crate) fn print_timeline(timeline: &RunTimeline, styles: &Styles) {
let use_color = styles.use_color;
let title = vec![
"@".cell().bold(true),
"Node".cell().bold(true),
"Details".cell().bold(true),
"@".cell().bold(use_color),
"Node".cell().bold(use_color),
"Details".cell().bold(use_color),
];
let rows: Vec<Vec<CellStruct>> = timeline
@ -100,10 +100,19 @@ pub(crate) fn print_timeline(timeline: &RunTimeline, styles: &Styles) {
})
.collect();
let color_choice = if use_color {
cli_table::ColorChoice::Auto
} else {
cli_table::ColorChoice::Never
};
let table = rows
.table()
.title(title)
.color_choice(color_choice)
.border(Border::builder().build())
.separator(Separator::builder().build());
let _ = print_stderr(table);
#[allow(clippy::print_stderr)]
if let Ok(display) = table.display() {
eprintln!("{display}");
}
}

View file

@ -3,7 +3,7 @@ use std::path::Path;
use anyhow::Result;
use chrono::Utc;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table, print_stdout};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_config::FabroSettingsExt;
use fabro_util::terminal::Styles;
@ -68,12 +68,12 @@ pub(crate) async fn list_command(
let use_color = styles.use_color;
let now = Utc::now();
let title = vec![
"RUN ID".cell().bold(true),
"WORKFLOW".cell().bold(true),
"STATUS".cell().bold(true),
"DIRECTORY".cell().bold(true),
"DURATION".cell().bold(true),
"GOAL".cell().bold(true),
"RUN ID".cell().bold(use_color),
"WORKFLOW".cell().bold(use_color),
"STATUS".cell().bold(use_color),
"DIRECTORY".cell().bold(use_color),
"DURATION".cell().bold(use_color),
"GOAL".cell().bold(use_color),
];
let rows: Vec<Vec<CellStruct>> = display_runs
@ -112,12 +112,19 @@ pub(crate) async fn list_command(
})
.collect();
let color_choice = if use_color {
cli_table::ColorChoice::Auto
} else {
cli_table::ColorChoice::Never
};
let table = rows
.table()
.title(title)
.color_choice(color_choice)
.border(Border::builder().build())
.separator(Separator::builder().build());
print_stdout(table)?;
#[allow(clippy::print_stdout)]
println!("{}", table.display()?);
eprintln!("\n{} run(s) listed.", display_runs.len());
Ok(())

View file

@ -3,7 +3,7 @@ use std::path::Path;
use anyhow::Result;
use chrono::{DateTime, Utc};
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table, print_stdout};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_config::FabroSettingsExt;
use fabro_workflows::run_lookup::{logs_base, runs_base, scan_runs_combined};
@ -121,12 +121,19 @@ async fn df_from(
};
let log_reclaim_pct = if total_log_size > 0 { 100 } else { 0 };
let use_color = console::colors_enabled();
let color_choice = if use_color {
cli_table::ColorChoice::Auto
} else {
cli_table::ColorChoice::Never
};
let summary_title = vec![
"TYPE".cell().bold(true),
"COUNT".cell().bold(true).justify(Justify::Right),
"ACTIVE".cell().bold(true).justify(Justify::Right),
"SIZE".cell().bold(true).justify(Justify::Right),
"RECLAIMABLE".cell().bold(true).justify(Justify::Right),
"TYPE".cell().bold(use_color),
"COUNT".cell().bold(use_color).justify(Justify::Right),
"ACTIVE".cell().bold(use_color).justify(Justify::Right),
"SIZE".cell().bold(use_color).justify(Justify::Right),
"RECLAIMABLE".cell().bold(use_color).justify(Justify::Right),
];
let summary_rows: Vec<Vec<CellStruct>> = vec![
vec![
@ -160,9 +167,11 @@ async fn df_from(
let summary_table = summary_rows
.table()
.title(summary_title)
.color_choice(color_choice)
.border(Border::builder().build())
.separator(Separator::builder().build());
print_stdout(summary_table)?;
#[allow(clippy::print_stdout)]
println!("{}", summary_table.display()?);
println!();
println!("Data directory: {}", data_dir.display());
@ -173,11 +182,11 @@ async fn df_from(
println!();
let verbose_title = vec![
"RUN ID".cell().bold(true),
"WORKFLOW".cell().bold(true),
"STATUS".cell().bold(true),
"AGE".cell().bold(true).justify(Justify::Right),
"SIZE".cell().bold(true).justify(Justify::Right),
"RUN ID".cell().bold(use_color),
"WORKFLOW".cell().bold(use_color),
"STATUS".cell().bold(use_color),
"AGE".cell().bold(use_color).justify(Justify::Right),
"SIZE".cell().bold(use_color).justify(Justify::Right),
];
let now = Utc::now();
@ -213,9 +222,11 @@ async fn df_from(
let verbose_table = verbose_rows
.table()
.title(verbose_title)
.color_choice(color_choice)
.border(Border::builder().build())
.separator(Separator::builder().build());
print_stdout(verbose_table)?;
#[allow(clippy::print_stdout)]
println!("{}", verbose_table.display()?);
println!();
println!("* = reclaimable");

View file

@ -36,28 +36,28 @@ fn bare() {
success: true
exit_code: 0
----- stdout -----
 MODEL   PROVIDER   ALIASES   CONTEXT   COST   SPEED 
 claude-opus-4-6   anthropic  opus, claude-opus    1m   $15.0 / $75.0   25 tok/s 
 claude-sonnet-4-5   anthropic      200k   $3.0 / $15.0   50 tok/s 
 claude-sonnet-4-6   anthropic  sonnet, claude-sonnet    200k   $3.0 / $15.0   50 tok/s 
 claude-haiku-4-5   anthropic  haiku, claude-haiku    200k   $0.8 / $4.0   100 tok/s 
 gpt-5.2   openai   gpt5    1m   $1.8 / $14.0   65 tok/s 
 gpt-5-mini   openai   gpt5-mini    1m   $0.2 / $2.0   70 tok/s 
 gpt-5.2-codex   openai       1m   $1.8 / $14.0   100 tok/s 
 gpt-5.3-codex   openai   codex    1m   $1.8 / $14.0   100 tok/s 
 gpt-5.3-codex-spark   openai   codex-spark    131k   - / -  1000 tok/s 
 gpt-5.4   openai   gpt54, gpt-54    1m   $2.5 / $15.0   70 tok/s 
 gpt-5.4-pro   openai   gpt54-pro, gpt-54-pro    1m  $30.0 / $180.0   20 tok/s 
 gpt-5.4-mini   openai   gpt54-mini, gpt-54-mini   400k   $0.8 / $4.5   140 tok/s 
 gemini-3.1-pro-preview   gemini   gemini-pro    1m   $2.0 / $12.0   85 tok/s 
 gemini-3.1-pro-preview-customtools  gemini   gemini-customtools    1m   $2.0 / $12.0   85 tok/s 
 gemini-3-flash-preview   gemini   gemini-flash    1m   $0.5 / $3.0   150 tok/s 
 gemini-3.1-flash-lite-preview   gemini   gemini-flash-lite    1m   $0.2 / $1.5   200 tok/s 
 kimi-k2.5   kimi   kimi    262k   $0.6 / $3.0   50 tok/s 
 glm-4.7   zai   glm, glm4    203k   $0.6 / $2.2   100 tok/s 
 minimax-m2.5   minimax   minimax    197k   $0.3 / $1.2   45 tok/s 
 mercury-2   inception  mercury    131k   $0.2 / $0.8  1000 tok/s 
----- stderr -----
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
gpt-5.2 openai gpt5 1m $1.8 / $14.0 65 tok/s
gpt-5-mini openai gpt5-mini 1m $0.2 / $2.0 70 tok/s
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
gpt-5.4 openai gpt54, gpt-54 1m $2.5 / $15.0 70 tok/s
gpt-5.4-pro openai gpt54-pro, gpt-54-pro 1m $30.0 / $180.0 20 tok/s
gpt-5.4-mini openai gpt54-mini, gpt-54-mini 400k $0.8 / $4.5 140 tok/s
gemini-3.1-pro-preview gemini gemini-pro 1m $2.0 / $12.0 85 tok/s
gemini-3.1-pro-preview-customtools gemini gemini-customtools 1m $2.0 / $12.0 85 tok/s
gemini-3-flash-preview gemini gemini-flash 1m $0.5 / $3.0 150 tok/s
gemini-3.1-flash-lite-preview gemini gemini-flash-lite 1m $0.2 / $1.5 200 tok/s
kimi-k2.5 kimi kimi 262k $0.6 / $3.0 50 tok/s
glm-4.7 zai glm, glm4 203k $0.6 / $2.2 100 tok/s
minimax-m2.5 minimax minimax 197k $0.3 / $1.2 45 tok/s
mercury-2 inception mercury 131k $0.2 / $0.8 1000 tok/s
----- stderr -----
");
}
@ -70,28 +70,28 @@ fn list() {
success: true
exit_code: 0
----- stdout -----
 MODEL   PROVIDER   ALIASES   CONTEXT   COST   SPEED 
 claude-opus-4-6   anthropic  opus, claude-opus    1m   $15.0 / $75.0   25 tok/s 
 claude-sonnet-4-5   anthropic      200k   $3.0 / $15.0   50 tok/s 
 claude-sonnet-4-6   anthropic  sonnet, claude-sonnet    200k   $3.0 / $15.0   50 tok/s 
 claude-haiku-4-5   anthropic  haiku, claude-haiku    200k   $0.8 / $4.0   100 tok/s 
 gpt-5.2   openai   gpt5    1m   $1.8 / $14.0   65 tok/s 
 gpt-5-mini   openai   gpt5-mini    1m   $0.2 / $2.0   70 tok/s 
 gpt-5.2-codex   openai       1m   $1.8 / $14.0   100 tok/s 
 gpt-5.3-codex   openai   codex    1m   $1.8 / $14.0   100 tok/s 
 gpt-5.3-codex-spark   openai   codex-spark    131k   - / -  1000 tok/s 
 gpt-5.4   openai   gpt54, gpt-54    1m   $2.5 / $15.0   70 tok/s 
 gpt-5.4-pro   openai   gpt54-pro, gpt-54-pro    1m  $30.0 / $180.0   20 tok/s 
 gpt-5.4-mini   openai   gpt54-mini, gpt-54-mini   400k   $0.8 / $4.5   140 tok/s 
 gemini-3.1-pro-preview   gemini   gemini-pro    1m   $2.0 / $12.0   85 tok/s 
 gemini-3.1-pro-preview-customtools  gemini   gemini-customtools    1m   $2.0 / $12.0   85 tok/s 
 gemini-3-flash-preview   gemini   gemini-flash    1m   $0.5 / $3.0   150 tok/s 
 gemini-3.1-flash-lite-preview   gemini   gemini-flash-lite    1m   $0.2 / $1.5   200 tok/s 
 kimi-k2.5   kimi   kimi    262k   $0.6 / $3.0   50 tok/s 
 glm-4.7   zai   glm, glm4    203k   $0.6 / $2.2   100 tok/s 
 minimax-m2.5   minimax   minimax    197k   $0.3 / $1.2   45 tok/s 
 mercury-2   inception  mercury    131k   $0.2 / $0.8  1000 tok/s 
----- stderr -----
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
gpt-5.2 openai gpt5 1m $1.8 / $14.0 65 tok/s
gpt-5-mini openai gpt5-mini 1m $0.2 / $2.0 70 tok/s
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
gpt-5.4 openai gpt54, gpt-54 1m $2.5 / $15.0 70 tok/s
gpt-5.4-pro openai gpt54-pro, gpt-54-pro 1m $30.0 / $180.0 20 tok/s
gpt-5.4-mini openai gpt54-mini, gpt-54-mini 400k $0.8 / $4.5 140 tok/s
gemini-3.1-pro-preview gemini gemini-pro 1m $2.0 / $12.0 85 tok/s
gemini-3.1-pro-preview-customtools gemini gemini-customtools 1m $2.0 / $12.0 85 tok/s
gemini-3-flash-preview gemini gemini-flash 1m $0.5 / $3.0 150 tok/s
gemini-3.1-flash-lite-preview gemini gemini-flash-lite 1m $0.2 / $1.5 200 tok/s
kimi-k2.5 kimi kimi 262k $0.6 / $3.0 50 tok/s
glm-4.7 zai glm, glm4 203k $0.6 / $2.2 100 tok/s
minimax-m2.5 minimax minimax 197k $0.3 / $1.2 45 tok/s
mercury-2 inception mercury 131k $0.2 / $0.8 1000 tok/s
----- stderr -----
");
}
@ -104,12 +104,12 @@ fn list_provider() {
success: true
exit_code: 0
----- stdout -----
 MODEL   PROVIDER   ALIASES   CONTEXT   COST   SPEED 
 claude-opus-4-6   anthropic  opus, claude-opus    1m  $15.0 / $75.0   25 tok/s 
 claude-sonnet-4-5  anthropic      200k   $3.0 / $15.0   50 tok/s 
 claude-sonnet-4-6  anthropic  sonnet, claude-sonnet   200k   $3.0 / $15.0   50 tok/s 
 claude-haiku-4-5   anthropic  haiku, claude-haiku    200k   $0.8 / $4.0  100 tok/s 
----- stderr -----
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
----- stderr -----
");
}
@ -122,9 +122,9 @@ fn list_query() {
success: true
exit_code: 0
----- stdout -----
 MODEL   PROVIDER   ALIASES   CONTEXT   COST   SPEED 
 claude-opus-4-6  anthropic  opus, claude-opus   1m  $15.0 / $75.0  25 tok/s 
----- stderr -----
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
----- stderr -----
");
}
@ -137,11 +137,11 @@ fn list_query_aliases() {
success: true
exit_code: 0
----- stdout -----
 MODEL   PROVIDER  ALIASES   CONTEXT   COST   SPEED 
 gpt-5.2-codex   openai       1m  $1.8 / $14.0   100 tok/s 
 gpt-5.3-codex   openai   codex    1m  $1.8 / $14.0   100 tok/s 
 gpt-5.3-codex-spark  openai   codex-spark   131k   - / -  1000 tok/s 
----- stderr -----
MODEL PROVIDER ALIASES CONTEXT COST SPEED
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
----- stderr -----
");
}
@ -154,8 +154,8 @@ fn list_query_case_insensitive() {
success: true
exit_code: 0
----- stdout -----
 MODEL   PROVIDER   ALIASES   CONTEXT   COST   SPEED 
 claude-opus-4-6  anthropic  opus, claude-opus   1m  $15.0 / $75.0  25 tok/s 
----- stderr -----
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
----- stderr -----
");
}

View file

@ -7,7 +7,7 @@ use std::time::Duration;
use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table, print_stdout};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use futures::{StreamExt, stream};
use serde::Deserialize;
use tokio::task;
@ -122,6 +122,14 @@ fn color_if(use_color: bool, color: Color) -> Option<Color> {
if use_color { Some(color) } else { None }
}
fn color_choice(use_color: bool) -> cli_table::ColorChoice {
if use_color {
cli_table::ColorChoice::Auto
} else {
cli_table::ColorChoice::Never
}
}
fn model_row(model: &Model, use_color: bool) -> Vec<CellStruct> {
let aliases = model.aliases.join(", ");
let cost = format!(
@ -149,14 +157,14 @@ fn model_row(model: &Model, use_color: bool) -> Vec<CellStruct> {
]
}
fn models_title() -> Vec<CellStruct> {
fn models_title(use_color: bool) -> Vec<CellStruct> {
vec![
"MODEL".cell().bold(true),
"PROVIDER".cell().bold(true),
"ALIASES".cell().bold(true),
"CONTEXT".cell().bold(true).justify(Justify::Right),
"COST".cell().bold(true).justify(Justify::Right),
"SPEED".cell().bold(true).justify(Justify::Right),
"MODEL".cell().bold(use_color),
"PROVIDER".cell().bold(use_color),
"ALIASES".cell().bold(use_color),
"CONTEXT".cell().bold(use_color).justify(Justify::Right),
"COST".cell().bold(use_color).justify(Justify::Right),
"SPEED".cell().bold(use_color).justify(Justify::Right),
]
}
@ -166,10 +174,14 @@ fn print_models_table(models: &[Model], s: &Styles) {
let rows: Vec<Vec<CellStruct>> = models.iter().map(|m| model_row(m, use_color)).collect();
let table = rows
.table()
.title(models_title())
.title(models_title(use_color))
.color_choice(color_choice(use_color))
.border(Border::builder().build())
.separator(Separator::builder().build());
let _ = print_stdout(table);
#[allow(clippy::print_stdout)]
{
println!("{}", table.display().unwrap());
}
}
fn read_stdin_prompt() -> Option<String> {
@ -948,8 +960,8 @@ async fn test_models_via_server(
}
let use_color = s.use_color;
let mut title = models_title();
title.push("RESULT".cell().bold(true));
let mut title = models_title(use_color);
title.push("RESULT".cell().bold(use_color));
let mut rows: Vec<Vec<CellStruct>> = Vec::new();
let mut failures = 0u32;
@ -985,9 +997,11 @@ async fn test_models_via_server(
let table = rows
.table()
.title(title)
.color_choice(color_choice(use_color))
.border(Border::builder().build())
.separator(Separator::builder().build());
print_stdout(table)?;
#[allow(clippy::print_stdout)]
println!("{}", table.display()?);
if failures > 0 {
bail!("{failures} model(s) failed");
@ -1134,8 +1148,8 @@ async fn test_models(
sorted_results.sort_by_key(|(idx, _, _)| *idx);
let use_color = s.use_color;
let mut title = models_title();
title.push("RESULT".cell().bold(true));
let mut title = models_title(use_color);
title.push("RESULT".cell().bold(use_color));
let mut rows: Vec<Vec<CellStruct>> = Vec::new();
let mut failures = 0u32;
@ -1155,9 +1169,11 @@ async fn test_models(
let table = rows
.table()
.title(title)
.color_choice(color_choice(use_color))
.border(Border::builder().build())
.separator(Separator::builder().build());
print_stdout(table)?;
#[allow(clippy::print_stdout)]
println!("{}", table.display()?);
if failures > 0 {
bail!("{failures} model(s) failed");