mirror of
https://github.com/openai/codex.git
synced 2026-11-11 06:31:37 +00:00
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`.
This commit is contained in:
1 parent
2d1ad374a7
commit
6b4b15a1ed
11 files changed
+277
-139
No files matched your search
@@ -393,8 +393,8 @@ async fn completed_plan_table_tail_skips_provisional_history_insert() {
|
|||||||
|
|
||||||
assert!(saw_source_backed_plan, "expected source-backed plan insert");
|
assert!(saw_source_backed_plan, "expected source-backed plan insert");
|
||||||
assert!(
|
assert!(
|
||||||
rendered_plan.contains('│') || rendered_plan.contains('┌'),
|
rendered_plan.contains('━'),
|
||||||
"expected completed plan table to render as a boxed table, got: {rendered_plan:?}"
|
"expected completed plan table to render with separators, got: {rendered_plan:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!saw_stream_plan,
|
!saw_stream_plan,
|
||||||
|
|||||||
+7
-11
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/history_cell.rs
|
source: tui/src/history_cell/tests.rs
|
||||||
expression: rendered
|
expression: rendered
|
||||||
---
|
---
|
||||||
rich before:
|
rich before:
|
||||||
@@ -10,11 +10,9 @@ rich before:
|
|||||||
• - first item
|
• - first item
|
||||||
- second item
|
- second item
|
||||||
|
|
||||||
┌──────┬───────┐
|
Col Value
|
||||||
│ Col │ Value │
|
━━━━━━ ━━━━━━━
|
||||||
├──────┼───────┤
|
code x = 1
|
||||||
│ code │ x = 1 │
|
|
||||||
└──────┴───────┘
|
|
||||||
|
|
||||||
copy me
|
copy me
|
||||||
• Called
|
• Called
|
||||||
@@ -48,11 +46,9 @@ rich after:
|
|||||||
• - first item
|
• - first item
|
||||||
- second item
|
- second item
|
||||||
|
|
||||||
┌──────┬───────┐
|
Col Value
|
||||||
│ Col │ Value │
|
━━━━━━ ━━━━━━━
|
||||||
├──────┼───────┤
|
code x = 1
|
||||||
│ code │ x = 1 │
|
|
||||||
└──────┴───────┘
|
|
||||||
|
|
||||||
copy me
|
copy me
|
||||||
• Called
|
• Called
|
||||||
|
|||||||
@@ -278,10 +278,8 @@ fn proposed_plan_cell_renders_markdown_table() {
|
|||||||
let rendered = render_lines(&plan.display_lines(/*width*/ 80));
|
let rendered = render_lines(&plan.display_lines(/*width*/ 80));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
rendered
|
rendered.iter().any(|line| line.contains('━')),
|
||||||
.iter()
|
"expected separated table in proposed plan output: {rendered:?}"
|
||||||
.any(|line| line.contains('│') || line.contains('┌')),
|
|
||||||
"expected boxed table in proposed plan output: {rendered:?}"
|
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!rendered
|
!rendered
|
||||||
@@ -308,10 +306,8 @@ fn proposed_plan_cell_unwraps_markdown_fenced_table() {
|
|||||||
let rendered = render_lines(&plan.display_lines(/*width*/ 80));
|
let rendered = render_lines(&plan.display_lines(/*width*/ 80));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
rendered
|
rendered.iter().any(|line| line.contains('━')),
|
||||||
.iter()
|
"expected separated table for markdown-fenced proposed plan output: {rendered:?}"
|
||||||
.any(|line| line.contains('│') || line.contains('┌')),
|
|
||||||
"expected boxed table for markdown-fenced proposed plan output: {rendered:?}"
|
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!rendered.iter().any(|line| line.trim() == "```markdown"),
|
!rendered.iter().any(|line| line.trim() == "```markdown"),
|
||||||
|
|||||||
@@ -397,8 +397,8 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
append_markdown_agent(src, /*width*/ None, &mut out);
|
append_markdown_agent(src, /*width*/ None, &mut out);
|
||||||
let rendered = lines_to_strings(&out);
|
let rendered = lines_to_strings(&out);
|
||||||
assert!(rendered.iter().any(|line| line.contains("┌")));
|
assert!(rendered.iter().any(|line| line.contains('━')));
|
||||||
assert!(rendered.iter().any(|line| line.contains("│ 1 │ 2 │")));
|
assert!(rendered.iter().any(|line| line.contains(" 1 2")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -407,11 +407,11 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
append_markdown_agent(src, /*width*/ None, &mut out);
|
append_markdown_agent(src, /*width*/ None, &mut out);
|
||||||
let rendered = lines_to_strings(&out);
|
let rendered = lines_to_strings(&out);
|
||||||
assert!(rendered.iter().any(|line| line.contains("┌")));
|
assert!(rendered.iter().any(|line| line.contains('━')));
|
||||||
assert!(
|
assert!(
|
||||||
rendered
|
rendered
|
||||||
.iter()
|
.iter()
|
||||||
.any(|line| line.contains("│ Col A │ Col B │ Col C │"))
|
.any(|line| line.contains(" Col A Col B Col C"))
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!rendered
|
!rendered
|
||||||
@@ -426,8 +426,8 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
append_markdown_agent(src, /*width*/ None, &mut out);
|
append_markdown_agent(src, /*width*/ None, &mut out);
|
||||||
let rendered = lines_to_strings(&out);
|
let rendered = lines_to_strings(&out);
|
||||||
assert!(rendered.iter().any(|line| line.contains("┌")));
|
assert!(rendered.iter().any(|line| line.contains('━')));
|
||||||
assert!(rendered.iter().any(|line| line.contains("│ A")));
|
assert!(rendered.iter().any(|line| line.contains(" left right")));
|
||||||
assert!(!rendered.iter().any(|line| line.trim() == "A | B"));
|
assert!(!rendered.iter().any(|line| line.trim() == "A | B"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ mod tests {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
append_markdown_agent(src, /*width*/ None, &mut out);
|
append_markdown_agent(src, /*width*/ None, &mut out);
|
||||||
let rendered = lines_to_strings(&out);
|
let rendered = lines_to_strings(&out);
|
||||||
assert!(rendered.iter().any(|line| line.contains("┌")));
|
assert!(rendered.iter().any(|line| line.contains('━')));
|
||||||
assert!(!rendered.iter().any(|line| line.trim() == "| Only |"));
|
assert!(!rendered.iter().any(|line| line.trim() == "| Only |"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,9 @@
|
|||||||
//! alignment count.
|
//! alignment count.
|
||||||
//! 3. **Compute column widths** -- allocate widths with content-aware
|
//! 3. **Compute column widths** -- allocate widths with content-aware
|
||||||
//! priority and iterative shrinking.
|
//! priority and iterative shrinking.
|
||||||
//! 4. **Render box grid** -- Unicode borders (`┌───┬───┐`) or fallback to pipe
|
//! 4. **Render row-separated layout** -- theme-accented bold headers, a
|
||||||
//! format when the minimum cannot fit.
|
//! heavier segmented header rule, and low-contrast segmented body
|
||||||
|
//! separators, or fallback to pipe format when the minimum cannot fit.
|
||||||
//! 5. **Append spillover** -- extracted spillover rows rendered as plain text
|
//! 5. **Append spillover** -- extracted spillover rows rendered as plain text
|
||||||
//! after the table.
|
//! after the table.
|
||||||
//!
|
//!
|
||||||
@@ -36,9 +37,11 @@
|
|||||||
//! preserved last. When even 3-char-wide columns cannot fit, the table falls
|
//! preserved last. When even 3-char-wide columns cannot fit, the table falls
|
||||||
//! back to pipe-delimited format.
|
//! back to pipe-delimited format.
|
||||||
|
|
||||||
|
use crate::render::highlight::foreground_style_for_scopes;
|
||||||
use crate::render::highlight::highlight_code_to_lines;
|
use crate::render::highlight::highlight_code_to_lines;
|
||||||
use crate::render::line_utils::line_to_static;
|
use crate::render::line_utils::line_to_static;
|
||||||
use crate::render::line_utils::push_owned_lines;
|
use crate::render::line_utils::push_owned_lines;
|
||||||
|
use crate::style::table_separator_style;
|
||||||
use crate::wrapping::RtOptions;
|
use crate::wrapping::RtOptions;
|
||||||
use crate::wrapping::adaptive_wrap_line;
|
use crate::wrapping::adaptive_wrap_line;
|
||||||
use crate::wrapping::word_wrap_line;
|
use crate::wrapping::word_wrap_line;
|
||||||
@@ -66,6 +69,11 @@ use std::sync::LazyLock;
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
const TABLE_COLUMN_GAP: usize = 2;
|
||||||
|
const TABLE_CELL_PADDING: usize = 1;
|
||||||
|
const TABLE_HEADER_SEPARATOR_CHAR: char = '━';
|
||||||
|
const TABLE_BODY_SEPARATOR_CHAR: char = '─';
|
||||||
|
|
||||||
struct MarkdownStyles {
|
struct MarkdownStyles {
|
||||||
h1: Style,
|
h1: Style,
|
||||||
h2: Style,
|
h2: Style,
|
||||||
@@ -207,7 +215,7 @@ impl TableState {
|
|||||||
|
|
||||||
/// Rendered table output split by wrapping behavior.
|
/// Rendered table output split by wrapping behavior.
|
||||||
///
|
///
|
||||||
/// `table_lines` are either prewrapped grid rows (box rendering) or pipe
|
/// `table_lines` are either prewrapped aligned rows or pipe
|
||||||
/// fallback rows that should still pass through normal wrapping.
|
/// fallback rows that should still pass through normal wrapping.
|
||||||
/// `spillover_lines` are prose rows extracted from parser artifacts and should
|
/// `spillover_lines` are prose rows extracted from parser artifacts and should
|
||||||
/// be routed through normal wrapping.
|
/// be routed through normal wrapping.
|
||||||
@@ -260,7 +268,7 @@ pub fn render_markdown_text(input: &str) -> Text<'static> {
|
|||||||
/// Render markdown constrained to a known terminal width.
|
/// Render markdown constrained to a known terminal width.
|
||||||
///
|
///
|
||||||
/// The renderer preserves table structure when possible and falls back to
|
/// The renderer preserves table structure when possible and falls back to
|
||||||
/// pipe-table output when a box table cannot fit the available width. Passing
|
/// pipe-table output when an aligned table cannot fit the available width. Passing
|
||||||
/// `None` keeps intrinsic line widths and disables width-driven wrapping in the
|
/// `None` keeps intrinsic line widths and disables width-driven wrapping in the
|
||||||
/// markdown writer. Local file links render relative to the current process
|
/// markdown writer. Local file links render relative to the current process
|
||||||
/// working directory.
|
/// working directory.
|
||||||
@@ -982,13 +990,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a completed `TableState` into styled `Line`s with Unicode
|
/// Convert a completed `TableState` into styled, row-separated `Line`s.
|
||||||
/// box-drawing borders.
|
|
||||||
///
|
///
|
||||||
/// Pipeline: filter spillover rows -> normalize column counts -> compute
|
/// Pipeline: filter spillover rows -> normalize column counts -> compute
|
||||||
/// column widths -> render box grid (or fall back to pipe format if the
|
/// column widths -> render aligned rows (or fall back to pipe format if the
|
||||||
/// minimum column widths exceed available terminal width). Spillover rows
|
/// minimum column widths exceed available terminal width). Spillover rows
|
||||||
/// are appended as plain text after the table grid.
|
/// are appended as plain text after the table.
|
||||||
///
|
///
|
||||||
/// Falls back to `render_table_pipe_fallback` (raw `| A | B |` format)
|
/// Falls back to `render_table_pipe_fallback` (raw `| A | B |` format)
|
||||||
/// when `compute_column_widths` returns `None` (terminal too narrow for
|
/// when `compute_column_widths` returns `None` (terminal too narrow for
|
||||||
@@ -1048,25 +1055,38 @@ where
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let border_style = Style::new().dim();
|
let header_style =
|
||||||
let mut out = Vec::with_capacity(3 + rows.len() * 2);
|
foreground_style_for_scopes(&["entity.name.type", "support.type", "variable"])
|
||||||
out.push(self.render_border_line('┌', '┬', '┐', &column_widths, border_style));
|
.unwrap_or(self.styles.strong)
|
||||||
|
.bold();
|
||||||
|
let separator_style = table_separator_style();
|
||||||
|
let mut out = Vec::with_capacity(2 + rows.len() * 2);
|
||||||
out.extend(self.render_table_row(
|
out.extend(self.render_table_row(
|
||||||
&header,
|
&header,
|
||||||
&column_widths,
|
&column_widths,
|
||||||
&table_state.alignments,
|
&table_state.alignments,
|
||||||
border_style,
|
header_style,
|
||||||
));
|
));
|
||||||
out.push(self.render_border_line('├', '┼', '┤', &column_widths, border_style));
|
out.push(Self::render_table_separator(
|
||||||
for row in &rows {
|
&column_widths,
|
||||||
|
TABLE_HEADER_SEPARATOR_CHAR,
|
||||||
|
separator_style,
|
||||||
|
));
|
||||||
|
for (row_idx, row) in rows.iter().enumerate() {
|
||||||
out.extend(self.render_table_row(
|
out.extend(self.render_table_row(
|
||||||
row,
|
row,
|
||||||
&column_widths,
|
&column_widths,
|
||||||
&table_state.alignments,
|
&table_state.alignments,
|
||||||
border_style,
|
Style::default(),
|
||||||
));
|
));
|
||||||
|
if row_idx + 1 < rows.len() {
|
||||||
|
out.push(Self::render_table_separator(
|
||||||
|
&column_widths,
|
||||||
|
TABLE_BODY_SEPARATOR_CHAR,
|
||||||
|
separator_style,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.push(self.render_border_line('└', '┴', '┘', &column_widths, border_style));
|
|
||||||
RenderedTableLines {
|
RenderedTableLines {
|
||||||
table_lines: out,
|
table_lines: out,
|
||||||
table_lines_prewrapped: true,
|
table_lines_prewrapped: true,
|
||||||
@@ -1079,17 +1099,19 @@ where
|
|||||||
row.resize(column_count, TableCell::default());
|
row.resize(column_count, TableCell::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// subtracts the space eaten by border characters
|
/// Subtract horizontal gutters and per-cell padding from the content budget.
|
||||||
fn available_table_width(&self, column_count: usize) -> Option<usize> {
|
fn available_table_width(&self, column_count: usize) -> Option<usize> {
|
||||||
self.wrap_width.map(|wrap_width| {
|
self.wrap_width.map(|wrap_width| {
|
||||||
let prefix_width =
|
let prefix_width =
|
||||||
Self::spans_display_width(&self.prefix_spans(self.pending_marker_line));
|
Self::spans_display_width(&self.prefix_spans(self.pending_marker_line));
|
||||||
let reserved = prefix_width + 1 + (column_count * 3);
|
let reserved = prefix_width
|
||||||
|
+ (column_count.saturating_sub(1) * TABLE_COLUMN_GAP)
|
||||||
|
+ (column_count * TABLE_CELL_PADDING * 2);
|
||||||
wrap_width.saturating_sub(reserved)
|
wrap_width.saturating_sub(reserved)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate column widths for box-drawing table rendering.
|
/// Allocate column widths for aligned, row-separated table rendering.
|
||||||
///
|
///
|
||||||
/// Each column starts at its natural (max cell content) width, then columns
|
/// Each column starts at its natural (max cell content) width, then columns
|
||||||
/// are iteratively shrunk one character at a time until the total fits within
|
/// are iteratively shrunk one character at a time until the total fits within
|
||||||
@@ -1277,25 +1299,19 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_border_line(
|
fn render_table_separator(
|
||||||
&self,
|
|
||||||
left: char,
|
|
||||||
sep: char,
|
|
||||||
right: char,
|
|
||||||
column_widths: &[usize],
|
column_widths: &[usize],
|
||||||
|
separator_char: char,
|
||||||
style: Style,
|
style: Style,
|
||||||
) -> Line<'static> {
|
) -> Line<'static> {
|
||||||
let mut spans = Vec::with_capacity(column_widths.len() * 2 + 1);
|
let segment_char = separator_char.to_string();
|
||||||
spans.push(Span::styled(String::from(left), style));
|
let gap = " ".repeat(TABLE_COLUMN_GAP);
|
||||||
for (idx, width) in column_widths.iter().enumerate() {
|
let text = column_widths
|
||||||
spans.push(Span::styled("─".repeat(*width + 2), style));
|
.iter()
|
||||||
if idx + 1 == column_widths.len() {
|
.map(|width| segment_char.repeat(*width + (TABLE_CELL_PADDING * 2)))
|
||||||
spans.push(Span::styled(String::from(right), style));
|
.collect::<Vec<_>>()
|
||||||
} else {
|
.join(&gap);
|
||||||
spans.push(Span::styled(String::from(sep), style));
|
Line::from(Span::styled(text, style))
|
||||||
}
|
|
||||||
}
|
|
||||||
Line::from(spans)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_table_row(
|
fn render_table_row(
|
||||||
@@ -1303,7 +1319,7 @@ where
|
|||||||
row: &[TableCell],
|
row: &[TableCell],
|
||||||
column_widths: &[usize],
|
column_widths: &[usize],
|
||||||
alignments: &[Alignment],
|
alignments: &[Alignment],
|
||||||
border_style: Style,
|
row_style: Style,
|
||||||
) -> Vec<Line<'static>> {
|
) -> Vec<Line<'static>> {
|
||||||
let wrapped_cells: Vec<Vec<Line<'static>>> = row
|
let wrapped_cells: Vec<Vec<Line<'static>>> = row
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1314,10 +1330,21 @@ where
|
|||||||
|
|
||||||
let mut out = Vec::with_capacity(row_height);
|
let mut out = Vec::with_capacity(row_height);
|
||||||
for row_line in 0..row_height {
|
for row_line in 0..row_height {
|
||||||
|
let Some(last_visible_column) = wrapped_cells.iter().rposition(|lines| {
|
||||||
|
lines
|
||||||
|
.get(row_line)
|
||||||
|
.is_some_and(|line| Self::line_display_width(line) > 0)
|
||||||
|
}) else {
|
||||||
|
out.push(Line::default().style(row_style));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
spans.push(Span::styled("│", border_style));
|
for (column, width) in column_widths
|
||||||
for (column, width) in column_widths.iter().enumerate() {
|
.iter()
|
||||||
spans.push(Span::raw(" "));
|
.enumerate()
|
||||||
|
.take(last_visible_column + 1)
|
||||||
|
{
|
||||||
|
spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING)));
|
||||||
let line = wrapped_cells[column]
|
let line = wrapped_cells[column]
|
||||||
.get(row_line)
|
.get(row_line)
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -1333,13 +1360,18 @@ where
|
|||||||
spans.push(Span::raw(" ".repeat(left_padding)));
|
spans.push(Span::raw(" ".repeat(left_padding)));
|
||||||
}
|
}
|
||||||
spans.extend(line.spans);
|
spans.extend(line.spans);
|
||||||
if right_padding > 0 {
|
let is_last_column = column == last_visible_column;
|
||||||
|
if right_padding > 0 && !is_last_column {
|
||||||
spans.push(Span::raw(" ".repeat(right_padding)));
|
spans.push(Span::raw(" ".repeat(right_padding)));
|
||||||
}
|
}
|
||||||
spans.push(Span::raw(" "));
|
if !is_last_column {
|
||||||
spans.push(Span::styled("│", border_style));
|
spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING)));
|
||||||
|
}
|
||||||
|
if !is_last_column {
|
||||||
|
spans.push(Span::raw(" ".repeat(TABLE_COLUMN_GAP)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.push(Line::from(spans));
|
out.push(Line::from(spans).style(row_style));
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -1635,8 +1667,8 @@ where
|
|||||||
/// Push a line that has already been laid out at the correct width, skipping
|
/// Push a line that has already been laid out at the correct width, skipping
|
||||||
/// word wrapping.
|
/// word wrapping.
|
||||||
///
|
///
|
||||||
/// Table lines are pre-formatted with exact column widths and box-drawing
|
/// Table lines are pre-formatted with exact column widths and separators.
|
||||||
/// borders. Passing them through `word_wrap_line` would break the grid at
|
/// Passing them through `word_wrap_line` would break the layout at
|
||||||
/// arbitrary positions. This method prepends the indent/blockquote prefix
|
/// arbitrary positions. This method prepends the indent/blockquote prefix
|
||||||
/// and pushes directly to `self.text.lines`.
|
/// and pushes directly to `self.text.lines`.
|
||||||
fn is_blockquote_active(&self) -> bool {
|
fn is_blockquote_active(&self) -> bool {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use ratatui::style::Modifier;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
use ratatui::text::Span;
|
use ratatui::text::Span;
|
||||||
@@ -1471,7 +1472,7 @@ fn code_block_preserves_trailing_blank_lines() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn table_renders_unicode_box() {
|
fn table_renders_app_style_rows_with_themed_bold_header() {
|
||||||
let md = "| A | B |\n|---|---|\n| 1 | 2 |\n";
|
let md = "| A | B |\n|---|---|\n| 1 | 2 |\n";
|
||||||
let text = render_markdown_text(md);
|
let text = render_markdown_text(md);
|
||||||
let lines: Vec<String> = text
|
let lines: Vec<String> = text
|
||||||
@@ -1483,13 +1484,33 @@ fn table_renders_unicode_box() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
lines,
|
lines,
|
||||||
vec![
|
vec![
|
||||||
"┌─────┬─────┐".to_string(),
|
" A B".to_string(),
|
||||||
"│ A │ B │".to_string(),
|
"━━━━━ ━━━━━".to_string(),
|
||||||
"├─────┼─────┤".to_string(),
|
" 1 2".to_string(),
|
||||||
"│ 1 │ 2 │".to_string(),
|
|
||||||
"└─────┴─────┘".to_string(),
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
text.lines[0]
|
||||||
|
.style
|
||||||
|
.add_modifier
|
||||||
|
.contains(Modifier::BOLD)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.lines[0].style.fg.is_some(),
|
||||||
|
"expected the syntax theme to provide a table header accent"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.lines[1].spans[0]
|
||||||
|
.style
|
||||||
|
.add_modifier
|
||||||
|
.contains(Modifier::DIM)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!text.lines[2]
|
||||||
|
.style
|
||||||
|
.add_modifier
|
||||||
|
.contains(Modifier::BOLD)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1502,13 +1523,13 @@ fn table_alignment_respects_markers() {
|
|||||||
.map(|line| line.spans.iter().map(|span| span.content.clone()).collect())
|
.map(|line| line.spans.iter().map(|span| span.content.clone()).collect())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert_eq!(lines[1], "│ Left │ Center │ Right │");
|
assert_eq!(lines[0], " Left Center Right");
|
||||||
assert_eq!(lines[3], "│ a │ b │ c │");
|
assert_eq!(lines[2], " a b c");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn table_wraps_cell_content_when_width_is_narrow() {
|
fn table_separates_logical_rows_after_wrapped_content() {
|
||||||
let md = "| Key | Description |\n| --- | --- |\n| -v | Enable very verbose logging output for debugging |\n";
|
let md = "| Key | Description |\n| --- | --- |\n| -v | Enable very verbose logging output for debugging |\n| -q | Quiet output |\n";
|
||||||
let text = crate::markdown_render::render_markdown_text_with_width(md, Some(30));
|
let text = crate::markdown_render::render_markdown_text_with_width(md, Some(30));
|
||||||
let lines: Vec<String> = text
|
let lines: Vec<String> = text
|
||||||
.lines
|
.lines
|
||||||
@@ -1516,7 +1537,6 @@ fn table_wraps_cell_content_when_width_is_narrow() {
|
|||||||
.map(|line| line.spans.iter().map(|span| span.content.clone()).collect())
|
.map(|line| line.spans.iter().map(|span| span.content.clone()).collect())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert!(lines[0].starts_with('┌') && lines[0].ends_with('┐'));
|
|
||||||
assert!(
|
assert!(
|
||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1524,6 +1544,26 @@ fn table_wraps_cell_content_when_width_is_narrow() {
|
|||||||
&& lines.iter().any(|line| line.contains("logging output")),
|
&& lines.iter().any(|line| line.contains("logging output")),
|
||||||
"expected wrapped row content: {lines:?}"
|
"expected wrapped row content: {lines:?}"
|
||||||
);
|
);
|
||||||
|
let separator_indices: Vec<usize> = lines
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, line)| {
|
||||||
|
((line.contains('━') || line.contains('─'))
|
||||||
|
&& line.chars().all(|ch| matches!(ch, '━' | '─' | ' ')))
|
||||||
|
.then_some(idx)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let wrapped_row_end = lines
|
||||||
|
.iter()
|
||||||
|
.position(|line| line.contains("logging output"))
|
||||||
|
.expect("expected final wrapped line");
|
||||||
|
assert_eq!(separator_indices.len(), 2);
|
||||||
|
assert!(separator_indices[1] > wrapped_row_end);
|
||||||
|
assert!(
|
||||||
|
!lines
|
||||||
|
.last()
|
||||||
|
.is_some_and(|line| line.contains('━') || line.contains('─'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1553,7 +1593,7 @@ fn table_inside_blockquote_has_quote_prefix() {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert!(lines.iter().all(|line| line.starts_with("> ")));
|
assert!(lines.iter().all(|line| line.starts_with("> ")));
|
||||||
assert!(lines.iter().any(|line| line.contains("┌─────┬─────┐")));
|
assert!(lines.iter().any(|line| line.contains("━━━━━ ━━━━━")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1580,5 +1620,9 @@ fn table_falls_back_to_pipe_rendering_if_it_cannot_fit() {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert!(lines.first().is_some_and(|line| line.starts_with('|')));
|
assert!(lines.first().is_some_and(|line| line.starts_with('|')));
|
||||||
assert!(!lines.iter().any(|line| line.contains('┌')));
|
assert!(
|
||||||
|
!lines
|
||||||
|
.iter()
|
||||||
|
.any(|line| line.contains('━') || line.contains('─'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -878,8 +878,8 @@ mod tests {
|
|||||||
let rendered_strs = lines_to_plain_strings(&rendered);
|
let rendered_strs = lines_to_plain_strings(&rendered);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
rendered_strs.iter().any(|line| line.contains('┌')),
|
rendered_strs.iter().any(|line| line.contains('━')),
|
||||||
"expected markdown-fenced table to render as boxed table: {rendered_strs:?}"
|
"expected markdown-fenced table to render with a separator: {rendered_strs:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!rendered_strs.iter().any(|line| line.trim() == "| A | B |"),
|
!rendered_strs.iter().any(|line| line.trim() == "| A | B |"),
|
||||||
|
|||||||
+3
-5
@@ -30,11 +30,9 @@ Image: alt text
|
|||||||
|
|
||||||
Table below (alignment test):
|
Table below (alignment test):
|
||||||
|
|
||||||
┌──────┬────────┬───────┐
|
Left Center Right
|
||||||
│ Left │ Center │ Right │
|
━━━━━━ ━━━━━━━━ ━━━━━━━
|
||||||
├──────┼────────┼───────┤
|
a b c
|
||||||
│ a │ b │ c │
|
|
||||||
└──────┴────────┴───────┘
|
|
||||||
Inline HTML: <sup>sup</sup> and <sub>sub</sub>.
|
Inline HTML: <sup>sup</sup> and <sub>sub</sub>.
|
||||||
HTML block:
|
HTML block:
|
||||||
<div style="border:1px solid #ccc;padding:2px">inline block</div>
|
<div style="border:1px solid #ccc;padding:2px">inline block</div>
|
||||||
|
|||||||
+21
-22
@@ -2,25 +2,24 @@
|
|||||||
source: tui/src/markdown_render_tests.rs
|
source: tui/src/markdown_render_tests.rs
|
||||||
expression: "plain_lines(&text).join(\"\\n\")"
|
expression: "plain_lines(&text).join(\"\\n\")"
|
||||||
---
|
---
|
||||||
┌────────────────────────────────────────┬──────────────────┬──────┬─────────┬─────────────────────────────────────────┐
|
Unit Files Adds Removes What It Adds
|
||||||
│ Unit │ Files │ Adds │ Removes │ What It Adds │
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━ ━━━━━━ ━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
├────────────────────────────────────────┼──────────────────┼──────┼─────────┼─────────────────────────────────────────┤
|
Suggestion engine and unit coverage codex-rs/core/ 704 0 Sampling workflow, stable-history
|
||||||
│ Suggestion engine and unit coverage │ codex-rs/core/ │ 704 │ 0 │ Sampling workflow, stable-history │
|
src/ checks, tool-flow suppression, fast
|
||||||
│ │ src/ │ │ │ checks, tool-flow suppression, fast │
|
next_prompt_sugg reasoning profile, filtering rules,
|
||||||
│ │ next_prompt_sugg │ │ │ reasoning profile, filtering rules, │
|
estion.rs:1, cancellation and timeout.
|
||||||
│ │ estion.rs:1, │ │ │ cancellation and timeout. │
|
codex-rs/core/
|
||||||
│ │ codex-rs/core/ │ │ │ │
|
src/
|
||||||
│ │ src/ │ │ │ │
|
next_prompt_sugg
|
||||||
│ │ next_prompt_sugg │ │ │ │
|
estion_tests.rs:
|
||||||
│ │ estion_tests.rs: │ │ │ │
|
1
|
||||||
│ │ 1 │ │ │ │
|
─────────────────────────────────────── ────────────────── ────── ───────── ────────────────────────────────────────
|
||||||
│ Model instruction fragment and │ codex-rs/core/ │ 54 │ 0 │ Synthetic suggestion prompt and an │
|
Model instruction fragment and codex-rs/core/ 54 0 Synthetic suggestion prompt and an
|
||||||
│ contextual isolation │ src/context/ │ │ │ isolation test for ordinary user text. │
|
contextual isolation src/context/ isolation test for ordinary user text.
|
||||||
│ │ next_prompt_sugg │ │ │ │
|
next_prompt_sugg
|
||||||
│ │ estion.rs:1, │ │ │ │
|
estion.rs:1,
|
||||||
│ │ codex-rs/core/ │ │ │ │
|
codex-rs/core/
|
||||||
│ │ src/context/ │ │ │ │
|
src/context/
|
||||||
│ │ contextual_user_ │ │ │ │
|
contextual_user_
|
||||||
│ │ message_tests.rs │ │ │ │
|
message_tests.rs
|
||||||
│ │ :1 │ │ │ │
|
:1
|
||||||
└────────────────────────────────────────┴──────────────────┴──────┴─────────┴─────────────────────────────────────────┘
|
|
||||||
@@ -1362,7 +1362,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn controller_renders_unicode_for_multi_table_response_shape() {
|
fn controller_renders_separators_for_multi_table_response_shape() {
|
||||||
let source = "Absolutely. Here are several different Markdown table patterns you can use for rendering tests.\n\n| Name | Role |
|
let source = "Absolutely. Here are several different Markdown table patterns you can use for rendering tests.\n\n| Name | Role |
|
||||||
Location |\n|-------|-----------|----------|\n| Ava | Engineer | NYC |\n| Malik | Designer | Berlin |\n| Priya | PM | Remote
|
Location |\n|-------|-----------|----------|\n| Ava | Engineer | NYC |\n| Malik | Designer | Berlin |\n| Priya | PM | Remote
|
||||||
|\n\n| Item | Qty | Price | In Stock |\n|:------------|----:|------:|:--------:|\n| Keyboard | 2 | 49.99 | Yes |\n| Mouse | 10
|
|\n\n| Item | Qty | Price | In Stock |\n|:------------|----:|------:|:--------:|\n| Keyboard | 2 | 49.99 | Yes |\n| Mouse | 10
|
||||||
@@ -1378,13 +1378,13 @@ mod tests {
|
|||||||
let deltas = chunked.iter().map(String::as_str).collect::<Vec<_>>();
|
let deltas = chunked.iter().map(String::as_str).collect::<Vec<_>>();
|
||||||
let streamed = collect_streamed_lines(&deltas, Some(120));
|
let streamed = collect_streamed_lines(&deltas, Some(120));
|
||||||
assert!(
|
assert!(
|
||||||
streamed.iter().any(|line| line.contains('┌')),
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
"expected unicode table border in streamed output: {streamed:?}"
|
"expected table separator in streamed output: {streamed:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn controller_renders_unicode_for_no_outer_pipes_table_shape() {
|
fn controller_renders_separators_for_no_outer_pipes_table_shape() {
|
||||||
let source = "### 1) Basic\n\n| Name | Role | Active |\n|---|---|---|\n| Alice | Engineer | Yes |\n| Bob | Designer | No |\n\n### 2) No outer
|
let source = "### 1) Basic\n\n| Name | Role | Active |\n|---|---|---|\n| Alice | Engineer | Yes |\n| Bob | Designer | No |\n\n### 2) No outer
|
||||||
pipes\n\nCol A | Col B | Col C\n--- | --- | ---\nx | y | z\n10 | 20 | 30\n\n### 3) Another table\n\n| Key | Value |\n|---|---|\n| a | b |\n";
|
pipes\n\nCol A | Col B | Col C\n--- | --- | ---\nx | y | z\n10 | 20 | 30\n\n### 3) Another table\n\n| Key | Value |\n|---|---|\n| a | b |\n";
|
||||||
|
|
||||||
@@ -1407,6 +1407,10 @@ mod tests {
|
|||||||
!has_raw_no_outer_header,
|
!has_raw_no_outer_header,
|
||||||
"no-outer-pipes header should not remain raw in final streamed output: {streamed:?}"
|
"no-outer-pipes header should not remain raw in final streamed output: {streamed:?}"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
|
"expected table separator in final streamed output: {streamed:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1429,8 +1433,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(streamed, expected);
|
assert_eq!(streamed, expected);
|
||||||
assert!(
|
assert!(
|
||||||
streamed.iter().any(|line| line.contains('┌')),
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
"expected unicode table border for no-outer-pipes streaming: {streamed:?}"
|
"expected table separator for no-outer-pipes streaming: {streamed:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed
|
!streamed
|
||||||
@@ -1458,8 +1462,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(streamed, expected);
|
assert_eq!(streamed, expected);
|
||||||
assert!(
|
assert!(
|
||||||
streamed.iter().any(|line| line.contains('┌')),
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
"expected unicode table border for two-column no-outer table: {streamed:?}"
|
"expected table separator for two-column no-outer table: {streamed:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed.iter().any(|line| line.trim() == "A | B"),
|
!streamed.iter().any(|line| line.trim() == "A | B"),
|
||||||
@@ -1490,8 +1494,8 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
streamed
|
streamed
|
||||||
.iter()
|
.iter()
|
||||||
.any(|line| line.contains("┌───────┬───────┬───────┐")),
|
.any(|line| line.contains(" Col A Col B Col C")),
|
||||||
"expected converted no-outer table border in streamed output: {streamed:?}"
|
"expected converted no-outer table header in streamed output: {streamed:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1513,8 +1517,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(streamed, expected);
|
assert_eq!(streamed, expected);
|
||||||
assert!(
|
assert!(
|
||||||
streamed.iter().any(|line| line.contains('┌')),
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
"expected unicode table border in streamed output: {streamed:?}"
|
"expected table separator in streamed output: {streamed:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed.iter().any(|line| line.trim() == "| A | B |"),
|
!streamed.iter().any(|line| line.trim() == "| A | B |"),
|
||||||
@@ -1542,8 +1546,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(streamed, expected);
|
assert_eq!(streamed, expected);
|
||||||
assert!(
|
assert!(
|
||||||
streamed.iter().any(|line| line.contains('┌')),
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
"expected unicode table border in streamed output: {streamed:?}"
|
"expected table separator in streamed output: {streamed:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed
|
!streamed
|
||||||
@@ -1621,8 +1625,10 @@ mod tests {
|
|||||||
"expected code-fenced pipe line to remain raw: {streamed:?}"
|
"expected code-fenced pipe line to remain raw: {streamed:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed.iter().any(|line| line.contains('┌')),
|
!streamed
|
||||||
"did not expect unicode table border for non-markdown fence: {streamed:?}"
|
.iter()
|
||||||
|
.any(|line| line.contains('━') || line.contains('─')),
|
||||||
|
"did not expect a table separator for non-markdown fence: {streamed:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,10 +1649,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(streamed, baseline);
|
assert_eq!(streamed, baseline);
|
||||||
assert!(
|
assert!(
|
||||||
streamed
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
.iter()
|
"expected table separators in plan streamed output: {streamed:?}"
|
||||||
.any(|line| line.contains('│') || line.contains('└') || line.contains('┌')),
|
|
||||||
"expected unicode table box drawing chars in plan streamed output: {streamed:?}"
|
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed
|
!streamed
|
||||||
@@ -1675,10 +1679,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(streamed, baseline);
|
assert_eq!(streamed, baseline);
|
||||||
assert!(
|
assert!(
|
||||||
streamed
|
streamed.iter().any(|line| line.contains('━')),
|
||||||
.iter()
|
"expected table separators in fenced plan output: {streamed:?}"
|
||||||
.any(|line| line.contains('│') || line.contains('└') || line.contains('┌')),
|
|
||||||
"expected unicode table box drawing chars in fenced plan output: {streamed:?}"
|
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!streamed
|
!streamed
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
use crate::color::blend;
|
use crate::color::blend;
|
||||||
use crate::color::is_light;
|
use crate::color::is_light;
|
||||||
|
use crate::terminal_palette::StdoutColorLevel;
|
||||||
use crate::terminal_palette::best_color;
|
use crate::terminal_palette::best_color;
|
||||||
use crate::terminal_palette::default_bg;
|
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::Color;
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
|
|
||||||
const LIGHT_BG_ACCENT_RGB: (u8, u8, u8) = (0, 95, 135);
|
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 {
|
pub fn user_message_style() -> Style {
|
||||||
user_message_style_for(default_bg())
|
user_message_style_for(default_bg())
|
||||||
@@ -16,6 +22,11 @@ pub fn proposed_plan_style() -> Style {
|
|||||||
proposed_plan_style_for(default_bg())
|
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.
|
/// Returns the shared accent style for active or selected TUI controls.
|
||||||
pub(crate) fn accent_style() -> Style {
|
pub(crate) fn accent_style() -> Style {
|
||||||
accent_style_for(default_bg())
|
accent_style_for(default_bg())
|
||||||
@@ -45,6 +56,22 @@ pub(crate) fn accent_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
|
pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
|
||||||
let (top, alpha) = if is_light(terminal_bg) {
|
let (top, alpha) = if is_light(terminal_bg) {
|
||||||
@@ -81,4 +108,48 @@ mod tests {
|
|||||||
assert_eq!(accent_style_for(Some((0, 0, 0))), expected);
|
assert_eq!(accent_style_for(Some((0, 0, 0))), expected);
|
||||||
assert_eq!(accent_style_for(/*terminal_bg*/ None), 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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user