Skip to content
Draft
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
4 changes: 4 additions & 0 deletions codex-rs/terminal-detection/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub enum TerminalName {
WarpTerminal,
/// Visual Studio Code integrated terminal.
VsCode,
/// Superset xterm.js terminal host.
Superset,
/// WezTerm terminal emulator.
WezTerm,
/// kitty terminal emulator.
Expand Down Expand Up @@ -190,6 +192,7 @@ impl TerminalInfo {
format_terminal_version("WarpTerminal", &self.version)
}
TerminalName::VsCode => format_terminal_version("vscode", &self.version),
TerminalName::Superset => format_terminal_version("Superset", &self.version),
TerminalName::WezTerm => format_terminal_version("WezTerm", &self.version),
TerminalName::Kitty => "kitty".to_string(),
TerminalName::Alacritty => "Alacritty".to_string(),
Expand Down Expand Up @@ -482,6 +485,7 @@ fn terminal_name_from_term_program(value: &str) -> Option<TerminalName> {
"iterm" | "iterm2" | "itermapp" => Some(TerminalName::Iterm2),
"warp" | "warpterminal" => Some(TerminalName::WarpTerminal),
"vscode" => Some(TerminalName::VsCode),
"superset" => Some(TerminalName::Superset),
"wezterm" => Some(TerminalName::WezTerm),
"kitty" => Some(TerminalName::Kitty),
"alacritty" => Some(TerminalName::Alacritty),
Expand Down
24 changes: 24 additions & 0 deletions codex-rs/terminal-detection/src/terminal_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@ fn detects_vscode() {
);
}

#[test]
fn detects_superset() {
let env = FakeEnvironment::new()
.with_var("TERM_PROGRAM", "Superset")
.with_var("TERM_PROGRAM_VERSION", "2.0.0");
let terminal = detect_terminal_info_from_env(&env);
assert_eq!(
terminal,
terminal_info(
TerminalName::Superset,
Some("Superset"),
Some("2.0.0"),
/*term*/ None,
/*multiplexer*/ None,
),
"superset_term_program_info"
);
assert_eq!(
terminal.user_agent_token(),
"Superset/2.0.0",
"superset_term_program_user_agent"
);
}

#[test]
fn detects_warp_terminal() {
let env = FakeEnvironment::new()
Expand Down
7 changes: 5 additions & 2 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4139,7 +4139,10 @@ impl App {
app_server: &mut AppServerSession,
event: TuiEvent,
) -> Result<AppRunControl> {
if matches!(event, TuiEvent::Draw) {
if matches!(event, TuiEvent::Resize) {
tui.force_full_repaint();
}
if matches!(event, TuiEvent::Draw | TuiEvent::Resize) {
let size = tui.terminal.size()?;
if size != tui.terminal.last_known_screen_size {
self.refresh_status_line();
Expand All @@ -4161,7 +4164,7 @@ impl App {
let pasted = pasted.replace("\r", "\n");
self.chat_widget.handle_paste(pasted);
}
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
if self.backtrack_render_pending {
self.backtrack_render_pending = false;
self.render_transcript_once(tui);
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/app_backtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ impl App {
/// source of truth for the active cell and its cache invalidation key, and because `App` owns
/// overlay lifecycle and frame scheduling for animations.
fn overlay_forward_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
if let TuiEvent::Draw = &event
if matches!(&event, TuiEvent::Draw | TuiEvent::Resize)
&& let Some(Overlay::Transcript(t)) = &mut self.overlay
{
let active_key = self.chat_widget.active_cell_transcript_key();
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyB
| TerminalName::GnomeTerminal
| TerminalName::Vte
| TerminalName::WindowsTerminal
| TerminalName::Superset
| TerminalName::Dumb
| TerminalName::Unknown => key_hint::alt(KeyCode::Up),
}
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/cwd_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ pub(crate) async fn run_cwd_selection_prompt(
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) => {}
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
})?;
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/model_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ pub(crate) async fn run_model_migration_prompt(
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) => {}
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
let _ = alt.tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
});
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/onboarding/onboarding_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ pub(crate) async fn run_onboarding_app(
TuiEvent::Paste(text) => {
onboarding_screen.handle_paste(text);
}
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
if !did_full_clear_after_success
&& onboarding_screen.steps.iter().any(|step| {
if let Step::Auth(w) = step {
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/tui/src/pager_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ impl TranscriptOverlay {
}
other => self.view.handle_key_event(tui, other),
},
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
tui.draw(u16::MAX, |frame| {
self.render(frame.area(), frame.buffer);
})?;
Expand Down Expand Up @@ -764,7 +764,7 @@ impl StaticOverlay {
}
other => self.view.handle_key_event(tui, other),
},
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
tui.draw(u16::MAX, |frame| {
self.render(frame.area(), frame.buffer);
})?;
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/resume_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ async fn run_session_picker_with_loader(
return Ok(sel);
}
}
TuiEvent::Draw => {
TuiEvent::Draw | TuiEvent::Resize => {
if let Ok(size) = alt.tui.terminal.size() {
let list_height = size.height.saturating_sub(4) as usize;
state.update_view_rows(list_height);
Expand Down
40 changes: 37 additions & 3 deletions codex-rs/tui/src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,20 @@ fn set_panic_hook() {
}));
}

/// Event consumed by the top-level TUI event loop.
///
/// Resize is intentionally modeled separately from [`TuiEvent::Draw`] because resize handling has to reconcile terminal geometry and cached viewport state before rendering the next frame.
#[derive(Clone, Debug)]
pub enum TuiEvent {
/// Keyboard input delivered by crossterm.
Key(KeyEvent),
/// Bracketed paste payload delivered by crossterm.
Paste(String),
/// Terminal geometry changed or was inferred to have changed.
///
/// Resize events should be rendered immediately and should not be downgraded to a scheduled draw. Some terminal hosts can report stale cursor positions during resize, so handlers use this variant to request a conservative repaint path.
Resize,
/// Scheduled repaint request from application state changes.
Draw,
}

Expand All @@ -294,6 +304,7 @@ pub struct Tui {
notification_backend: Option<DesktopNotificationBackend>,
notification_condition: NotificationCondition,
is_zellij: bool,
force_full_repaint: bool,
// When false, enter_alt_screen() becomes a no-op (for Zellij scrollback support)
alt_screen_enabled: bool,
}
Expand All @@ -309,8 +320,9 @@ impl Tui {
// Cache this to avoid contention with the event reader.
supports_color::on_cached(supports_color::Stream::Stdout);
let _ = crate::terminal_palette::default_colors();
let terminal_info = codex_terminal_detection::terminal_info();
let is_zellij = matches!(
codex_terminal_detection::terminal_info().multiplexer,
terminal_info.multiplexer,
Some(codex_terminal_detection::Multiplexer::Zellij {})
);

Expand All @@ -329,6 +341,7 @@ impl Tui {
notification_backend: Some(detect_backend(NotificationMethod::default())),
notification_condition: NotificationCondition::default(),
is_zellij,
force_full_repaint: false,
alt_screen_enabled: true,
}
}
Expand All @@ -351,6 +364,13 @@ impl Tui {
self.frame_requester.clone()
}

/// Requests that the next draw clear and invalidate the terminal viewport.
///
/// This is used for resize handling when the cursor-position viewport heuristic is less trustworthy than the size event itself. It is a one-shot flag consumed by [`Tui::draw`]; callers should set it immediately before the draw that needs the conservative repaint.
pub(crate) fn force_full_repaint(&mut self) {
self.force_full_repaint = true;
}

pub fn enhanced_keys_supported(&self) -> bool {
self.enhanced_keys_supported
}
Expand Down Expand Up @@ -590,9 +610,18 @@ impl Tui {
.suspend_context
.prepare_resume_action(&mut self.terminal, &mut self.alt_saved_viewport);

let force_full_repaint = self.force_full_repaint;
self.force_full_repaint = false;

// Precompute any viewport updates that need a cursor-position query before entering
// the synchronized update, to avoid racing with the event reader.
let mut pending_viewport_area = self.pending_viewport_area()?;
// the synchronized update, to avoid racing with the event reader. Forced repaint
// callers skip this heuristic because some terminal hosts can report stale cursor
// positions while a blurred split pane is being resized rapidly.
let mut pending_viewport_area = if force_full_repaint {
None
} else {
self.pending_viewport_area()?
};

stdout().sync_update(|_| {
#[cfg(unix)]
Expand All @@ -614,6 +643,11 @@ impl Tui {
self.is_zellij,
)?;

if force_full_repaint {
terminal.clear()?;
needs_full_repaint = true;
}

if needs_full_repaint {
terminal.invalidate_viewport();
}
Expand Down
Loading
Loading