fix: clean up styles & colors and define in styles.md (#2401)

New style guide:

  # Headers, primary, and secondary text
  
- **Headers:** Use `bold`. For markdown with various header levels,
leave in the `#` signs.
  - **Primary text:** Default.
  - **Secondary text:** Use `dim`.
  
  # Foreground colors
  
- **Default:** Most of the time, just use the default foreground color.
`reset` can help get it back.
- **Selection:** Use ANSI `blue`. (Ed & AE want to make this cyan too,
but we'll do that in a followup since it's riskier in different themes.)
  - **User input tips and status indicators:** Use ANSI `cyan`.
  - **Success and additions:** Use ANSI `green`.
  - **Errors, failures and deletions:** Use ANSI `red`.
  - **Codex:** Use ANSI `magenta`.
  
  # Avoid
  
- Avoid custom colors because there's no guarantee that they'll contrast
well or look good on various terminal color themes.
- Avoid ANSI `black`, `white`, `yellow` as foreground colors because the
terminal theme will do a better job. (Use `reset` if you need to in
order to get those.) The exception is if you need contrast rendering
over a manually colored background.
  
  (There are some rules to try to catch this in `clippy.toml`.)

# Testing

Tested in a variety of light and dark color themes in Terminal, iTerm2, and Ghostty.
This commit is contained in:
ae
2025-08-18 08:26:29 -07:00
committed by GitHub
parent a269754668
commit 5bce369c4d
10 changed files with 62 additions and 50 deletions

View File

@@ -12,6 +12,10 @@ Before finalizing a change to `codex-rs`, run `just fmt` (in `codex-rs` director
1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`. 1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`.
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`. 2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`.
## TUI style conventions
See `codex-rs/tui/styles.md`.
## TUI code conventions ## TUI code conventions
- Use concise styling helpers from ratatuis Stylize trait. - Use concise styling helpers from ratatuis Stylize trait.

View File

@@ -1,2 +1,9 @@
allow-expect-in-tests = true allow-expect-in-tests = true
allow-unwrap-in-tests = true allow-unwrap-in-tests = true
disallowed-methods = [
{ path = "ratatui::style::Color::Rgb", reason = "Use ANSI colors, which work better in various terminal themes." },
{ path = "ratatui::style::Color::Indexed", reason = "Use ANSI colors, which work better in various terminal themes." },
{ path = "ratatui::style::Stylize::white", reason = "Avoid hardcoding white; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." },
{ path = "ratatui::style::Stylize::black", reason = "Avoid hardcoding black; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." },
{ path = "ratatui::style::Stylize::yellow", reason = "Avoid yellow; prefer other colors in `tui/styles.md`." },
]

View File

@@ -101,7 +101,7 @@ pub(crate) fn render_rows(
if Some(i) == state.selected_idx { if Some(i) == state.selected_idx {
cell = cell.style( cell = cell.style(
Style::default() Style::default()
.fg(Color::Yellow) .fg(Color::Blue)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
); );
} else if *is_current { } else if *is_current {

View File

@@ -1,4 +0,0 @@
use ratatui::style::Color;
pub(crate) const LIGHT_BLUE: Color = Color::Rgb(134, 238, 255);
pub(crate) const SUCCESS_GREEN: Color = Color::Rgb(169, 230, 158);

View File

@@ -1,4 +1,3 @@
use crate::colors::LIGHT_BLUE;
use crate::diff_render::create_diff_summary; use crate::diff_render::create_diff_summary;
use crate::exec_command::relativize_to_home; use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape; use crate::exec_command::strip_bash_lc_and_escape;
@@ -252,12 +251,13 @@ fn new_parsed_command(
lines.push(Line::from(spans)); lines.push(Line::from(spans));
} }
Some(o) if o.exit_code == 0 => { Some(o) if o.exit_code == 0 => {
lines.push(Line::from(" Completed".green().bold())); lines.push(Line::from(vec!["".green(), " Completed".into()]));
} }
Some(o) => { Some(o) => {
lines.push(Line::from( lines.push(Line::from(vec![
format!("✗ Failed (exit {})", o.exit_code).red().bold(), "".red(),
)); format!(" Failed (exit {})", o.exit_code).into(),
]));
} }
}; };
@@ -304,7 +304,7 @@ fn new_parsed_command(
let prefix = if j == 0 { first_prefix } else { " " }; let prefix = if j == 0 { first_prefix } else { " " };
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)), Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)),
Span::styled(line_text.to_string(), Style::default().fg(LIGHT_BLUE)), line_text.to_string().dim(),
])); ]));
} }
} }

View File

@@ -2,6 +2,7 @@
// The standalone `codex-tui` binary prints a short help message before the // The standalone `codex-tui` binary prints a short help message before the
// alternatescreen mode starts; that file optsout locally via `allow`. // alternatescreen mode starts; that file optsout locally via `allow`.
#![deny(clippy::print_stdout, clippy::print_stderr)] #![deny(clippy::print_stdout, clippy::print_stderr)]
#![deny(clippy::disallowed_methods)]
use app::App; use app::App;
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
use codex_core::config::Config; use codex_core::config::Config;
@@ -28,7 +29,6 @@ mod bottom_pane;
mod chatwidget; mod chatwidget;
mod citation_regex; mod citation_regex;
mod cli; mod cli;
mod colors;
mod common; mod common;
pub mod custom_terminal; pub mod custom_terminal;
mod diff_render; mod diff_render;

View File

@@ -9,6 +9,7 @@ use ratatui::prelude::Widget;
use ratatui::style::Color; use ratatui::style::Color;
use ratatui::style::Modifier; use ratatui::style::Modifier;
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line; use ratatui::text::Line;
use ratatui::text::Span; use ratatui::text::Span;
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
@@ -19,8 +20,6 @@ use codex_login::AuthMode;
use crate::app_event::AppEvent; use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender; use crate::app_event_sender::AppEventSender;
use crate::colors::LIGHT_BLUE;
use crate::colors::SUCCESS_GREEN;
use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider; use crate::onboarding::onboarding_screen::StepStateProvider;
use crate::shimmer::shimmer_spans; use crate::shimmer::shimmer_spans;
@@ -131,11 +130,8 @@ impl AuthModeWidget {
let line1 = if is_selected { let line1 = if is_selected {
Line::from(vec![ Line::from(vec![
Span::styled( format!("{} {}. ", caret, idx + 1).blue().dim(),
format!("{} {}. ", caret, idx + 1), text.to_string().blue(),
Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
),
Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
]) ])
} else { } else {
Line::from(format!(" {}. {text}", idx + 1)) Line::from(format!(" {}. {text}", idx + 1))
@@ -143,7 +139,8 @@ impl AuthModeWidget {
let line2 = if is_selected { let line2 = if is_selected {
Line::from(format!(" {description}")) Line::from(format!(" {description}"))
.style(Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM)) .fg(Color::Blue)
.add_modifier(Modifier::DIM)
} else { } else {
Line::from(format!(" {description}")) Line::from(format!(" {description}"))
.style(Style::default().add_modifier(Modifier::DIM)) .style(Style::default().add_modifier(Modifier::DIM))
@@ -197,12 +194,7 @@ impl AuthModeWidget {
lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:")); lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:"));
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::raw(" "), Span::raw(" "),
Span::styled( state.auth_url.as_str().blue().underlined(),
state.auth_url.as_str(),
Style::default()
.fg(LIGHT_BLUE)
.add_modifier(Modifier::UNDERLINED),
),
])); ]));
lines.push(Line::from("")); lines.push(Line::from(""));
} }
@@ -218,8 +210,7 @@ impl AuthModeWidget {
fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) { fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) {
let lines = vec![ let lines = vec![
Line::from("✓ Signed in with your ChatGPT account") Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green),
.style(Style::default().fg(SUCCESS_GREEN)),
Line::from(""), Line::from(""),
Line::from("> Before you start:"), Line::from("> Before you start:"),
Line::from(""), Line::from(""),
@@ -233,8 +224,7 @@ impl AuthModeWidget {
]) ])
.style(Style::default().add_modifier(Modifier::DIM)), .style(Style::default().add_modifier(Modifier::DIM)),
Line::from(""), Line::from(""),
Line::from(" Codex can make mistakes") Line::from(" Codex can make mistakes"),
.style(Style::default().fg(Color::White)),
Line::from(" Review the code it writes and commands it runs") Line::from(" Review the code it writes and commands it runs")
.style(Style::default().add_modifier(Modifier::DIM)), .style(Style::default().add_modifier(Modifier::DIM)),
Line::from(""), Line::from(""),
@@ -248,7 +238,7 @@ impl AuthModeWidget {
]) ])
.style(Style::default().add_modifier(Modifier::DIM)), .style(Style::default().add_modifier(Modifier::DIM)),
Line::from(""), Line::from(""),
Line::from(" Press Enter to continue").style(Style::default().fg(LIGHT_BLUE)), Line::from(" Press Enter to continue").fg(Color::Blue),
]; ];
Paragraph::new(lines) Paragraph::new(lines)
@@ -257,10 +247,7 @@ impl AuthModeWidget {
} }
fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) {
let lines = vec![ let lines = vec![Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green)];
Line::from("✓ Signed in with your ChatGPT account")
.style(Style::default().fg(SUCCESS_GREEN)),
];
Paragraph::new(lines) Paragraph::new(lines)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
@@ -268,8 +255,7 @@ impl AuthModeWidget {
} }
fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) { fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) {
let lines = let lines = vec![Line::from("✓ Using OPENAI_API_KEY").fg(Color::Green)];
vec![Line::from("✓ Using OPENAI_API_KEY").style(Style::default().fg(SUCCESS_GREEN))];
Paragraph::new(lines) Paragraph::new(lines)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })

View File

@@ -18,8 +18,6 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef; use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap; use ratatui::widgets::Wrap;
use crate::colors::LIGHT_BLUE;
use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider; use crate::onboarding::onboarding_screen::StepStateProvider;
@@ -77,13 +75,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> { |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> {
let is_selected = self.highlighted == option; let is_selected = self.highlighted == option;
if is_selected { if is_selected {
Line::from(vec![ Line::from(format!("> {}. {text}", idx + 1)).blue()
Span::styled(
format!("> {}. ", idx + 1),
Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
),
Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
])
} else { } else {
Line::from(format!(" {}. {}", idx + 1, text)) Line::from(format!(" {}. {}", idx + 1, text))
} }

View File

@@ -46,9 +46,14 @@ pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
let brightness = 0.4 + 0.6 * t; let brightness = 0.4 + 0.6 * t;
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8; let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
let style = if has_true_color { let style = if has_true_color {
Style::default() // Allow custom RGB colors, as the implementation is thoughtfully
.fg(Color::Rgb(level, level, level)) // adjusting the level of the default foreground color.
.add_modifier(Modifier::BOLD) #[allow(clippy::disallowed_methods)]
{
Style::default()
.fg(Color::Rgb(level, level, level))
.add_modifier(Modifier::BOLD)
}
} else { } else {
color_for_level(level) color_for_level(level)
}; };

22
codex-rs/tui/styles.md Normal file
View File

@@ -0,0 +1,22 @@
# Headers, primary, and secondary text
- **Headers:** Use `bold`. For markdown with various header levels, leave in the `#` signs.
- **Primary text:** Default.
- **Secondary text:** Use `dim`.
# Foreground colors
- **Default:** Most of the time, just use the default foreground color. `reset` can help get it back.
- **Selection:** Use ANSI `blue`. (Ed & AE want to make this cyan too, but we'll do that in a followup since it's riskier in different themes.)
- **User input tips and status indicators:** Use ANSI `cyan`.
- **Success and additions:** Use ANSI `green`.
- **Errors, failures and deletions:** Use ANSI `red`.
- **Codex:** Use ANSI `magenta`.
# Avoid
- Avoid custom colors because there's no guarantee that they'll contrast well or look good in various terminal color themes.
- Avoid ANSI `black` & `white` as foreground colors because the default terminal theme color will do a better job. (Use `reset` if you need to in order to get those.) The exception is if you need contrast rendering over a manually colored background.
- Avoid ANSI `yellow` because for now the style guide doesn't use it. Prefer a foreground color mentioned above.
(There are some rules to try to catch this in `clippy.toml`.)