Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 2 additions & 5 deletions codex-rs/config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,12 +658,9 @@ pub struct Tui {

/// Controls whether the TUI uses the terminal's alternate screen buffer.
///
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
/// - `always`: Always use alternate screen (original behavior).
/// - `auto` (default): Use alternate screen.
/// - `always`: Always use alternate screen.
/// - `never`: Never use alternate screen (inline mode only, preserves scrollback).
///
/// Using alternate screen provides a cleaner fullscreen experience but prevents
/// scrollback in terminal multiplexers like Zellij that follow the xterm spec.
#[serde(default)]
pub alternate_screen: AltScreenMode,

Expand Down
8 changes: 4 additions & 4 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,17 @@
"type": "object"
},
"AltScreenMode": {
"description": "Controls whether the TUI uses the terminal's alternate screen buffer.\n\n**Background:** The alternate screen buffer provides a cleaner fullscreen experience without polluting the terminal's scrollback history. However, it conflicts with terminal multiplexers like Zellij that strictly follow the xterm specification, which defines that alternate screen buffers should not have scrollback.\n\n**Zellij's behavior:** Zellij intentionally disables scrollback in alternate screen mode (see https://github.com/zellij-org/zellij/pull/1032) to comply with the xterm spec. This is by design and not configurable in Zellij—there is no option to enable scrollback in alternate screen mode.\n\n**Solution:** This setting provides a pragmatic workaround: - `auto` (default): Automatically detect the terminal multiplexer. If running in Zellij, disable alternate screen to preserve scrollback. Enable it everywhere else. - `always`: Always use alternate screen mode (original behavior before this fix). - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback in all multiplexers.\n\nThe CLI flag `--no-alt-screen` can override this setting at runtime.",
"description": "Controls whether the TUI uses the terminal's alternate screen buffer.\n\n- `auto` (default): Use alternate screen mode. - `always`: Always use alternate screen mode. - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback.\n\nThe CLI flag `--no-alt-screen` can override this setting at runtime.",
"oneOf": [
{
"description": "Auto-detect: disable alternate screen in Zellij, enable elsewhere.",
"description": "Use alternate screen mode.",
"enum": [
"auto"
],
"type": "string"
},
{
"description": "Always use alternate screen (original behavior).",
"description": "Always use alternate screen mode.",
"enum": [
"always"
],
Expand Down Expand Up @@ -2530,7 +2530,7 @@
}
],
"default": "auto",
"description": "Controls whether the TUI uses the terminal's alternate screen buffer.\n\n- `auto` (default): Disable alternate screen in Zellij, enable elsewhere. - `always`: Always use alternate screen (original behavior). - `never`: Never use alternate screen (inline mode only, preserves scrollback).\n\nUsing alternate screen provides a cleaner fullscreen experience but prevents scrollback in terminal multiplexers like Zellij that follow the xterm spec."
"description": "Controls whether the TUI uses the terminal's alternate screen buffer.\n\n- `auto` (default): Use alternate screen. - `always`: Always use alternate screen. - `never`: Never use alternate screen (inline mode only, preserves scrollback)."
},
"animations": {
"default": true,
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,8 @@ pub struct Config {
/// Controls whether the TUI uses the terminal's alternate screen buffer.
///
/// This is the same `tui.alternate_screen` value from `config.toml`.
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
/// - `always`: Always use alternate screen (original behavior).
/// - `auto` (default): Use alternate screen.
/// - `always`: Always use alternate screen.
/// - `never`: Never use alternate screen (inline mode, preserves scrollback).
pub tui_alternate_screen: AltScreenMode,
/// Ordered list of status line item identifiers for the TUI.
Expand Down
23 changes: 5 additions & 18 deletions codex-rs/protocol/src/config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,22 +465,9 @@ pub enum TrustLevel {

/// Controls whether the TUI uses the terminal's alternate screen buffer.
///
/// **Background:** The alternate screen buffer provides a cleaner fullscreen experience
/// without polluting the terminal's scrollback history. However, it conflicts with terminal
/// multiplexers like Zellij that strictly follow the xterm specification, which defines
/// that alternate screen buffers should not have scrollback.
///
/// **Zellij's behavior:** Zellij intentionally disables scrollback in alternate screen mode
/// (see https://github.com/zellij-org/zellij/pull/1032) to comply with the xterm spec. This
/// is by design and not configurable in Zellij—there is no option to enable scrollback in
/// alternate screen mode.
///
/// **Solution:** This setting provides a pragmatic workaround:
/// - `auto` (default): Automatically detect the terminal multiplexer. If running in Zellij,
/// disable alternate screen to preserve scrollback. Enable it everywhere else.
/// - `always`: Always use alternate screen mode (original behavior before this fix).
/// - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback
/// in all multiplexers.
/// - `auto` (default): Use alternate screen mode.
/// - `always`: Always use alternate screen mode.
/// - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback.
///
/// The CLI flag `--no-alt-screen` can override this setting at runtime.
#[derive(
Expand All @@ -489,10 +476,10 @@ pub enum TrustLevel {
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum AltScreenMode {
/// Auto-detect: disable alternate screen in Zellij, enable elsewhere.
/// Use alternate screen mode.
#[default]
Auto,
/// Always use alternate screen (original behavior).
/// Always use alternate screen mode.
Always,
/// Never use alternate screen (inline mode only).
Never,
Expand Down
127 changes: 6 additions & 121 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,6 @@ pub(crate) struct ChatComposer {
audio_device_selection_enabled: bool,
windows_degraded_sandbox_active: bool,
side_conversation_active: bool,
is_zellij: bool,
status_line_value: Option<Line<'static>>,
status_line_hyperlink_url: Option<String>,
status_line_enabled: bool,
Expand Down Expand Up @@ -599,10 +598,6 @@ impl ChatComposer {
audio_device_selection_enabled: false,
windows_degraded_sandbox_active: false,
side_conversation_active: false,
is_zellij: matches!(
codex_terminal_detection::terminal_info().multiplexer,
Some(codex_terminal_detection::Multiplexer::Zellij {})
),
status_line_value: None,
status_line_hyperlink_url: None,
status_line_enabled: false,
Expand Down Expand Up @@ -4758,56 +4753,20 @@ impl ChatComposer {
}
}
}
self.render_textarea(
composer_rect,
remote_images_rect,
textarea_rect,
buf,
mask_char,
);
}

/// Paint the composer's text input area, prompt chevron, and placeholder text.
///
/// In Zellij sessions the textarea uses explicit `Color::Reset` foreground styling
/// to prevent the multiplexer's pane chrome from bleeding into cell styles, and
/// substitutes hardcoded colors for `.bold()` / `.dim()` modifiers that Zellij
/// renders inconsistently. The standard path is unchanged.
fn render_textarea(
&self,
composer_rect: Rect,
remote_images_rect: Rect,
textarea_rect: Rect,
buf: &mut Buffer,
mask_char: Option<char>,
) {
let is_zellij = self.is_zellij;
let style = user_message_style();
let textarea_style = style.fg(ratatui::style::Color::Reset);
Block::default().style(style).render_ref(composer_rect, buf);
if !remote_images_rect.is_empty() {
Paragraph::new(self.remote_images_lines(remote_images_rect.width))
.style(style)
.render_ref(remote_images_rect, buf);
}
if is_zellij && !textarea_rect.is_empty() {
buf.set_style(textarea_rect, textarea_style);
}
if !textarea_rect.is_empty() {
let prompt = if self.input_enabled {
if self.is_bash_mode {
if is_zellij {
Span::from("!").light_red()
} else {
Span::from("!").light_red().bold()
}
} else if is_zellij {
Span::styled("›", style.fg(ratatui::style::Color::Cyan))
Span::from("!").light_red().bold()
} else {
"›".bold()
}
} else if is_zellij {
Span::styled("›", style.fg(ratatui::style::Color::DarkGray))
} else {
"›".dim()
};
Expand All @@ -4822,39 +4781,8 @@ impl ChatComposer {
let mut state = self.textarea_state.borrow_mut();
let textarea_is_empty = self.textarea.text().is_empty() && !self.is_bash_mode;
if let Some(mask_char) = mask_char {
self.textarea.render_ref_masked(
textarea_rect,
buf,
&mut state,
mask_char,
if is_zellij {
textarea_style
} else {
ratatui::style::Style::default()
},
);
} else if is_zellij && textarea_is_empty {
buf.set_style(textarea_rect, textarea_style);
} else if is_zellij {
let highlight_ranges = self.history_search_highlight_ranges();
if highlight_ranges.is_empty() {
self.textarea
.render_ref_styled(textarea_rect, buf, &mut state, textarea_style);
} else {
let highlight_style =
textarea_style.add_modifier(Modifier::REVERSED | Modifier::BOLD);
let highlights = highlight_ranges
.into_iter()
.map(|range| (range, highlight_style))
.collect::<Vec<_>>();
self.textarea.render_ref_styled_with_highlights(
textarea_rect,
buf,
&mut state,
textarea_style,
&highlights,
);
}
self.textarea
.render_ref_masked(textarea_rect, buf, &mut state, mask_char);
} else {
let highlight_ranges = self.history_search_highlight_ranges();
if highlight_ranges.is_empty() {
Expand Down Expand Up @@ -4885,18 +4813,9 @@ impl ChatComposer {
.to_string()
};
if !textarea_rect.is_empty() {
if is_zellij {
buf.set_string(
textarea_rect.x,
textarea_rect.y,
text,
textarea_style.fg(ratatui::style::Color::White).italic(),
);
} else {
let placeholder = Span::from(text).dim();
let line = Line::from(vec![placeholder]);
line.render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
}
let placeholder = Span::from(text).dim();
Line::from(vec![placeholder])
.render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
}
}
}
Expand Down Expand Up @@ -5119,35 +5038,6 @@ mod tests {
);
}

fn snapshot_zellij_composer_state<F>(name: &str, setup: F)
where
F: FnOnce(&mut ChatComposer),
{
use ratatui::Terminal;
use ratatui::backend::TestBackend;

let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ true,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
composer.is_zellij = true;
setup(&mut composer);
let footer_props = composer.footer_props();
let footer_lines = footer_height(&footer_props);
let footer_spacing = ChatComposer::footer_spacing(footer_lines);
let height = footer_lines + footer_spacing + 8;
let mut terminal = Terminal::new(TestBackend::new(100, height)).unwrap();
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.unwrap();
insta::assert_snapshot!(name, terminal.backend());
}

#[test]
fn footer_mode_snapshots() {
use crossterm::event::KeyCode;
Expand Down Expand Up @@ -5667,11 +5557,6 @@ mod tests {
);
}

#[test]
fn zellij_empty_composer_snapshot() {
snapshot_zellij_composer_state("zellij_empty_composer", |_composer| {});
}

#[test]
fn esc_hint_stays_hidden_with_draft_content() {
use crossterm::event::KeyCode;
Expand Down

This file was deleted.

28 changes: 4 additions & 24 deletions codex-rs/tui/src/bottom_pane/textarea.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Style;
use ratatui::widgets::StatefulWidgetRef;
use ratatui::widgets::WidgetRef;
Expand Down Expand Up @@ -1893,33 +1894,14 @@ impl TextArea {
buf: &mut Buffer,
state: &mut TextAreaState,
mask_char: char,
base_style: Style,
) {
let lines = self.wrapped_lines(area.width);
let scroll = self.effective_scroll(area.height, &lines, state.scroll);
state.scroll = scroll;

let start = scroll as usize;
let end = (scroll + area.height).min(lines.len() as u16) as usize;
self.render_lines_masked(area, buf, &lines, start..end, mask_char, base_style);
}

/// Render the textarea with an explicit `base_style` applied to every cell,
/// used by the Zellij code path to override inherited terminal styles.
pub(crate) fn render_ref_styled(
&self,
area: Rect,
buf: &mut Buffer,
state: &mut TextAreaState,
base_style: Style,
) {
let lines = self.wrapped_lines(area.width);
let scroll = self.effective_scroll(area.height, &lines, state.scroll);
state.scroll = scroll;

let start = scroll as usize;
let end = (scroll + area.height).min(lines.len() as u16) as usize;
self.render_lines(area, buf, &lines, start..end, base_style, &[]);
self.render_lines_masked(area, buf, &lines, start..end, mask_char);
}

/// Render the textarea with `base_style` plus additional render-only highlight ranges.
Expand Down Expand Up @@ -1970,7 +1952,7 @@ impl TextArea {
}
let styled = &self.text[overlap_start..overlap_end];
let x_off = self.text[line_range.start..overlap_start].width() as u16;
let style = base_style.fg(ratatui::style::Color::Cyan);
let style = base_style.fg(Color::Cyan);
buf.set_string(area.x + x_off, y, styled, style);
}

Expand All @@ -1996,18 +1978,16 @@ impl TextArea {
lines: &[Range<usize>],
range: std::ops::Range<usize>,
mask_char: char,
base_style: Style,
) {
for (row, idx) in range.enumerate() {
let r = &lines[idx];
let y = area.y + row as u16;
let line_range = r.start..r.end - 1;
buf.set_style(Rect::new(area.x, y, area.width, 1), base_style);
let masked = self.text[line_range.clone()]
.chars()
.map(|_| mask_char)
.collect::<String>();
buf.set_string(area.x, y, &masked, base_style);
buf.set_string(area.x, y, &masked, Style::default());
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions codex-rs/tui/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ pub struct Cli {

/// Disable alternate screen mode
///
/// Runs the TUI in inline mode, preserving terminal scrollback history. This is useful
/// in terminal multiplexers like Zellij that follow the xterm spec strictly and disable
/// scrollback in alternate screen buffers.
/// Runs the TUI in inline mode, preserving terminal scrollback history.
#[arg(long = "no-alt-screen", default_value_t = false)]
pub no_alt_screen: bool,

Expand Down
5 changes: 2 additions & 3 deletions codex-rs/tui/src/custom_terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,8 @@ where
}

/// Force the next draw pass to repaint the entire viewport by resetting the
/// diff buffer. Call this after operations that move screen content outside of
/// ratatui's knowledge (e.g., Zellij-mode scrolling via raw newlines), since
/// the diff buffer's assumptions about what is currently displayed are invalid.
/// diff buffer. Call this after raw terminal operations that move screen
/// content outside ratatui's knowledge.
pub fn invalidate_viewport(&mut self) {
self.previous_buffer_mut().reset();
}
Expand Down
Loading
Loading