Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
11f3f7e
lighter rendering of user/agent messages
nornagon-openai Aug 26, 2025
9c659d6
wip
nornagon-openai Aug 27, 2025
8170786
wip
nornagon-openai Aug 27, 2025
1e56853
wip
nornagon-openai Aug 27, 2025
54dee3a
simplify
nornagon-openai Aug 27, 2025
792fb82
fix tests
nornagon-openai Aug 27, 2025
5159b46
agent message >
nornagon-openai Aug 27, 2025
f51eda8
warning emoji
nornagon-openai Aug 27, 2025
b2d79b9
single-line command runs
nornagon-openai Aug 27, 2025
bb45161
command wrapping
nornagon-openai Aug 27, 2025
93d9da0
diff wip
nornagon-openai Aug 28, 2025
e6ba0af
diffs
nornagon-openai Aug 28, 2025
ca93201
fix wrapping
nornagon-openai Aug 28, 2025
a25a6be
cleanup
nornagon-openai Aug 28, 2025
2567354
wip
nornagon-openai Aug 29, 2025
99dd4f2
fix spacing in textarea
nornagon-openai Aug 29, 2025
8a86495
tui: show spinner instead of bullet in Exploring header
nornagon-openai Aug 29, 2025
7ecac28
test
nornagon-openai Aug 29, 2025
e7548e2
revert status changes
nornagon-openai Aug 29, 2025
1bc20d1
placeholder
nornagon-openai Aug 29, 2025
990e997
update plan, approval spacing
nornagon-openai Aug 29, 2025
3339c3b
Merge remote-tracking branch 'origin/main' into nornagon/message-styling
nornagon-openai Aug 29, 2025
599e5d0
fix testst
nornagon-openai Aug 29, 2025
64d548b
tui: ChatComposer rect fixes and hint line; split patch history snaps…
nornagon-openai Aug 29, 2025
49d8633
remove empty test
nornagon-openai Aug 29, 2025
3a10412
test
nornagon-openai Aug 29, 2025
0f18de3
simpler test
nornagon-openai Aug 29, 2025
230bd1a
more simpler
nornagon-openai Aug 29, 2025
b8adb5a
test
nornagon-openai Aug 29, 2025
b6d38e9
tui: parse bash commands and dim some syntax
nornagon-openai Aug 29, 2025
cdfd879
fix headers
nornagon-openai Aug 29, 2025
7afe05a
better wrapping
nornagon-openai Aug 29, 2025
c312e26
diff simplify
nornagon-openai Aug 29, 2025
5bc0d64
simplify
nornagon-openai Aug 29, 2025
3da7f52
better path display
nornagon-openai Aug 29, 2025
a8d37e0
simplify diff
nornagon-openai Aug 29, 2025
d7ed1fd
better wrap in userhistorycell
nornagon-openai Aug 29, 2025
582d7fd
simplify
nornagon-openai Aug 29, 2025
27ef3c5
revert changes in tui.rs
nornagon-openai Aug 29, 2025
c21e10a
show single file for change approved
nornagon-openai Aug 29, 2025
0d96cb8
reparse commands for ideal response
nornagon-openai Aug 29, 2025
521a71b
dim output text
nornagon-openai Aug 29, 2025
861f7b5
convert binary size test to snapshot
nornagon-openai Aug 29, 2025
70b83f4
dim output text
nornagon-openai Aug 29, 2025
1aa8cec
don't try to detect image paths for normal typing
nornagon-openai Aug 30, 2025
93934a1
defensively, assume 1-char strings are not image paths
nornagon-openai Aug 30, 2025
1dbfa1f
Merge branch 'nornagon/lag' into nornagon/message-styling
nornagon-openai Aug 30, 2025
bf72447
update plan cell
nornagon-openai Sep 1, 2025
e8fc8c2
simplify
nornagon-openai Sep 1, 2025
1677132
wip
nornagon-openai Sep 1, 2025
47c9068
Merge branch 'nornagon/message-styling' into nornagon/parse-bash
nornagon-openai Sep 2, 2025
34bdfdd
Merge remote-tracking branch 'origin/main' into nornagon/parse-bash
nornagon-openai Sep 2, 2025
e700c44
Merge remote-tracking branch 'origin/main' into nornagon/parse-bash
nornagon-openai Sep 2, 2025
62c8ee3
fix
nornagon-openai Sep 2, 2025
19fd5e6
standardize on allowing hyphenation and breaking words
nornagon-openai Sep 2, 2025
59a4c63
restore no-hyphenation for bash
nornagon-openai Sep 2, 2025
92144d9
wip
nornagon-openai Sep 2, 2025
3750b6a
wip
nornagon-openai Sep 3, 2025
e2688ff
wip
nornagon-openai Sep 3, 2025
fbefb73
Merge remote-tracking branch 'origin/main' into nornagon/parse-bash
nornagon-openai Sep 3, 2025
f85f54e
wip
nornagon-openai Sep 3, 2025
6ca03b6
reorg
nornagon-openai Sep 4, 2025
d508cff
wip
nornagon-openai Sep 4, 2025
78d911f
move prefix_lines helper
nornagon-openai Sep 4, 2025
01e311b
gitignore
nornagon-openai Sep 4, 2025
54630ca
order
nornagon-openai Sep 4, 2025
79c66d9
consolidate wrapping helpers
nornagon-openai Sep 4, 2025
7508ec2
agents.md
nornagon-openai Sep 4, 2025
d9fd052
lint
nornagon-openai Sep 4, 2025
9f52dd2
better test
nornagon-openai Sep 4, 2025
c11f63a
simplify
nornagon-openai Sep 4, 2025
9e28bb4
tests
nornagon-openai Sep 4, 2025
8d2eeb1
Merge branch 'main' into nornagon/parse-bash
nornagon-openai Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
tui: parse bash commands and dim some syntax
  • Loading branch information
nornagon-openai committed Aug 29, 2025
commit b6d38e9ad1f7e34d6b68dc4e2e581c587ad53a7b
2 changes: 1 addition & 1 deletion codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]

mod apply_patch;
mod bash;
pub mod bash;
mod chat_completions;
mod client;
mod client_common;
Expand Down
122 changes: 113 additions & 9 deletions codex-rs/tui/src/history_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use std::time::Instant;
use tracing::error;
use unicode_width::UnicodeWidthStr;
use uuid::Uuid;
use codex_core::bash::try_parse_bash;

#[derive(Clone, Debug)]
pub(crate) struct CommandOutput {
Expand Down Expand Up @@ -278,6 +279,56 @@ impl HistoryCell for ExecCell {
}

impl ExecCell {
/// Produce syntax-highlighted spans for bash, dimming operator tokens like
/// &&, ||, |, &, (, ) and leaving other text regular. If parsing fails,
/// returns the entire string as a single unstyled span.
fn bash_syntax_spans(s: &str) -> Vec<Span<'static>> {
let Some(tree) = try_parse_bash(s) else {
return vec![s.to_string().into()];
};
let mut ranges: Vec<(usize, usize)> = Vec::new();
let root = tree.root_node();
let mut cursor = root.walk();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if !node.is_named() && !node.is_extra() {
let kind = node.kind();
// Treat anonymous, non-extra punctuation/delimiter tokens as operators to dim,
// but leave quotes/backticks unstyled to keep strings readable.
let is_quote = matches!(kind, "\"" | "'" | "`");
let is_whitespace = kind.trim().is_empty();
if !is_quote && !is_whitespace {
ranges.push((node.start_byte(), node.end_byte()));
}
}
for child in node.children(&mut cursor) {
stack.push(child);
}
}
if ranges.is_empty() {
return vec![s.to_string().into()];
}
ranges.sort_by_key(|(st, _)| *st);

let mut out: Vec<Span<'static>> = Vec::new();
let mut i = 0usize; // current cursor in source string
for (start, end) in ranges.into_iter() {
// Defensively handle overlapping or out-of-order ranges by clamping.
let dim_start = start.max(i);
let dim_end = end;
if dim_start < dim_end {
if dim_start > i {
out.push(s[i..dim_start].to_string().into());
}
out.push(s[dim_start..dim_end].to_string().dim());
i = dim_end;
}
}
if i < s.len() {
out.push(s[i..].to_string().into());
}
out
}
fn is_active(&self) -> bool {
self.calls.iter().any(|c| c.output.is_none())
}
Expand Down Expand Up @@ -433,13 +484,9 @@ impl ExecCell {
if !cmd_display.contains('\n')
&& cmd_display.width() < (width as usize).saturating_sub(reserved)
{
lines.push(Line::from(vec![
bullet,
" ".into(),
title.bold(),
" ".into(),
cmd_display.clone().into(),
]));
let mut spans = vec![bullet, " ".into(), title.bold(), " ".into()];
spans.extend(Self::bash_syntax_spans(&cmd_display));
lines.push(Line::from(spans));
} else {
lines.push(Line::from(vec![bullet, " ".into(), title.bold()]));
let first_prefix = " └ ".dim();
Expand Down Expand Up @@ -483,7 +530,9 @@ impl ExecCell {
} else {
line_cont_prefix.clone()
};
lines.push(Line::from(vec![prefix_span, piece.clone().into()]));
let mut spans = vec![prefix_span];
spans.extend(Self::bash_syntax_spans(piece));
lines.push(Line::from(spans));
}
}
} else {
Expand All @@ -502,7 +551,9 @@ impl ExecCell {
} else {
cont_prefix.clone()
};
lines.push(Line::from(vec![prefix_span, piece.clone().into()]));
let mut spans = vec![prefix_span];
spans.extend(Self::bash_syntax_spans(piece));
lines.push(Line::from(spans));
}
}
}
Expand Down Expand Up @@ -544,6 +595,59 @@ impl ExecCell {
}
}

#[cfg(test)]
mod syntax_tests {
use super::*;
use ratatui::style::Modifier;

#[test]
fn dims_expected_bash_operators() {
let s = "echo foo && bar || baz | qux & (echo hi)";
let spans = ExecCell::bash_syntax_spans(s);

let reconstructed: String = spans
.iter()
.map(|sp| sp.content.clone())
.collect();
assert_eq!(reconstructed, s);

fn is_dim(span: &Span<'_>) -> bool {
span.style.add_modifier.contains(Modifier::DIM)
}
let dimmed: Vec<String> = spans
.iter()
.filter(|sp| is_dim(sp))
.map(|sp| sp.content.clone().into_owned())
.collect();
assert_eq!(dimmed, vec!["&&", "||", "|", "&", "(", ")"]);
}

#[test]
fn does_not_dim_quotes_but_dims_other_punct() {
let s = "echo \"hi\" > out.txt; echo 'ok'";
let spans = ExecCell::bash_syntax_spans(s);

let reconstructed: String = spans
.iter()
.map(|sp| sp.content.clone())
.collect();
assert_eq!(reconstructed, s);

fn is_dim(span: &Span<'_>) -> bool {
span.style.add_modifier.contains(Modifier::DIM)
}
let dimmed: Vec<String> = spans
.iter()
.filter(|sp| is_dim(sp))
.map(|sp| sp.content.clone().into_owned())
.collect();
assert!(dimmed.contains(&">".to_string()));
assert!(dimmed.contains(&";".to_string()));
assert!(!dimmed.contains(&"\"".to_string()));
assert!(!dimmed.contains(&"'".to_string()));
}
}

impl WidgetRef for &ExecCell {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
if area.height == 0 {
Expand Down