mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
## Why The TUI should respect the host's accessibility preference even when `tui.animations` is enabled. ## What changed - Read the system motion preference once at launch on macOS, Windows, and Linux. Changes to that preference require restarting the TUI. - Suppress animations when reduced motion is requested without changing saved configuration. Preserve configured behavior when detection is unavailable, and limit the Linux portal query to 250 ms. - Use the effective animation setting for voice controls and transcripts, and honor system reduced motion in the startup composer. ## Testing Add tests for animation preference resolution and preservation of saved settings, plus composer snapshots without sparkles at widths of 40 and 80 columns. GitOrigin-RevId: 3ea4e721ae23220a040a06123a27286a8c8f1c7c
209 lines
7.5 KiB
Rust
209 lines
7.5 KiB
Rust
use super::*;
|
|
use crate::legacy_core::config::ConfigBuilder;
|
|
use crate::legacy_core::config::edit::ConfigEditsBuilder;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_config::types::SessionPickerViewMode;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[tokio::test]
|
|
async fn system_reduced_motion_renders_astra_composer_without_sparkles() -> anyhow::Result<()> {
|
|
use crate::app_event_sender::AppEventSender;
|
|
use crate::bottom_pane::BottomPane;
|
|
use crate::bottom_pane::BottomPaneParams;
|
|
use crate::motion::MotionMode;
|
|
use crate::render::renderable::Renderable;
|
|
use crate::tui::FrameRequester;
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
|
|
let home = tempfile::tempdir()?;
|
|
let config = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.loader_overrides(LoaderOverrides {
|
|
ignore_project_config: true,
|
|
..LoaderOverrides::without_managed_config_for_tests()
|
|
})
|
|
.build()
|
|
.await?;
|
|
let settings = LocalSettings::with_system_motion(&config, MotionMode::Reduced);
|
|
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
|
let mut pane = BottomPane::new(BottomPaneParams {
|
|
app_event_tx: AppEventSender::new(tx),
|
|
frame_requester: FrameRequester::test_dummy(),
|
|
has_input_focus: true,
|
|
enhanced_keys_supported: false,
|
|
placeholder_text: "Ask Codex to do anything".into(),
|
|
disable_paste_burst: true,
|
|
animations_enabled: settings.tui.animations,
|
|
skills: None,
|
|
});
|
|
pane.set_astra_sparkle("astra", &settings.tui);
|
|
pane.set_composer_text("Explore the night sky".into(), Vec::new(), Vec::new());
|
|
crate::terminal_palette::with_test_default_colors(
|
|
crate::terminal_probe::DefaultColors {
|
|
fg: (230, 216, 255),
|
|
bg: (36, 27, 53),
|
|
},
|
|
|| {
|
|
for width in [40, 80] {
|
|
let area = Rect::new(
|
|
/*x*/ 0,
|
|
/*y*/ 0,
|
|
width,
|
|
pane.desired_height(width),
|
|
);
|
|
let mut buffer = Buffer::empty(area);
|
|
pane.render(area, &mut buffer);
|
|
let rows = buffer
|
|
.content
|
|
.chunks(usize::from(width))
|
|
.map(|row| {
|
|
row.iter()
|
|
.map(ratatui::buffer::Cell::symbol)
|
|
.collect::<String>()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
insta::assert_snapshot!(format!("system_reduced_motion_astra_{width}"), rows);
|
|
}
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn system_motion_suppresses_animations_without_changing_saved_preferences()
|
|
-> anyhow::Result<()> {
|
|
use crate::motion::MotionMode;
|
|
|
|
for configured in [true, false] {
|
|
let home = tempfile::tempdir()?;
|
|
let config_text = format!("[tui]\nanimations = {configured}\nwhimsy = true\n");
|
|
std::fs::write(home.path().join("config.toml"), &config_text)?;
|
|
let config = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.loader_overrides(LoaderOverrides {
|
|
ignore_project_config: true,
|
|
..LoaderOverrides::without_managed_config_for_tests()
|
|
})
|
|
.build()
|
|
.await?;
|
|
let animated = LocalSettings::with_system_motion(&config, MotionMode::Animated);
|
|
let reduced = LocalSettings::with_system_motion(&config, MotionMode::Reduced);
|
|
let mut expected = animated.clone();
|
|
expected.tui.animations = false;
|
|
assert_eq!(reduced, expected);
|
|
assert_eq!(animated.tui.animations, configured);
|
|
assert_eq!(config.animations, configured);
|
|
assert_eq!(
|
|
std::fs::read_to_string(home.path().join("config.toml"))?,
|
|
config_text
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_load_preserves_defaults_and_resolved_overrides() -> anyhow::Result<()> {
|
|
for config_text in [
|
|
"",
|
|
r#"
|
|
[tui]
|
|
animations = false
|
|
whimsy = false
|
|
show_tooltips = false
|
|
show_server_version_notice = false
|
|
auto_recap = false
|
|
vim_mode_default = true
|
|
terminal_resize_reflow_max_rows = 0
|
|
session_picker_view = "comfortable"
|
|
[history]
|
|
persistence = "none"
|
|
max_bytes = 4096
|
|
[notice]
|
|
fast_default_opt_out = true
|
|
"#,
|
|
] {
|
|
let home = tempfile::tempdir()?;
|
|
std::fs::write(home.path().join("config.toml"), config_text)?;
|
|
let config = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.loader_overrides(LoaderOverrides {
|
|
ignore_project_config: true,
|
|
..LoaderOverrides::without_managed_config_for_tests()
|
|
})
|
|
.cli_overrides(vec![("tui.disable_paste_burst".into(), true.into())])
|
|
.build()
|
|
.await?;
|
|
let local = LocalSettings::from(&config);
|
|
let mut expected: Tui = toml::from_str("")?;
|
|
expected.disable_paste_burst = Some(true);
|
|
expected.session_picker_view = Some(SessionPickerViewMode::Dense);
|
|
if !config_text.is_empty() {
|
|
expected.animations = false;
|
|
expected.whimsy = false;
|
|
expected.show_tooltips = false;
|
|
expected.show_server_version_notice = false;
|
|
expected.auto_recap = false;
|
|
expected.vim_mode_default = true;
|
|
expected.terminal_resize_reflow_max_rows = Some(0);
|
|
expected.session_picker_view = Some(SessionPickerViewMode::Comfortable);
|
|
}
|
|
assert_eq!(local.tui, expected);
|
|
assert_eq!(
|
|
local.terminal_resize_reflow(),
|
|
config.terminal_resize_reflow
|
|
);
|
|
assert_eq!(
|
|
(&local.history, &local.notices),
|
|
(&config.history, &config.notices)
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_writes_preserve_selected_user_file_and_home_destinations() -> anyhow::Result<()> {
|
|
let home = tempfile::tempdir()?;
|
|
let selected = AbsolutePathBuf::from_absolute_path(home.path().join("work.config.toml"))?;
|
|
std::fs::write(&selected, "[tui]\ntheme = \"dracula\"\n")?;
|
|
let overrides = LoaderOverrides {
|
|
user_config_path: Some(selected.clone()),
|
|
user_config_profile: Some("work".parse()?),
|
|
ignore_project_config: true,
|
|
..LoaderOverrides::without_managed_config_for_tests()
|
|
};
|
|
let config = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.loader_overrides(overrides.clone())
|
|
.build()
|
|
.await?;
|
|
let local = LocalSettings::from(&config);
|
|
assert_eq!(local.user_config_path, selected);
|
|
ConfigEditsBuilder::for_config_path(local.user_config_path.as_path())
|
|
.with_edits([crate::legacy_core::config::edit::syntax_theme_edit("nord")])
|
|
.apply()
|
|
.await?;
|
|
ConfigEditsBuilder::new(local.codex_home.as_path())
|
|
.set_session_picker_view(SessionPickerViewMode::Comfortable)
|
|
.apply()
|
|
.await?;
|
|
let reloaded = ConfigBuilder::default()
|
|
.codex_home(home.path().to_path_buf())
|
|
.loader_overrides(overrides)
|
|
.build()
|
|
.await?;
|
|
assert_eq!(
|
|
LocalSettings::from(&reloaded).tui.theme.as_deref(),
|
|
Some("nord")
|
|
);
|
|
let home_config: toml::Value =
|
|
toml::from_str(&std::fs::read_to_string(home.path().join("config.toml"))?)?;
|
|
assert_eq!(
|
|
home_config["tui"]["session_picker_view"].as_str(),
|
|
Some("comfortable")
|
|
);
|
|
assert_eq!(home_config["tui"].get("theme"), None);
|
|
Ok(())
|
|
}
|