Files
codex/codex-rs/tui/src/style.rs
T
Felipe Coury 6b4b15a1ed feat(tui): render markdown tables in app style [1 of 2] (#24489)
## Stack

- **Current: #24489 [1 of 2]** - render markdown tables in app style.
- **Stacked follow-up: #24636 [2 of 2]** - render cramped markdown
tables as key/value records.

## Why

Markdown tables currently render as boxed terminal grids, which gives
ordinary assistant output a heavier visual treatment than surrounding
rich text. This row-separated layout is the best match for how the App
renders tables, while accented headers remain distinguishable even when
a terminal font renders bold subtly.

<table>
<tr><td>
<p align="center">Codex CLI - Before</p>
<img width="1722" height="742" alt="CleanShot 2026-05-25 at 18 46 17"
src="https://github.com/user-attachments/assets/f673d92a-ebd8-46e2-b414-3d985e41b6a4"
/>
</td></tr>
<tr><td>
<p align="center">Codex CLI - After</p>
<img width="1720" height="957" alt="image"
src="https://github.com/user-attachments/assets/36a3d331-bea1-439b-b5be-e97b0731bd6f"
/>
</td></tr>
<tr><td>
<p align="center">Codex App</p>
<img width="979" height="1293" alt="CleanShot 2026-05-25 at 18 45 04"
src="https://github.com/user-attachments/assets/7d97cae0-9256-4f6e-a4b3-8b8f22b0d901"
/>
</td></tr>
</table>

## What Changed

- Render markdown tables as padded, aligned rows without an enclosing
box.
- Style table headers with the active syntax-theme accent plus bold
text, while keeping separators low contrast and theme-aware.
- Use a segmented heavy header rule and thin body-row rules, preserving
wrapping, narrow-width fallback, streaming parity, and rich-history
rendering.
- Update focused assertions and snapshots for the final table layout.

## How to Test

1. Render a markdown table in the TUI with several rows and columns.
2. Confirm the header uses the active theme accent, rows use
one-character interior padding, and the table has no enclosing box.
3. Confirm the header is followed by segmented `━` rules and multiple
body rows are separated by muted segmented `─` rules.
4. Render the same table while streaming and in history/raw-mode
toggles; the final rich layout should remain stable.
5. Render a narrow table with long content and verify wrapping or pipe
fallback does not overflow.

## Validation

- `just test -p codex-tui table`
- `just test -p codex-tui streaming::controller::tests`
- `just argument-comment-lint-from-source -p codex-tui -- --all-targets`
- `just fix -p codex-tui`
- `just fmt`

`just test -p codex-tui` was also run after accepting the snapshots; it
fails only in the unrelated existing guardian app tests
`update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
and
`update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`.
2026-05-27 16:18:24 -03:00

156 lines
4.7 KiB
Rust

use crate::color::blend;
use crate::color::is_light;
use crate::terminal_palette::StdoutColorLevel;
use crate::terminal_palette::best_color;
use crate::terminal_palette::default_bg;
use crate::terminal_palette::default_fg;
use crate::terminal_palette::rgb_color;
use crate::terminal_palette::stdout_color_level;
use ratatui::style::Color;
use ratatui::style::Style;
use ratatui::style::Stylize;
const LIGHT_BG_ACCENT_RGB: (u8, u8, u8) = (0, 95, 135);
// Decorative table rules should remain visible without competing with cell content.
const TABLE_SEPARATOR_FG_ALPHA: f32 = 0.20;
pub fn user_message_style() -> Style {
user_message_style_for(default_bg())
}
pub fn proposed_plan_style() -> Style {
proposed_plan_style_for(default_bg())
}
/// Returns a low-contrast rule style for separators within markdown tables.
pub(crate) fn table_separator_style() -> Style {
table_separator_style_for(default_fg(), default_bg(), stdout_color_level())
}
/// Returns the shared accent style for active or selected TUI controls.
pub(crate) fn accent_style() -> Style {
accent_style_for(default_bg())
}
/// Returns the style for a user-authored message using the provided terminal background.
pub fn user_message_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style {
match terminal_bg {
Some(bg) => Style::default().bg(user_message_bg(bg)),
None => Style::default(),
}
}
pub fn proposed_plan_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style {
match terminal_bg {
Some(bg) => Style::default().bg(proposed_plan_bg(bg)),
None => Style::default(),
}
}
/// Returns the shared accent style for the provided terminal background.
pub(crate) fn accent_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style {
if terminal_bg.is_some_and(is_light) {
Style::default().fg(best_color(LIGHT_BG_ACCENT_RGB)).bold()
} else {
Style::default().fg(Color::Cyan).bold()
}
}
fn table_separator_style_for(
terminal_fg: Option<(u8, u8, u8)>,
terminal_bg: Option<(u8, u8, u8)>,
color_level: StdoutColorLevel,
) -> Style {
let (Some(fg), Some(bg)) = (terminal_fg, terminal_bg) else {
return Style::default().dim();
};
let separator_rgb = blend(fg, bg, TABLE_SEPARATOR_FG_ALPHA);
match color_level {
StdoutColorLevel::TrueColor => Style::default().fg(rgb_color(separator_rgb)),
StdoutColorLevel::Ansi256 => Style::default().fg(best_color(separator_rgb)),
StdoutColorLevel::Ansi16 | StdoutColorLevel::Unknown => Style::default().dim(),
}
}
#[allow(clippy::disallowed_methods)]
pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
let (top, alpha) = if is_light(terminal_bg) {
((0, 0, 0), 0.04)
} else {
((255, 255, 255), 0.12)
};
best_color(blend(top, terminal_bg, alpha))
}
#[allow(clippy::disallowed_methods)]
pub fn proposed_plan_bg(terminal_bg: (u8, u8, u8)) -> Color {
user_message_bg(terminal_bg)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use ratatui::style::Modifier;
#[test]
fn accent_style_uses_darker_cyan_on_light_backgrounds() {
let style = accent_style_for(Some((255, 255, 255)));
assert_eq!(style.fg, Some(best_color(LIGHT_BG_ACCENT_RGB)));
assert!(style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn accent_style_uses_cyan_on_dark_or_unknown_backgrounds() {
let expected = Style::default().fg(Color::Cyan).bold();
assert_eq!(accent_style_for(Some((0, 0, 0))), expected);
assert_eq!(accent_style_for(/*terminal_bg*/ None), expected);
}
#[test]
fn table_separator_blends_toward_dark_background() {
let style = table_separator_style_for(
Some((255, 255, 255)),
Some((0, 0, 0)),
StdoutColorLevel::TrueColor,
);
assert_eq!(style.fg, Some(rgb_color((51, 51, 51))));
}
#[test]
fn table_separator_blends_toward_light_background() {
let style = table_separator_style_for(
Some((0, 0, 0)),
Some((255, 255, 255)),
StdoutColorLevel::TrueColor,
);
assert_eq!(style.fg, Some(rgb_color((204, 204, 204))));
}
#[test]
fn table_separator_dims_when_palette_aware_color_is_unavailable() {
let expected = Style::default().dim();
assert_eq!(
table_separator_style_for(
Some((255, 255, 255)),
Some((0, 0, 0)),
StdoutColorLevel::Ansi16,
),
expected
);
assert_eq!(
table_separator_style_for(
/*terminal_fg*/ None,
Some((0, 0, 0)),
StdoutColorLevel::TrueColor,
),
expected
);
}
}