mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
feat(tui): width-aware theme subtitle fallback and 94x35 regression test
This commit is contained in:
@@ -1003,7 +1003,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn render_lines_with_width(view: &ListSelectionView, width: u16) -> String {
|
||||
let height = view.desired_height(width);
|
||||
render_lines_in_area(view, width, view.desired_height(width))
|
||||
}
|
||||
|
||||
fn render_lines_in_area(view: &ListSelectionView, width: u16, height: u16) -> String {
|
||||
let area = Rect::new(0, 0, width, height);
|
||||
let mut buf = Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
@@ -1089,6 +1092,20 @@ mod tests {
|
||||
assert_snapshot!("list_selection_spacing_with_subtitle", render_lines(&view));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_picker_subtitle_uses_fallback_text_in_94x35_terminal() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let home = dirs::home_dir().expect("home directory should be available");
|
||||
let codex_home = home.join(".codex");
|
||||
let params =
|
||||
crate::theme_picker::build_theme_picker_params(None, Some(&codex_home), Some(94));
|
||||
let view = ListSelectionView::new(params, tx);
|
||||
|
||||
let rendered = render_lines_in_area(&view, 94, 35);
|
||||
assert!(rendered.contains("Move up/down to live preview themes"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_footer_note_wraps() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
@@ -4436,9 +4436,14 @@ impl ChatWidget {
|
||||
|
||||
fn open_theme_picker(&mut self) {
|
||||
let codex_home = codex_core::config::find_codex_home().ok();
|
||||
let terminal_width = self
|
||||
.last_rendered_width
|
||||
.get()
|
||||
.and_then(|width| u16::try_from(width).ok());
|
||||
let params = crate::theme_picker::build_theme_picker_params(
|
||||
self.config.tui_theme.as_deref(),
|
||||
codex_home.as_deref(),
|
||||
terminal_width,
|
||||
);
|
||||
self.bottom_pane.show_selection_view(params);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ use crate::bottom_pane::SideContentWidth;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::render::highlight;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::status::format_directory_display;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Widget;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Rust snippet for the theme preview — compact enough to fit in the picker,
|
||||
/// varied enough to exercise keywords, types, strings, and macros.
|
||||
@@ -40,6 +42,17 @@ const WIDE_PREVIEW_LEFT_INSET: u16 = 2;
|
||||
/// Minimum frame padding used for vertically centered wide preview.
|
||||
const PREVIEW_FRAME_PADDING: u16 = 1;
|
||||
|
||||
const PREVIEW_FALLBACK_SUBTITLE: &str = "Move up/down to live preview themes";
|
||||
|
||||
/// Shared menu-surface horizontal inset (2 cells per side) used by selection popups.
|
||||
const MENU_SURFACE_HORIZONTAL_INSET: u16 = 4;
|
||||
|
||||
/// Horizontal gap between list and side panel when side-by-side layout is active.
|
||||
const SIDE_CONTENT_GAP: u16 = 2;
|
||||
|
||||
/// Minimum list width required for side-by-side mode in the selection popup.
|
||||
const MIN_LIST_WIDTH_FOR_SIDE: u16 = 40;
|
||||
|
||||
/// Renders a syntax-highlighted code snippet below the theme list so users can
|
||||
/// preview what each theme looks like on real code.
|
||||
struct ThemePreviewWideRenderable;
|
||||
@@ -133,6 +146,39 @@ impl Renderable for ThemePreviewNarrowRenderable {
|
||||
}
|
||||
}
|
||||
|
||||
fn subtitle_available_width(terminal_width: Option<u16>) -> usize {
|
||||
let width = terminal_width.unwrap_or(80);
|
||||
let content_width = width.saturating_sub(MENU_SURFACE_HORIZONTAL_INSET);
|
||||
let side_width = content_width.saturating_sub(SIDE_CONTENT_GAP) / 2;
|
||||
let list_width = content_width.saturating_sub(SIDE_CONTENT_GAP + side_width);
|
||||
let side_by_side =
|
||||
side_width >= WIDE_PREVIEW_MIN_WIDTH && list_width >= MIN_LIST_WIDTH_FOR_SIDE;
|
||||
if side_by_side {
|
||||
list_width as usize
|
||||
} else {
|
||||
content_width as usize
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_picker_subtitle(codex_home: Option<&Path>, terminal_width: Option<u16>) -> String {
|
||||
let themes_dir = codex_home.map(|home| home.join("themes"));
|
||||
let themes_dir_display = themes_dir
|
||||
.as_deref()
|
||||
.map(|path| format_directory_display(path, None));
|
||||
let available_width = subtitle_available_width(terminal_width);
|
||||
|
||||
if let Some(path) = themes_dir_display
|
||||
&& path.starts_with('~')
|
||||
{
|
||||
let subtitle = format!("Custom .tmTheme files can be added to the {path} directory.");
|
||||
if UnicodeWidthStr::width(subtitle.as_str()) <= available_width {
|
||||
return subtitle;
|
||||
}
|
||||
}
|
||||
|
||||
PREVIEW_FALLBACK_SUBTITLE.to_string()
|
||||
}
|
||||
|
||||
/// Builds [`SelectionViewParams`] for the `/theme` picker dialog.
|
||||
///
|
||||
/// Lists all bundled themes plus custom `.tmTheme` files, with live preview
|
||||
@@ -140,6 +186,7 @@ impl Renderable for ThemePreviewNarrowRenderable {
|
||||
pub(crate) fn build_theme_picker_params(
|
||||
current_name: Option<&str>,
|
||||
codex_home: Option<&Path>,
|
||||
terminal_width: Option<u16>,
|
||||
) -> SelectionViewParams {
|
||||
// Snapshot the current theme so we can restore on cancel.
|
||||
let original_theme = highlight::current_syntax_theme();
|
||||
@@ -204,17 +251,11 @@ pub(crate) fn build_theme_picker_params(
|
||||
highlight::set_syntax_theme(original_theme.clone());
|
||||
})
|
||||
as Box<dyn Fn(&crate::app_event_sender::AppEventSender) + Send + Sync>);
|
||||
let themes_dir_display = codex_home_owned
|
||||
.as_ref()
|
||||
.map(|home| home.join("themes"))
|
||||
.unwrap_or_else(|| Path::new("$CODEX_HOME").join("themes"))
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
SelectionViewParams {
|
||||
title: Some("Select Syntax Theme".to_string()),
|
||||
subtitle: Some(format!(
|
||||
"Custom .tmTheme files can be added to the {themes_dir_display} directory."
|
||||
subtitle: Some(theme_picker_subtitle(
|
||||
codex_home_owned.as_deref(),
|
||||
terminal_width,
|
||||
)),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
@@ -271,7 +312,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn theme_picker_uses_half_width_with_stacked_fallback_preview() {
|
||||
let params = build_theme_picker_params(None, None);
|
||||
let params = build_theme_picker_params(None, None, None);
|
||||
assert_eq!(params.side_content_width, SideContentWidth::Half);
|
||||
assert_eq!(params.side_content_min_width, WIDE_PREVIEW_MIN_WIDTH);
|
||||
assert!(params.stacked_side_content.is_some());
|
||||
@@ -328,4 +369,42 @@ mod tests {
|
||||
"expected narrow preview line numbers to start at the left edge"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtitle_uses_tilde_path_when_codex_home_under_home_directory() {
|
||||
let home = dirs::home_dir().expect("home directory should be available");
|
||||
let codex_home = home.join(".codex");
|
||||
|
||||
let subtitle = theme_picker_subtitle(Some(&codex_home), Some(200));
|
||||
|
||||
assert!(subtitle.contains("~"));
|
||||
assert!(subtitle.contains("directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtitle_falls_back_when_tilde_path_subtitle_is_too_wide() {
|
||||
let home = dirs::home_dir().expect("home directory should be available");
|
||||
let long_segment = "a".repeat(120);
|
||||
let codex_home = home.join(long_segment).join(".codex");
|
||||
|
||||
let subtitle = theme_picker_subtitle(Some(&codex_home), Some(140));
|
||||
|
||||
assert_eq!(subtitle, PREVIEW_FALLBACK_SUBTITLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtitle_falls_back_to_preview_instructions_without_tilde_path() {
|
||||
let subtitle = theme_picker_subtitle(None, None);
|
||||
assert_eq!(subtitle, PREVIEW_FALLBACK_SUBTITLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtitle_falls_back_for_94_column_terminal_side_by_side_layout() {
|
||||
let home = dirs::home_dir().expect("home directory should be available");
|
||||
let codex_home = home.join(".codex");
|
||||
|
||||
let subtitle = theme_picker_subtitle(Some(&codex_home), Some(94));
|
||||
|
||||
assert_eq!(subtitle, PREVIEW_FALLBACK_SUBTITLE);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user