Apply configuration toggles live and improve settings navigation (#53429)

## Why

Several configuration toggles required restarting Codex, and settings search only filtered the current group. Narrow terminals also truncated setting values and descriptions.

## What changed

- Apply animations, composer stars, paste detection, automatic recaps, and tips without restarting Codex. Update active motion effects and paste detection when their preferences change.
- Group animations and composer stars in an Effects picker that refreshes after saving and requires animations for composer stars.
- Search across all configuration groups, show each result's group, and preserve the query when switching groups.
- Keep setting values visible in narrow terminals and show the selected description below the list. Reserve space to keep the panel height stable and tailor footer hints to the selected action.
- Simplify configuration notices and omit routine save confirmations.

## Testing

Add and update tests and snapshots for live preference saves while offline, paste detection updates, Effects picker refreshes, global search, and stable panel height at narrow widths.

GitOrigin-RevId: 83773da2f6457ce461ef16f0cddde0df640a36c3
This commit is contained in:
Felipe Coury
2026-10-12 17:14:59 +00:00
committed by copyberry
parent f605162354
commit b3c3bad976
39 changed files with 737 additions and 341 deletions
+15 -22
View File
@@ -45,14 +45,9 @@ impl App {
}
};
let requires_restart = matches!(
preference,
ConfigPreference::Toggle { setting, .. } if setting.requires_restart()
);
if !requires_restart {
preference.adopt(&mut self.local_settings.tui, &resolved.tui);
preference.adopt(&mut self.chat_widget.local_settings.tui, &resolved.tui);
}
preference.adopt(&mut self.local_settings.tui, &resolved.tui);
self.chat_widget
.apply_config_preference(preference, &resolved.tui);
if matches!(
preference,
ConfigPreference::Toggle {
@@ -67,7 +62,7 @@ impl App {
self.local_settings.custom_notification_filters = resolved.custom_notification_filters;
self.chat_widget.local_settings.custom_notification_filters =
resolved.custom_notification_filters;
self.chat_widget.config_panel_tui = resolved.tui.clone();
preference.adopt(&mut self.chat_widget.config_panel_tui, &resolved.tui);
if matches!(
preference,
ConfigPreference::Toggle {
@@ -95,9 +90,7 @@ impl App {
ConfigPreference::ResumeDirectory(mode) => resolved.tui.resume_cwd == mode,
};
let notice = if !effective {
"Saved · another config layer or system preference overrides this value"
} else if requires_restart {
"Saved · restart Codex to apply"
Some("Saved · overridden by config or system setting")
} else if matches!(
preference,
ConfigPreference::Toggle {
@@ -105,12 +98,12 @@ impl App {
..
}
) {
"Saved · applies to new composers"
Some("Saved · applies to new composers")
} else {
"Saved"
None
};
self.chat_widget
.refresh_config_panel(/*notice*/ Some(notice.to_string()));
.refresh_config_panel(notice.map(str::to_string));
tui.frame_requester().schedule_frame();
}
}
@@ -119,7 +112,7 @@ impl App {
pub(super) fn schedule_config_notification_test(&mut self) {
if self.config_notification_test_pending.is_some() {
self.chat_widget.refresh_config_panel(/*notice*/ Some(
"A test is already scheduled · switch to another app".into(),
"Test already scheduled · switch apps".into(),
));
return;
}
@@ -128,9 +121,9 @@ impl App {
.wrapping_add(/*rhs*/ 1);
let generation = self.config_notification_test_generation;
self.config_notification_test_pending = Some(generation);
self.chat_widget.refresh_config_panel(/*notice*/ Some(
"Test scheduled in 5 seconds · switch to another app".into(),
));
self.chat_widget.refresh_config_panel(
/*notice*/ Some("Test scheduled in 5s · switch apps".into()),
);
let tx = self.app_event_tx.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(/*secs*/ 5)).await;
@@ -144,14 +137,14 @@ impl App {
}
self.config_notification_test_pending = None;
let notice = if !tui.notify_test("Codex notification test") {
"Could not send the notification test · check the terminal connection"
"Could not send test · check terminal connection"
} else if codex_terminal_detection::terminal_info().name
== codex_terminal_detection::TerminalName::Ghostty
&& tui.is_terminal_focused()
{
"Test sent to terminal · Ghostty hides banners while focused; switch apps and retry"
"Test sent · Ghostty hides banners while focused"
} else {
"Test sent to terminal · delivery depends on terminal and system settings"
"Test sent · check terminal/system settings"
};
if !self
.chat_widget
+54 -3
View File
@@ -5,12 +5,15 @@ use crate::AppServerTarget;
use crate::app::test_support::make_test_app;
use crate::app_event::AppEvent;
use crate::chatwidget::tests::make_chatwidget_manual_with_sender;
use crate::chatwidget::tests::render_bottom_popup;
use crate::config_panel::ConfigEditor;
use crate::config_panel::ConfigEditorHandoff;
use crate::local_settings::LocalSettings;
use codex_protocol::config_types::TrustLevel;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn config_toggle_save_uses_host_config_while_offline() -> Result<()> {
async fn config_toggle_save_uses_host_config_and_applies_live_while_offline() -> Result<()> {
let root = tempfile::tempdir()?;
let home = root.path().join("home");
let local_cwd = root.path().join("local");
@@ -80,18 +83,36 @@ async fn config_toggle_save_uses_host_config_while_offline() -> Result<()> {
))
.await?;
app.chat_widget.handle_key_event(KeyCode::Char('a').into());
assert!(app.chat_widget.is_in_paste_burst());
Box::pin(app.handle_event(
&mut tui,
&mut app_server,
AppEvent::SaveConfigPreference(ConfigPreference::Toggle {
setting: ConfigToggle::PasteDetection,
enabled: false,
}),
))
.await?;
assert_eq!(
(
app.local_settings.tui.auto_recap,
app.chat_widget.local_settings.tui.auto_recap,
app.chat_widget.config_panel_tui.auto_recap,
app.local_settings.tui.disable_paste_burst,
app.chat_widget.local_settings.tui.disable_paste_burst,
),
(true, true, false),
(false, false, false, Some(true), Some(true)),
);
assert!(!app.chat_widget.is_in_paste_burst());
app.chat_widget.handle_key_event(KeyCode::Char('b').into());
assert!(!app.chat_widget.is_in_paste_burst());
let saved: toml::Value = toml::from_str(&std::fs::read_to_string(
&app.local_settings.user_config_path,
)?)?;
assert_eq!(saved["tui"]["auto_recap"].as_bool(), Some(false));
assert_eq!(saved["tui"]["disable_paste_burst"].as_bool(), Some(true));
assert_eq!(app.launch_cwd, local_cwd.to_path_buf());
assert_eq!(
(&app.config.cwd, &app.chat_widget.config_ref().cwd),
@@ -102,6 +123,36 @@ async fn config_toggle_save_uses_host_config_while_offline() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn effects_editor_refreshes_after_animation_save() -> Result<()> {
let (mut app, mut rx, _ops) = crate::app::tests::make_test_app_with_channels().await;
let mut tui = crate::tui::test_support::make_test_tui()?;
app.local_settings.tui.animations = false;
app.chat_widget.local_settings.tui.animations = false;
app.chat_widget.config_panel_tui.animations = false;
let handoff = ConfigEditorHandoff::new(
ConfigEditor::Effects,
crate::key_hint::plain(KeyCode::Enter),
);
handoff.mark_activation_inactive();
app.chat_widget.open_config_editor(
handoff,
crate::config_panel::ResumeCurrentAvailability::Available,
);
app.chat_widget.handle_key_event(KeyCode::Enter.into());
let AppEvent::SaveConfigPreference(preference) = rx.try_recv()? else {
panic!("expected animation save");
};
app.save_config_preference(&mut tui, preference).await;
insta::assert_snapshot!(
"effects_picker_after_animation_save",
render_bottom_popup(&app.chat_widget, /*width*/ 80)
);
Ok(())
}
#[tokio::test(start_paused = true)]
async fn notification_test_delay_rejects_cancelled_and_rescheduled_timers() -> Result<()> {
let (mut app, mut rx, _ops) = crate::app::tests::make_test_app_with_channels().await;
@@ -146,7 +197,7 @@ async fn notification_test_delay_rejects_cancelled_and_rescheduled_timers() -> R
assert!(
cell.display_lines(/*width*/ 120)
.iter()
.any(|line| line.to_string().starts_with("• Test sent to terminal · "))
.any(|line| line.to_string().starts_with("• Test sent · "))
);
app.schedule_config_notification_test();
+3 -3
View File
@@ -3246,7 +3246,7 @@ impl App {
self.local_settings.tui.status_line = Some(ids.clone());
self.local_settings.tui.status_line_use_colors = use_theme_colors;
self.chat_widget.setup_status_line(items, use_theme_colors);
self.chat_widget.refresh_config_panel(Some("Saved".into()));
self.chat_widget.refresh_config_panel(/*notice*/ None);
}
Err(err) => {
let error = format_config_error(&err);
@@ -3288,7 +3288,7 @@ impl App {
Ok(()) => {
self.local_settings.tui.terminal_title = Some(ids.clone());
self.chat_widget.setup_terminal_title(items);
self.chat_widget.refresh_config_panel(Some("Saved".into()));
self.chat_widget.refresh_config_panel(/*notice*/ None);
}
Err(err) => {
tracing::error!(error = %err, "failed to persist terminal title items; keeping previous selection");
@@ -3357,7 +3357,7 @@ impl App {
crate::render::highlight::set_syntax_theme(theme);
}
self.sync_tui_theme_selection(name);
self.chat_widget.refresh_config_panel(Some("Saved".into()));
self.chat_widget.refresh_config_panel(/*notice*/ None);
self.refresh_status_line();
tui.frame_requester().schedule_frame();
}
+4 -4
View File
@@ -54,17 +54,17 @@ impl PasteEnvironment {
return Ok(false);
}
if !transcript_mode.is_owned() {
return Err("Unavailable in inline mode. Use your terminal's paste command.");
return Err("Unavailable in inline mode; use your terminal's paste command");
}
if self.ssh {
return Err("Unavailable over SSH. Use your terminal's paste command.");
return Err("Unavailable over SSH; use your terminal's paste command");
}
if self.vscode == VscodeDetection::VsCode {
return Err("VS Code handles pasting. Use its terminal paste command.");
return Err("VS Code handles pasting; use its terminal paste command");
}
if mode == RightClickPaste::Auto && self.wsl && self.vscode == VscodeDetection::Unknown {
return Err(
"Auto cannot identify this WSL terminal. Use On or your terminal's paste command.",
"Auto cannot identify this WSL terminal; use On or your terminal's paste command",
);
}
match mode {
@@ -7,12 +7,12 @@ fn policy_preserves_explicit_modes_and_terminal_ownership() {
use VscodeDetection::Other;
use VscodeDetection::Unknown;
use VscodeDetection::VsCode;
const SSH_UNAVAILABLE: &str = "Unavailable over SSH. Use your terminal's paste command.";
const VSCODE_UNAVAILABLE: &str = "VS Code handles pasting. Use its terminal paste command.";
const SSH_UNAVAILABLE: &str = "Unavailable over SSH; use your terminal's paste command";
const VSCODE_UNAVAILABLE: &str = "VS Code handles pasting; use its terminal paste command";
const WSL_AUTO_UNAVAILABLE: &str =
"Auto cannot identify this WSL terminal. Use On or your terminal's paste command.";
"Auto cannot identify this WSL terminal; use On or your terminal's paste command";
const INLINE_UNAVAILABLE: &str =
"Unavailable in inline mode. Use your terminal's paste command.";
"Unavailable in inline mode; use your terminal's paste command";
let on = if cfg!(target_os = "android") {
Err("Unavailable on Android")
} else {
@@ -0,0 +1,13 @@
---
source: tui/src/app/config_panel_tests.rs
expression: "render_bottom_popup(&app.chat_widget, 80)"
---
Effects
Configure visual effects
› 1. [x] Animations Animate the welcome screen, working indicator, and
transitions
2. [x] Composer stars Show stars briefly for new Astra tasks
enter toggle · esc back
@@ -33,6 +33,7 @@ use crate::render::Insets;
use crate::render::RectExt as _;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::wrapping::word_wrap_lines;
use super::CancellationEvent;
use super::bottom_pane_view::BottomPaneView;
@@ -48,6 +49,7 @@ use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::measure_rows_height_with_col_width_mode;
use super::selection_popup_common::render_rows_single_line_with_col_width_mode;
use super::selection_popup_common::render_rows_with_col_width_mode;
use super::selection_popup_common::should_show_selected_description_below;
pub(crate) use super::selection_row_layout::SelectionDescriptionLayout;
use super::selection_tabs::SelectionTab;
use super::selection_tabs::render_tab_bar;
@@ -165,12 +167,18 @@ pub(crate) struct SelectionItem {
pub toggle: Option<SelectionToggle>,
pub toggle_placeholder: Option<&'static str>,
pub display_shortcut: Option<ShortcutHint>,
/// Required row metadata retained when the secondary description hides.
pub category_tag: Option<String>,
pub description: Option<String>,
pub selected_description: Option<String>,
pub is_current: bool,
pub is_disabled: bool,
pub actions: Vec<SelectionAction>,
pub secondary_action: Option<SelectionSecondaryAction>,
/// Footer hint shown while this row is selected.
pub footer_hint: Option<Line<'static>>,
/// Footer hint shown instead while on-demand search is active.
pub search_footer_hint: Option<Line<'static>>,
pub dismiss_on_select: bool,
/// Require an explicit accept key after a direct shortcut highlights this sensitive item.
pub require_explicit_confirmation: bool,
@@ -208,6 +216,8 @@ pub(crate) struct SelectionViewParams {
pub footer_hint: Option<Line<'static>>,
pub tab_footer_hints: Vec<(String, Line<'static>)>,
pub items: Vec<SelectionItem>,
/// Optional flat item list used while a tabbed picker has a non-empty search query.
pub search_items: Vec<SelectionItem>,
pub tabs: Vec<SelectionTab>,
pub initial_tab_id: Option<String>,
pub is_searchable: bool,
@@ -274,6 +284,7 @@ impl Default for SelectionViewParams {
footer_hint: None,
tab_footer_hints: Vec::new(),
items: Vec::new(),
search_items: Vec::new(),
tabs: Vec::new(),
initial_tab_id: None,
is_searchable: false,
@@ -325,6 +336,7 @@ pub(crate) struct ListSelectionView {
footer_hint: Option<Line<'static>>,
tab_footer_hints: Vec<(String, Line<'static>)>,
items: Vec<SelectionItem>,
search_items: Vec<SelectionItem>,
tabs: Vec<SelectionTab>,
active_tab_idx: Option<usize>,
state: ScrollState,
@@ -473,6 +485,7 @@ impl ListSelectionView {
footer_hint: params.footer_hint,
tab_footer_hints: params.tab_footer_hints,
items: params.items,
search_items: params.search_items,
tabs: params.tabs,
active_tab_idx,
state: ScrollState::new(),
@@ -526,7 +539,23 @@ impl ListSelectionView {
self.active_tab_idx.is_some()
}
fn search_items_active(&self) -> bool {
!self.search_query.is_empty() && !self.search_items.is_empty()
}
fn edit_search_query(&mut self, edit: impl FnOnce(&mut String)) {
let search_items_were_active = self.search_items_active();
edit(&mut self.search_query);
if search_items_were_active != self.search_items_active() {
self.state.reset();
}
self.apply_filter();
}
fn active_items(&self) -> &[SelectionItem] {
if self.search_items_active() {
return self.search_items.as_slice();
}
self.active_tab_idx
.and_then(|idx| self.tabs.get(idx))
.map(|tab| tab.items.as_slice())
@@ -534,6 +563,9 @@ impl ListSelectionView {
}
fn active_items_mut(&mut self) -> &mut [SelectionItem] {
if self.search_items_active() {
return self.search_items.as_mut_slice();
}
if let Some(idx) = self.active_tab_idx
&& let Some(tab) = self.tabs.get_mut(idx)
{
@@ -565,12 +597,21 @@ impl ListSelectionView {
}
fn active_footer_hint(&self) -> Option<&Line<'static>> {
if let Some(action) = self
if let Some(item) = self
.selected_actual_idx()
.and_then(|idx| self.active_items().get(idx))
.and_then(|item| item.secondary_action.as_ref())
{
return Some(&action.footer_hint);
if self.on_demand_search_active()
&& let Some(hint) = item.search_footer_hint.as_ref()
{
return Some(hint);
}
if let Some(hint) = item.footer_hint.as_ref() {
return Some(hint);
}
if let Some(action) = item.secondary_action.as_ref() {
return Some(&action.footer_hint);
}
}
self.active_tab_id()
.and_then(|active_tab_id| {
@@ -735,7 +776,7 @@ impl ListSelectionView {
let wrap_indent = (description.is_none() && item.disabled_reason.is_none())
.then_some(wrap_prefix_width);
GenericDisplayRow {
category_tag: None,
category_tag: item.category_tag.clone(),
selection_style: row_selection_style,
name: name_with_marker,
name_prefix_spans,
@@ -766,7 +807,9 @@ impl ListSelectionView {
(active_idx + 1) % len
};
self.active_tab_idx = Some(next_idx);
self.search_query.clear();
if self.search_items.is_empty() {
self.search_query.clear();
}
self.rendered_item_count.set(/*val*/ 0);
self.state.reset();
self.apply_filter();
@@ -961,16 +1004,22 @@ impl ListSelectionView {
fn stop_on_demand_search(&mut self) {
self.search_active = false;
self.search_query.clear();
self.apply_filter();
self.edit_search_query(String::clear);
}
pub(crate) fn set_search_query(&mut self, query: String) {
let selected_actual_idx = self.selected_actual_idx();
let changes_item_source =
self.search_items_active() != (!query.is_empty() && !self.search_items.is_empty());
self.search_active = true;
self.search_query = query;
self.state.selected_idx = None;
self.initial_selected_idx = selected_actual_idx;
if changes_item_source {
self.state.reset();
self.initial_selected_idx = None;
} else {
self.state.selected_idx = None;
self.initial_selected_idx = selected_actual_idx;
}
self.apply_filter();
}
@@ -993,6 +1042,53 @@ impl ListSelectionView {
.iter()
.map(|hint| wrap_styled_line(hint, width).len()),
)
.chain(
self.items
.iter()
.chain(self.search_items.iter())
.chain(self.tabs.iter().flat_map(|tab| tab.items.iter()))
.flat_map(|item| {
[
item.footer_hint.as_ref().or_else(|| {
item.secondary_action
.as_ref()
.map(|action| &action.footer_hint)
}),
item.search_footer_hint.as_ref(),
]
.into_iter()
.flatten()
})
.map(|hint| wrap_styled_line(hint, width).len()),
)
.max()
.unwrap_or(lines.len());
lines.resize(height.max(lines.len()), Line::default());
}
lines
}
fn footer_note_lines(&self, width: u16, show_selected_description: bool) -> Vec<Line<'static>> {
let wrap = |line| word_wrap_lines([line], usize::from(width.max(1)));
let note = if show_selected_description {
self.selected_actual_idx()
.and_then(|idx| self.active_items().get(idx))
.and_then(|item| item.description.as_ref())
.map(|description| Line::from(description.clone()).dim())
} else {
None
}
.or_else(|| self.footer_note.clone());
let mut lines = note.map(&wrap).unwrap_or_default();
if self.reserve_result_rows && show_selected_description {
let height = self
.items
.iter()
.chain(self.search_items.iter())
.chain(self.tabs.iter().flat_map(|tab| tab.items.iter()))
.filter_map(|item| item.description.as_deref())
.map(|description| wrap(Line::from(description).dim()).len())
.chain(self.footer_note.iter().map(|note| wrap(note.clone()).len()))
.max()
.unwrap_or(lines.len());
lines.resize(height.max(lines.len()), Line::default());
@@ -1212,8 +1308,9 @@ impl BottomPaneView for ListSelectionView {
code: KeyCode::Backspace,
..
} if search_field_visible => {
self.search_query.pop();
self.apply_filter();
self.edit_search_query(|query| {
query.pop();
});
}
KeyEvent {
code: KeyCode::Char(' '),
@@ -1274,8 +1371,7 @@ impl BottomPaneView for ListSelectionView {
&& !modifiers.contains(KeyModifiers::CONTROL)
&& !modifiers.contains(KeyModifiers::ALT) =>
{
self.search_query.push(c);
self.apply_filter();
self.edit_search_query(|query| query.push(c));
}
KeyEvent {
code: KeyCode::Char(c),
@@ -1314,8 +1410,7 @@ impl BottomPaneView for ListSelectionView {
let Some(pasted) = normalize_pasted_search_query(&pasted) else {
return false;
};
self.search_query.push_str(&pasted);
self.apply_filter();
self.edit_search_query(|query| query.push_str(&pasted));
true
}
@@ -1437,11 +1532,17 @@ impl Renderable for ListSelectionView {
}
}
if let Some(note) = &self.footer_note {
let note_width = width.saturating_sub(2);
let note_lines = wrap_styled_line(note, note_width);
height = height.saturating_add(note_lines.len() as u16);
}
let show_selected_description = should_show_selected_description_below(
&rows,
&self.state,
self.max_visible_rows,
effective_rows_width,
column_width,
);
height = height.saturating_add(
self.footer_note_lines(width.saturating_sub(/*rhs*/ 2), show_selected_description)
.len() as u16,
);
height = height.saturating_add(
self.footer_hint_lines(width.saturating_sub(/*rhs*/ 2))
.len() as u16,
@@ -1456,16 +1557,29 @@ impl Renderable for ListSelectionView {
return;
}
let search_field_visible = self.search_field_visible();
let inner_width = popup_content_width(area.width);
let side_w = self.side_layout_width(inner_width);
let effective_rows_width = if let Some(sw) = side_w {
area.width
.saturating_sub(MENU_SURFACE_HORIZONTAL_INSET / 2 + SIDE_CONTENT_GAP + sw)
} else {
area.width
};
let rows = self.build_rows();
let column_width = ColumnWidthConfig::new(self.col_width_mode, self.name_column_width)
.with_description_layout(self.description_layout);
let show_selected_description = should_show_selected_description_below(
&rows,
&self.state,
self.max_visible_rows,
effective_rows_width,
column_width,
);
let note_width = area.width.saturating_sub(2);
let note_lines = self
.footer_note
.as_ref()
.map(|note| wrap_styled_line(note, note_width));
let note_height = note_lines.as_ref().map_or(0, |lines| lines.len() as u16);
let note_lines = self.footer_note_lines(note_width, show_selected_description);
let note_height = note_lines.len() as u16;
let hint_lines = self.footer_hint_lines(note_width);
let hint_height = hint_lines.len() as u16;
let inner_width = popup_content_width(area.width);
let header_height = self.header_height(inner_width);
let tab_height = tab_bar_height(&self.tabs, inner_width);
let mut footer_rows = note_height + hint_height;
@@ -1502,21 +1616,7 @@ impl Renderable for ListSelectionView {
content_area
};
let side_w = self.side_layout_width(inner_width);
// When side-by-side is active, shrink the list to make room.
let effective_rows_width = if let Some(sw) = side_w {
outer_content_area
.width
.saturating_sub(MENU_SURFACE_HORIZONTAL_INSET / 2 + SIDE_CONTENT_GAP + sw)
} else {
outer_content_area.width
};
let header = self.active_header(inner_width);
let rows = self.build_rows();
let column_width = ColumnWidthConfig::new(self.col_width_mode, self.name_column_width)
.with_description_layout(self.description_layout);
let rows_height = match self.row_display {
SelectionRowDisplay::Wrapped => measure_rows_height_with_col_width_mode(
&rows,
@@ -1711,14 +1811,14 @@ impl Renderable for ListSelectionView {
])
.areas(footer_area);
if let Some(lines) = note_lines {
if !note_lines.is_empty() {
let note_area = Rect {
x: note_area.x + 2,
y: note_area.y,
width: note_area.width.saturating_sub(2),
height: note_area.height,
};
for (idx, line) in lines.iter().enumerate() {
for (idx, line) in note_lines.iter().enumerate() {
if idx as u16 >= note_area.height {
break;
}
+9
View File
@@ -426,6 +426,15 @@ impl BottomPane {
self.request_redraw();
}
pub(crate) fn set_motion_preferences(&mut self, tui: &codex_config::types::Tui) {
self.animations_enabled = tui.animations;
self.effects = tui.effects;
if let Some(status) = self.status.as_mut() {
status.set_motion_preferences(tui);
}
self.request_redraw();
}
/// Mirrors the effective reasoning effort into the composer so its next
/// visible frame can play a one-shot Max/Ultra effect.
pub(crate) fn set_active_reasoning_effort(&mut self, effort: Option<&ReasoningEffort>) {
@@ -176,7 +176,7 @@ fn compute_desc_col(
.min(max_auto_desc_col);
if column_width
.description_layout
.should_hide(content_width, desc_col)
.should_hide_column(content_width, desc_col)
{
0
} else {
@@ -184,6 +184,24 @@ fn compute_desc_col(
}
}
pub(super) fn should_show_selected_description_below(
rows_all: &[GenericDisplayRow],
state: &ScrollState,
max_results: usize,
width: u16,
column_width: ColumnWidthConfig,
) -> bool {
if rows_all.is_empty() {
return false;
}
let visible_items = max_results.min(rows_all.len());
let start_idx = compute_item_window_start(rows_all, state, visible_items);
let desc_col = compute_desc_col(rows_all, start_idx, visible_items, width, column_width);
column_width
.description_layout
.should_show_selected_below(width, desc_col)
}
/// Determine how many spaces to indent wrapped lines for a row.
fn wrap_indent(row: &GenericDisplayRow, desc_col: usize, max_width: u16) -> usize {
let max_indent = max_width.saturating_sub(1) as usize;
@@ -15,19 +15,33 @@ pub(crate) enum SelectionDescriptionLayout {
HideWhenNarrow {
min_description_width: u16,
},
SelectedBelowWhenNarrow {
min_description_width: u16,
},
}
impl SelectionDescriptionLayout {
pub(super) fn should_hide(self, width: u16, desc_col: usize) -> bool {
let Self::HideWhenNarrow {
min_description_width,
} = self
else {
return false;
fn is_narrow(self, width: u16, desc_col: usize) -> bool {
let min_description_width = match self {
Self::Columns => return false,
Self::HideWhenNarrow {
min_description_width,
}
| Self::SelectedBelowWhenNarrow {
min_description_width,
} => min_description_width,
};
let desc_col = desc_col.min(width as usize) as u16;
width.saturating_sub(desc_col) < min_description_width
}
pub(super) fn should_hide_column(self, width: u16, desc_col: usize) -> bool {
matches!(self, Self::HideWhenNarrow { .. }) && self.is_narrow(width, desc_col)
}
pub(super) fn should_show_selected_below(self, width: u16, desc_col: usize) -> bool {
matches!(self, Self::SelectedBelowWhenNarrow { .. }) && self.is_narrow(width, desc_col)
}
}
fn combined_description(
@@ -41,6 +55,7 @@ fn combined_description(
if matches!(
description_layout,
SelectionDescriptionLayout::HideWhenNarrow { .. }
| SelectionDescriptionLayout::SelectedBelowWhenNarrow { .. }
) =>
{
Some(reason.clone())
@@ -104,9 +119,10 @@ pub(super) fn build_full_line(
width: u16,
description_layout: SelectionDescriptionLayout,
) -> Line<'static> {
let description = (desc_col > 0)
.then(|| combined_description(row, description_layout))
.flatten();
let description = (desc_col > 0
&& !description_layout.should_show_selected_below(width, desc_col))
.then(|| combined_description(row, description_layout))
.flatten();
let name_prefix_width = line_width(&Line::from(row.name_prefix_spans.clone()));
// Category tags identify the result even when secondary descriptions hide.
// Reserve their cells before truncating the name on narrow rows.
+58 -3
View File
@@ -6,6 +6,8 @@ use crate::config_panel::ConfigEditorHandoff;
use crate::config_panel::ConfigPanel;
use crate::config_panel::ConfigPanelStatus;
use crate::config_panel::ConfigPanelUpdate;
use crate::config_panel::ConfigPreference;
use crate::config_panel::ConfigToggle;
use crossterm::event::KeyEvent;
impl ChatWidget {
@@ -25,6 +27,11 @@ impl ChatWidget {
) {
match handoff.editor {
ConfigEditor::Theme => self.open_theme_picker(),
ConfigEditor::Effects => {
let keymap = self.bottom_pane.list_keymap();
let params = crate::config_panel::effects(&self.local_settings.tui, &keymap);
self.bottom_pane.show_selection_view(params);
}
ConfigEditor::StatusLine => self.open_status_line_setup(),
ConfigEditor::TerminalTitle => self.open_terminal_title_setup(),
ConfigEditor::Keyboard => self.open_keymap_picker(),
@@ -72,18 +79,66 @@ impl ChatWidget {
self.bottom_pane.interrupt_config_editor_transition();
}
pub(crate) fn apply_config_preference(
&mut self,
preference: ConfigPreference,
tui: &codex_config::types::Tui,
) {
preference.adopt(&mut self.local_settings.tui, tui);
let tui = &self.local_settings.tui;
let ConfigPreference::Toggle { setting, .. } = preference else {
return;
};
match setting {
ConfigToggle::Animations | ConfigToggle::ComposerStars => {
self.bottom_pane.set_motion_preferences(tui);
let model = self.current_model().to_string();
self.bottom_pane.stop_ineligible_sparkle(&model, tui);
self.bottom_pane.select_sparkle_model(&model, tui);
}
ConfigToggle::PasteDetection => self.bottom_pane.set_disable_paste_burst(
/*disabled*/ tui.disable_paste_burst.unwrap_or(/*default*/ false),
),
ConfigToggle::AutomaticRecaps
| ConfigToggle::CopyOnSelectClearSelection
| ConfigToggle::Notifications
| ConfigToggle::StartInVimMode
| ConfigToggle::StatusLineColors
| ConfigToggle::Tips => {}
}
if setting == ConfigToggle::Animations
&& let Some(pet) = self.ambient_pet.as_mut()
{
pet.set_animations_enabled(tui.animations);
}
}
pub(crate) fn observe_config_editor_transition_key_event(&mut self, key: KeyEvent) {
self.bottom_pane
.observe_config_editor_transition_key_event(key);
}
pub(crate) fn refresh_config_panel(&mut self, notice: Option<String>) -> bool {
self.bottom_pane.update_config_panel(&ConfigPanelUpdate {
let panel_updated = self.bottom_pane.update_config_panel(&ConfigPanelUpdate {
tui: self.config_panel_tui.clone(),
keymap: self.bottom_pane.list_keymap(),
status: self.config_panel_status(),
notice,
})
notice: notice.clone(),
});
let Some(selected_idx) = self
.bottom_pane
.selected_index_for_active_view(crate::config_panel::EFFECTS_VIEW_ID)
else {
return panel_updated;
};
let keymap = self.bottom_pane.list_keymap();
let mut params = crate::config_panel::effects(&self.config_panel_tui, &keymap);
params.initial_selected_idx = Some(selected_idx);
params.subtitle = notice.or(params.subtitle);
let effects_updated = self
.bottom_pane
.replace_selection_view_if_active(crate::config_panel::EFFECTS_VIEW_ID, params);
panel_updated || effects_updated
}
fn config_panel_status(&self) -> ConfigPanelStatus {
+1 -1
View File
@@ -172,7 +172,7 @@ impl ChatWidget {
self.copy_last_response_binding = runtime_keymap.app.copy.clone();
self.chat_keymap = runtime_keymap.chat.clone();
self.bottom_pane.set_keymap_bindings(runtime_keymap);
self.refresh_config_panel(Some("Saved".into()));
self.refresh_config_panel(/*notice*/ None);
self.update_realtime_footer();
self.request_redraw();
}
+1
View File
@@ -307,6 +307,7 @@ mod usage;
mod worktree_picker;
pub(crate) use helpers::make_chatwidget_manual_with_sender;
pub(crate) use helpers::render_bottom_popup;
pub(crate) use helpers::set_chatgpt_auth;
pub(crate) use helpers::set_fast_mode_test_catalog;
pub(super) use helpers::*;
@@ -70,6 +70,14 @@ fn config_panel_renders_each_group_at_a_stable_height() {
for section in ConfigSection::ALL {
assert_eq!(panel.picker.active_tab_id(), Some(section.title()));
assert_eq!(panel.desired_height(/*width*/ 140), height);
let narrow_height = panel.desired_height(/*width*/ 50);
panel
.picker
.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
assert_eq!(panel.desired_height(/*width*/ 50), narrow_height);
panel
.picker
.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
insta::assert_snapshot!(
format!("config_{}", section.title()),
render(&panel, /*width*/ 140)
@@ -80,6 +88,36 @@ fn config_panel_renders_each_group_at_a_stable_height() {
}
}
#[test]
fn effects_picker_groups_animation_settings() {
let (tx, mut rx) = unbounded_channel();
let keymap = RuntimeKeymap::defaults().list;
let mut view = ListSelectionView::new(
effects(&Tui::default(), &keymap),
AppEventSender::new(tx),
keymap,
);
insta::assert_snapshot!("effects_picker", render(&view, /*width*/ 80));
view.handle_key_event(KeyCode::Enter.into());
assert!(!view.is_complete());
assert!(matches!(
rx.try_recv().expect("save animation event"),
AppEvent::SaveConfigPreference(ConfigPreference::Toggle {
setting: ConfigToggle::Animations,
enabled: true,
})
));
view.handle_key_event(KeyCode::Enter.into());
assert!(matches!(
rx.try_recv().expect("save animation event"),
AppEvent::SaveConfigPreference(ConfigPreference::Toggle {
setting: ConfigToggle::Animations,
enabled: false,
})
));
}
#[test]
fn deselect_after_copy_requires_effective_copy_on_select() {
let (mut panel, mut rx) = self::panel();
@@ -97,7 +135,7 @@ fn deselect_after_copy_requires_effective_copy_on_select() {
});
insta::assert_snapshot!(
"deselect_after_copy_unavailable",
render(&panel, /*width*/ 140)
render(&panel, /*width*/ 50)
);
panel.handle_key_event(KeyCode::Enter.into());
assert!(rx.try_recv().is_err());
@@ -156,19 +194,31 @@ fn notification_controls_render_dependency_states_and_condition_picker() {
/*custom_notification_filters*/ false,
);
panel.handle_key_event(KeyCode::Right.into());
panel.handle_key_event(KeyCode::Down.into());
insta::assert_snapshot!("config_notifications_off", render(&panel, /*width*/ 100));
}
#[test]
fn filtered_selection_opens_the_matching_existing_editor() {
fn search_finds_settings_across_groups_and_preserves_the_selected_group() {
let (mut panel, mut rx) = panel();
panel.handle_key_event(KeyEvent::from(KeyCode::Char('/')));
assert!(panel.handle_paste("terminal title".to_string()));
assert!(panel.handle_paste("notify when".to_string()));
insta::assert_snapshot!("config_global_search", render(&panel, /*width*/ 100));
panel.handle_key_event(KeyEvent::from(KeyCode::Right));
assert_eq!(panel.picker.active_tab_id(), Some("Notifications"));
assert_eq!(panel.picker.search_query(), Some("notify when"));
panel.handle_key_event(KeyEvent::from(KeyCode::Esc));
assert_eq!(panel.picker.active_tab_id(), Some("Notifications"));
assert_eq!(panel.picker.search_query(), None);
panel.handle_key_event(KeyEvent::from(KeyCode::Char('/')));
assert!(panel.handle_paste("composer stars".to_string()));
panel.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(matches!(
rx.try_recv().expect("open editor event"),
AppEvent::OpenConfigEditor(handoff) if handoff.editor == ConfigEditor::TerminalTitle
AppEvent::OpenConfigEditor(handoff) if handoff.editor == ConfigEditor::Effects
));
assert!(rx.try_recv().is_err());
assert!(!panel.is_complete());
@@ -210,6 +260,7 @@ fn on_demand_search_preserves_printable_list_bindings() {
let mut buffer = Buffer::empty(area);
panel.render(area, &mut buffer);
assert!(panel.cursor_pos(area).is_some());
assert!(render(&panel, /*width*/ 84).contains("enter open · esc clear search"));
panel.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert!(matches!(
rx.try_recv().expect("open theme editor event"),
@@ -320,9 +371,7 @@ fn unavailable_right_click_paste_remains_editable() {
AppEventSender::new(tx),
RuntimeKeymap::defaults().list,
ConfigPanelStatus {
right_click_paste: Err(
"Unavailable in inline mode. Use your terminal's paste command.",
),
right_click_paste: Err("Unavailable in inline mode; use your terminal's paste command"),
..STATUS
},
);
@@ -392,12 +441,12 @@ fn resume_directory_disables_current_without_remote_cwd_override() {
fn refresh_preserves_search_and_updates_visible_state() {
let (mut panel, mut rx) = panel();
panel.handle_key_event(KeyEvent::from(KeyCode::Char('/')));
assert!(panel.handle_paste("animations".to_string()));
assert!(panel.handle_paste("tips".to_string()));
panel.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(matches!(
rx.try_recv().expect("save toggle event"),
AppEvent::SaveConfigPreference(ConfigPreference::Toggle {
setting: ConfigToggle::Animations,
setting: ConfigToggle::Tips,
enabled: true,
})
));
@@ -407,21 +456,21 @@ fn refresh_preserves_search_and_updates_visible_state() {
keymap.move_right.clear();
panel.update_config_panel(&ConfigPanelUpdate {
tui: Tui {
animations: true,
show_tooltips: true,
..Default::default()
},
keymap,
status: STATUS,
notice: Some("Saved · restart Codex to apply".to_string()),
notice: Some("Test sent · check terminal/system settings".to_string()),
});
assert_eq!(panel.picker.search_query(), Some("animations"));
assert_eq!(panel.picker.search_query(), Some("tips"));
assert!(!panel.saving);
panel.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(matches!(
rx.try_recv().expect("immediate second press saves again"),
AppEvent::SaveConfigPreference(ConfigPreference::Toggle {
setting: ConfigToggle::Animations,
setting: ConfigToggle::Tips,
enabled: false,
})
));
+7
View File
@@ -18,6 +18,13 @@ use crossterm::event::KeyEventKind;
impl ConfigPanel {
fn setting_index(&self, selected: usize) -> Option<usize> {
if self
.picker
.search_query()
.is_some_and(|query| !query.is_empty())
{
return self.settings.get(selected).map(|_| selected);
}
let tab = self
.picker
.active_tab_id()
+33 -54
View File
@@ -6,7 +6,9 @@
mod input;
mod render;
mod toggle;
pub(crate) use render::EFFECTS_VIEW_ID;
pub(crate) use render::copy_on_select;
pub(crate) use render::effects;
pub(crate) use render::notification_condition;
pub(crate) use render::resume_directory;
pub(crate) use render::right_click_paste;
@@ -31,8 +33,7 @@ use std::sync::Arc;
use std::sync::Mutex;
use std::time::Instant;
const RIGHT_CLICK_PASTE_DESCRIPTION: &str =
"Paste when nothing is selected. Auto follows the platform default; On also enables macOS.";
const RIGHT_CLICK_PASTE_DESCRIPTION: &str = "Paste when nothing is selected, with Auto following platform defaults and On also enabling macOS";
pub(crate) struct ConfigPanelUpdate {
pub(crate) tui: Tui,
@@ -52,6 +53,7 @@ struct PickerState {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ConfigEditor {
Theme,
Effects,
StatusLine,
TerminalTitle,
Keyboard,
@@ -174,7 +176,9 @@ impl ConfigEditor {
NotificationCondition::Always => "Always",
}
.to_string(),
Self::StatusLine | Self::TerminalTitle | Self::Keyboard => "Configure".to_string(),
Self::Effects | Self::StatusLine | Self::TerminalTitle | Self::Keyboard => {
"Configure".to_string()
}
};
ConfigSettingValue::Editor {
label,
@@ -220,16 +224,6 @@ struct ConfigSetting {
value: ConfigSettingValue,
}
impl ConfigSetting {
fn display_label(&self) -> String {
if matches!(self.value, ConfigSettingValue::DependentToggle { .. }) {
format!(" {}", self.label)
} else {
self.label.to_string()
}
}
}
enum ConfigSettingValue {
Toggle {
setting: ConfigToggle,
@@ -261,7 +255,7 @@ impl ConfigSettingValue {
..
} => if *enabled { "[x] On" } else { "[ ] Off" }.to_string(),
Self::Editor { label, .. } => format!("{label} ›"),
Self::NotificationTest => "Send in 5s ›".to_string(),
Self::NotificationTest => "Send in 5s".to_string(),
Self::DependentToggle {
dependency_met: false,
..
@@ -280,36 +274,21 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::Appearance,
label: "Theme",
description: "Choose a syntax color palette with a live preview.",
description: "Choose a syntax color palette with a live preview",
keywords: "colors dark light palette",
value: ConfigEditor::Theme.value(tui, status),
},
ConfigSetting {
section: ConfigSection::Appearance,
label: "Animations",
description: "Animate the welcome screen, working indicator, and transitions. Restart Codex to apply.",
keywords: "motion accessibility spinner",
value: ConfigSettingValue::Toggle {
setting: ConfigToggle::Animations,
enabled: ConfigToggle::Animations.enabled(tui),
},
},
ConfigSetting {
section: ConfigSection::Appearance,
label: "Composer stars",
description: "Animate stars in the composer. Requires animations and a restart.",
keywords: "effects starfield stars motion",
value: ConfigSettingValue::DependentToggle {
setting: ConfigToggle::ComposerStars,
enabled: ConfigToggle::ComposerStars.enabled(tui),
dependency_met: tui.animations,
unavailable_reason: "Turn on animations, then restart Codex to enable composer stars.",
},
label: "Effects",
description: "Configure animations and composer effects",
keywords: "animations motion accessibility spinner starfield stars composer stars",
value: ConfigEditor::Effects.value(tui, status),
},
ConfigSetting {
section: ConfigSection::Appearance,
label: "Tips",
description: "Show helpful shortcuts and suggestions. Restart Codex to apply.",
description: "Show helpful shortcuts and suggestions",
keywords: "welcome hints tooltips",
value: ConfigSettingValue::Toggle {
setting: ConfigToggle::Tips,
@@ -319,7 +298,7 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::Appearance,
label: "Status line colors",
description: "Use theme colors for the model, branch, and other status line fields.",
description: "Use theme colors for the model, branch, and other status line details",
keywords: "statusline footer colour",
value: ConfigSettingValue::Toggle {
setting: ConfigToggle::StatusLineColors,
@@ -328,26 +307,26 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
},
ConfigSetting {
section: ConfigSection::Appearance,
label: "Status line fields",
description: "Choose and reorder the information below the composer.",
label: "Status line display",
description: "Choose and reorder the information below the composer",
keywords: "statusline footer model branch context",
value: ConfigEditor::StatusLine.value(tui, status),
},
ConfigSetting {
section: ConfigSection::Appearance,
label: "Terminal title fields",
description: "Choose and reorder the information in your terminal tab or window title.",
label: "Terminal title display",
description: "Choose and reorder the information in your terminal tab or window title",
keywords: "title tab window name",
value: ConfigEditor::TerminalTitle.value(tui, status),
},
ConfigSetting {
section: ConfigSection::Notifications,
label: "Notifications",
description: "Let your terminal notify you when a turn finishes or approval is needed.",
description: "Let your terminal notify you when a turn finishes or approval is needed",
keywords: "alerts bell completion approval",
value: if status.custom_notification_filters {
ConfigSettingValue::Unavailable {
reason: "Event-specific notifications are configured. Edit them in config.toml.",
reason: "Event-specific notifications are configured in config.toml",
}
} else {
ConfigSettingValue::Toggle {
@@ -359,34 +338,34 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::Notifications,
label: "Notify when",
description: "Only notify while you are away, or allow notifications while this terminal is focused too.",
description: "Notify only while you are away or while this terminal is focused too",
keywords: "focus alerts background always unfocused",
value: if notifications_enabled {
ConfigEditor::NotificationCondition.value(tui, status)
} else {
ConfigSettingValue::Unavailable {
reason: "Turn on notifications to choose when they appear.",
reason: "Turn on notifications to choose when they appear",
}
},
},
ConfigSetting {
section: ConfigSection::Notifications,
label: "Test notification",
description: "Sends in 5 seconds. Switch apps: your terminal may hide notifications while focused.",
description: "Send in 5 seconds so you can switch apps before the notification appears",
keywords: "alerts bell sound test",
value: ConfigSettingValue::NotificationTest,
},
ConfigSetting {
section: ConfigSection::Input,
label: "Keyboard shortcuts",
description: "Browse actions and remap their keys.",
description: "Browse actions and remap their keys",
keywords: "keymap bindings hotkeys",
value: ConfigEditor::Keyboard.value(tui, status),
},
ConfigSetting {
section: ConfigSection::Input,
label: "Start in Vim mode",
description: "Applies to new composers. Use /vim to switch the current composer.",
description: "Start new composers in Vim mode and use /vim for the current composer",
keywords: "editor modal normal",
value: ConfigSettingValue::Toggle {
setting: ConfigToggle::StartInVimMode,
@@ -396,7 +375,7 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::Input,
label: "Paste detection",
description: "Recognize pasted-text bursts and keep multiline input together. Restart Codex to apply.",
description: "Recognize pasted-text bursts and keep multiline input together",
keywords: "burst clipboard multiline",
value: ConfigSettingValue::Toggle {
setting: ConfigToggle::PasteDetection,
@@ -406,7 +385,7 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::Sessions,
label: "Automatic recaps",
description: "Prepare recaps while the terminal is unfocused. Restart Codex to apply.",
description: "Prepare recaps while the terminal is unfocused",
keywords: "summary background",
value: ConfigSettingValue::Toggle {
setting: ConfigToggle::AutomaticRecaps,
@@ -416,34 +395,34 @@ fn settings(tui: &Tui, status: ConfigPanelStatus) -> Vec<ConfigSetting> {
ConfigSetting {
section: ConfigSection::Sessions,
label: "Session picker layout",
description: "Choose tighter rows or a roomier list when resuming and forking conversations.",
description: "Choose tighter rows or a roomier list when resuming and forking conversations",
keywords: "dense comfortable spacing resume fork",
value: ConfigEditor::SessionPickerLayout.value(tui, status),
},
ConfigSetting {
section: ConfigSection::Sessions,
label: "Resume directory",
description: "Choose which working directory to use when the current directory and saved session differ.",
description: "Choose which working directory to use when the current directory and saved session differ",
keywords: "cwd folder path fork ask",
value: ConfigEditor::ResumeDirectory.value(tui, status),
},
ConfigSetting {
section: ConfigSection::TextSelection,
label: "Copy on select",
description: "Copy transcript selections on release. Auto uses terminal defaults.",
description: "Copy transcript selections on release, with Auto following terminal defaults",
keywords: "clipboard mouse selection automatic drag",
value: ConfigEditor::CopyOnSelect.value(tui, status),
},
ConfigSetting {
section: ConfigSection::TextSelection,
label: "Deselect after copy",
description: "Remove the selection highlight after Copy on select succeeds.",
description: "Remove the selection highlight after Copy on select succeeds",
keywords: "clipboard deselect clear highlight automatic mouse selection",
value: ConfigSettingValue::DependentToggle {
setting: ConfigToggle::CopyOnSelectClearSelection,
enabled: ConfigToggle::CopyOnSelectClearSelection.enabled(tui),
dependency_met: status.copy_on_select,
unavailable_reason: "Turn on Copy on select to deselect after copying.",
unavailable_reason: "Turn on Copy on select to deselect after copying",
},
},
ConfigSetting {
+158 -61
View File
@@ -5,6 +5,7 @@ use super::ConfigPreference;
use super::ConfigSection;
use super::ConfigSetting;
use super::ConfigSettingValue;
use super::ConfigToggle;
use super::PickerState;
use super::ResumeCurrentAvailability;
use crate::app_event::AppEvent;
@@ -12,7 +13,9 @@ use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::ListSelectionView;
use crate::bottom_pane::PickerSurface;
use crate::bottom_pane::SELECTION_TOGGLE_UNAVAILABLE_PREFIX;
use crate::bottom_pane::SearchMode;
use crate::bottom_pane::SelectionDescriptionLayout;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionRowDisplay;
use crate::bottom_pane::SelectionTab;
@@ -31,8 +34,72 @@ use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use unicode_width::UnicodeWidthStr;
pub(crate) const EFFECTS_VIEW_ID: &str = "config-effects";
pub(crate) fn effects(tui: &codex_config::types::Tui, keymap: &ListKeymap) -> SelectionViewParams {
let footer_hint = [(ListAction::Accept, "toggle"), (ListAction::Cancel, "back")]
.into_iter()
.filter_map(|(action, label)| {
keymap
.primary_hint(action)
.map(|hint| (hint.display_label(), label.into()))
})
.collect::<Vec<_>>();
let settings = [
(
"Animations",
"Animate the welcome screen, working indicator, and transitions",
ConfigToggle::Animations,
),
(
"Composer stars",
"Show stars briefly for new Astra tasks",
ConfigToggle::ComposerStars,
),
];
SelectionViewParams {
view_id: Some(EFFECTS_VIEW_ID),
title: Some("Effects".to_string()),
subtitle: Some("Configure visual effects".to_string()),
footer_hint: Some(footer_hint_items_line(&footer_hint)),
allow_input_when_disconnected: true,
items: settings
.into_iter()
.map(|(label, description, setting)| {
let enabled = setting.enabled(tui);
let next_enabled = AtomicBool::new(!enabled);
let available = setting != ConfigToggle::ComposerStars || tui.animations;
SelectionItem {
name: label.to_string(),
toggle_placeholder: Some(if !available {
SELECTION_TOGGLE_UNAVAILABLE_PREFIX
} else if enabled {
"[x] "
} else {
"[ ] "
}),
description: available.then(|| description.to_string()),
disabled_reason: (!available)
.then(|| "Turn on Animations to show Astra task stars".to_string()),
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::SaveConfigPreference(ConfigPreference::Toggle {
setting,
enabled: next_enabled.fetch_xor(/*val*/ true, Ordering::Relaxed),
}));
})],
dismiss_on_select: false,
..Default::default()
}
})
.collect(),
..SelectionViewParams::picker()
}
}
pub(crate) fn notification_condition(
tui: &codex_config::types::Tui,
keymap: &ListKeymap,
@@ -47,8 +114,7 @@ pub(crate) fn notification_condition(
SelectionViewParams {
title: Some("Notify when".to_string()),
subtitle: Some(
"Only notify while you are away, or allow notifications while this terminal is focused too."
.to_string(),
"Notify only while you are away or while this terminal is focused too".to_string(),
),
footer_hint: Some(picker_hint_line_for_keymap(keymap)),
initial_selected_idx: conditions
@@ -88,7 +154,8 @@ pub(crate) fn copy_on_select(
SelectionViewParams {
title: Some("Copy on select".to_string()),
subtitle: Some(
"Copy transcript selections on release. Auto uses terminal defaults.".to_string(),
"Copy transcript selections on release, with Auto following terminal defaults"
.to_string(),
),
footer_hint: Some(picker_hint_line_for_keymap(keymap)),
initial_selected_idx: modes.iter().position(|(_, mode)| *mode == current),
@@ -161,7 +228,7 @@ pub(crate) fn session_picker_layout(
SelectionViewParams {
title: Some("Session picker layout".to_string()),
subtitle: Some(
"Choose tighter rows or a roomier list when resuming and forking conversations."
"Choose tighter rows or a roomier list when resuming and forking conversations"
.to_string(),
),
footer_hint: Some(picker_hint_line_for_keymap(keymap)),
@@ -201,7 +268,7 @@ pub(crate) fn resume_directory(
SelectionViewParams {
title: Some("Resume directory".to_string()),
subtitle: Some(
"Choose which working directory to use when the current directory and saved session differ. A directory passed with --cd takes precedence for that Codex session."
"Choose the working directory when resuming, with --cd taking precedence for this session"
.to_string(),
),
footer_hint: Some(picker_hint_line_for_keymap(keymap)),
@@ -218,7 +285,7 @@ pub(crate) fn resume_directory(
ResumeCurrentAvailability::RequiresCwdOverride
)
{
Some("Requires --cd for remote sessions.".to_string())
Some("Requires --cd for remote sessions".to_string())
} else {
None
},
@@ -251,15 +318,19 @@ impl Renderable for ConfigPanelHeader {
}
}
fn header(summary: String) -> Box<dyn Renderable> {
fn header(notice: Option<String>) -> Box<dyn Renderable> {
let subtitle = notice.unwrap_or_else(|| "Changes save automatically".to_string());
Box::new(ConfigPanelHeader(vec![
Line::from("Configuration".bold()),
Line::from("Changes save automatically; some apply after restarting Codex.".dim()),
Line::from(summary.dim()),
Line::from(subtitle.dim()),
]))
}
fn hint(keymap: &ListKeymap, search: Option<KeyBinding>) -> Line<'static> {
fn hint(
keymap: &ListKeymap,
search: Option<KeyBinding>,
accept_label: Option<&str>,
) -> Line<'static> {
let key_label = |action| {
keymap.primary_hint(action).map(|hint| {
hint.display_label()
@@ -280,28 +351,80 @@ fn hint(keymap: &ListKeymap, search: Option<KeyBinding>) -> Line<'static> {
items.push((search.display_label(), "search".into()));
}
items.extend(
[
(ListAction::Accept, "edit setting"),
(ListAction::Cancel, "close"),
]
.into_iter()
.filter_map(|(action, label)| key_label(action).map(|key| (key, label.into()))),
accept_label
.map(|label| (ListAction::Accept, label))
.into_iter()
.chain([(ListAction::Cancel, "clear search/close")])
.filter_map(|(action, label)| key_label(action).map(|key| (key, label.into()))),
);
footer_hint_items_line(&items)
}
#[derive(Clone, Copy)]
enum SettingItemContext {
Group,
Search,
}
fn setting_item(
setting: &ConfigSetting,
keymap: &ListKeymap,
search_hint: Option<KeyBinding>,
context: SettingItemContext,
) -> SelectionItem {
let (unavailable, accept_label) = match &setting.value {
ConfigSettingValue::DependentToggle {
dependency_met: false,
unavailable_reason,
..
} => (Some(*unavailable_reason), None),
ConfigSettingValue::Unavailable { reason } => (Some(*reason), None),
ConfigSettingValue::Toggle { .. } | ConfigSettingValue::DependentToggle { .. } => {
(None, Some("toggle"))
}
ConfigSettingValue::Editor { .. } => (None, Some("open")),
ConfigSettingValue::NotificationTest => (None, Some("test")),
};
let value_label = setting.value.label();
let description = unavailable.unwrap_or(setting.description);
let search_footer_hint = footer_hint_items_line(
&accept_label
.map(|label| ("enter".to_string(), label.to_string()))
.into_iter()
.chain([("esc".to_string(), "clear search".to_string())])
.collect::<Vec<_>>(),
);
let description = match context {
SettingItemContext::Group => description.to_string(),
SettingItemContext::Search => {
format!("{} · {description}", setting.section.title())
}
};
SelectionItem {
name: setting.label.to_string(),
category_tag: Some(format!("{value_label:<20}")),
description: Some(description),
is_disabled: unavailable.is_some(),
footer_hint: Some(hint(keymap, search_hint, accept_label)),
search_footer_hint: Some(search_footer_hint),
search_value: Some(format!(
"{} {} {} {} {}",
setting.section.title(),
setting.label,
value_label,
setting.keywords,
setting.description
)),
..Default::default()
}
}
pub(super) fn picker(
settings: &[ConfigSetting],
app_event_tx: AppEventSender,
keymap: ListKeymap,
state: PickerState,
) -> ListSelectionView {
let summary = |count| {
state
.notice
.clone()
.unwrap_or_else(|| format!("Settings in this group: {count}."))
};
let search = crate::key_hint::plain(KeyCode::Char('/'));
let (search_code, search_modifiers) = search.parts();
let search_event = KeyEvent::new(search_code, search_modifiers);
@@ -323,10 +446,17 @@ pub(super) fn picker(
reserve_result_rows: true,
row_display: SelectionRowDisplay::SingleLine,
col_width_mode: ColumnWidthMode::AutoAllRows,
footer_hint: Some(hint(&keymap, search_hint)),
description_layout: SelectionDescriptionLayout::SelectedBelowWhenNarrow {
min_description_width: 45,
},
footer_hint: Some(hint(&keymap, search_hint, /*accept_label*/ None)),
is_searchable: true,
search_mode: SearchMode::OnDemand(search),
search_placeholder: Some("Type to search settings".to_string()),
search_items: settings
.iter()
.map(|setting| setting_item(setting, &keymap, search_hint, SettingItemContext::Search))
.collect(),
name_column_width: settings
.iter()
.map(|setting| UnicodeWidthStr::width(setting.label))
@@ -334,49 +464,16 @@ pub(super) fn picker(
tabs: ConfigSection::ALL
.iter()
.map(|section| {
let group = settings
.iter()
.filter(|setting| setting.section == *section)
.collect::<Vec<_>>();
let title = section.title();
SelectionTab {
id: title.to_string(),
label: title.to_string(),
header: header(summary(group.len())),
items: group
.into_iter()
header: header(state.notice.clone()),
items: settings
.iter()
.filter(|setting| setting.section == *section)
.map(|setting| {
let unavailable = match &setting.value {
ConfigSettingValue::DependentToggle {
dependency_met: false,
unavailable_reason,
..
} => Some(*unavailable_reason),
ConfigSettingValue::Unavailable { reason } => Some(*reason),
ConfigSettingValue::Toggle { .. }
| ConfigSettingValue::DependentToggle { .. }
| ConfigSettingValue::Editor { .. }
| ConfigSettingValue::NotificationTest => None,
};
let value_label = setting.value.label();
SelectionItem {
name: setting.display_label(),
description: Some(format!(
"{:<19} {}",
value_label,
unavailable.unwrap_or(setting.description)
)),
is_disabled: unavailable.is_some(),
search_value: Some(format!(
"{} {} {} {} {}",
section.title(),
setting.label,
value_label,
setting.keywords,
setting.description
)),
..Default::default()
}
setting_item(setting, &keymap, search_hint, SettingItemContext::Group)
})
.collect(),
}
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 61
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 7.
Changes save automatically
Appearance Notifications Input Sessions Text selection
› Theme Automatic › Choose a syntax color palette with a live preview.
Animations [ ] Off Animate the welcome screen, working indicator, and transitions. Restart Codex to apply.
Composer stars Unavailable Turn on animations, then restart Codex to enable composer stars.
Tips [ ] Off Show helpful shortcuts and suggestions. Restart Codex to apply.
Status line colors [ ] Off Use theme colors for the model, branch, and other status line fields.
Status line fields Configure › Choose and reorder the information below the composer.
Terminal title fields Configure › Choose and reorder the information in your terminal tab or window title.
› Theme Automatic › Choose a syntax color palette with a live preview
Effects Configure › Configure animations and composer effects
Tips [ ] Off Show helpful shortcuts and suggestions
Status line colors [ ] Off Use theme colors for the model, branch, and other status line details
Status line display Configure › Choose and reorder the information below the composer
Terminal title display Configure › Choose and reorder the information in your terminal tab or window title
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter open · esc clear search/close
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 61
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
› Keyboard shortcuts Configure › Browse actions and remap their keys.
Start in Vim mode [ ] Off Applies to new composers. Use /vim to switch the current composer.
Paste detection [x] On Recognize pasted-text bursts and keep multiline input together. Restart Codex to apply.
› Keyboard shortcuts Configure › Browse actions and remap their keys
Start in Vim mode [ ] Off Start new composers in Vim mode and use /vim for the current composer
Paste detection [x] On Recognize pasted-text bursts and keep multiline input together
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter open · esc clear search/close
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 73
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
› Notifications [x] On Let your terminal notify you when a turn finishes or approval is needed.
Notify when Unfocused › Only notify while you are away, or allow notifications while this terminal is focused too.
Test notification Send in 5s › Sends in 5 seconds. Switch apps: your terminal may hide notifications while focused.
› Notifications [x] On Let your terminal notify you when a turn finishes or approval is needed
Notify when Unfocused › Notify only while you are away or while this terminal is focused too
Test notification Send in 5s Send in 5 seconds so you can switch apps before the notification appears
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter toggle · esc clear search/close
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 61
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
› Automatic recaps [ ] Off Prepare recaps while the terminal is unfocused. Restart Codex to apply.
Session picker layout Dense › Choose tighter rows or a roomier list when resuming and forking conversations.
Resume directory Ask › Choose which working directory to use when the current directory and saved session differ.
› Automatic recaps [ ] Off Prepare recaps while the terminal is unfocused
Session picker layout Dense › Choose tighter rows or a roomier list when resuming and forking conversations
Resume directory Ask › Choose which working directory to use when the current directory and saved session differ
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter toggle · esc clear search/close
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 71
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
› Copy on select Auto · On › Copy transcript selections on release. Auto uses terminal defaults.
Deselect after copy [ ] Off Remove the selection highlight after Copy on select succeeds.
Right-click paste Auto · Off › Paste when nothing is selected. Auto follows the platform default; On also enables macOS.
› Copy on select Auto · On › Copy transcript selections on release, with Auto following terminal defaults
Deselect after copy [ ] Off Remove the selection highlight after Copy on select succeeds
Right-click paste Auto · Off › Paste when nothing is selected, with Auto following platform defaults and On also enabling mac…
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter open · esc clear search/close
@@ -2,8 +2,9 @@
source: tui/src/config_panel/config_panel_tests.rs
expression: "render(&picker, 100)"
---
Copy on select
Copy transcript selections on release. Auto uses terminal defaults.
Copy transcript selections on release, with Auto following terminal defaults
› 1. Auto (default) (current)
@@ -0,0 +1,21 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 206
expression: "render(&panel, 100)"
---
Configuration
Changes save automatically
Appearance Notifications Input Sessions Text selection
Search › notify when
› Notify when Unfocused › Notifications · Notify only while you are away or whil…
enter open · esc clear search
@@ -2,8 +2,9 @@
source: tui/src/config_panel/config_panel_tests.rs
expression: "render(&picker, 100)"
---
Notify when
Only notify while you are away, or allow notifications while this terminal is focused too.
Notify only while you are away or while this terminal is focused too
› 1. Unfocused (current)
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 140
expression: "render(&panel, 100)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
Notifications Unavailable Event-specific notifications are configured. Edit them …
› Notify when Unfocused › Only notify while you are away, or allow notifications …
Test notification Send in 5s › Sends in 5 seconds. Switch apps: your terminal may hide…
Notifications Unavailable Event-specific notifications are configured in config.…
› Notify when Unfocused › Notify only while you are away or while this terminal …
Test notification Send in 5s Send in 5 seconds so you can switch apps before the no…
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter open · esc clear search/close
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 159
expression: "render(&panel, 100)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
› Notifications [ ] Off Let your terminal notify you when a turn finishes or ap…
Notify when Unavailable Turn on notifications to choose when they appear.
Test notification Send in 5s › Sends in 5 seconds. Switch apps: your terminal may hide…
Notifications [ ] Off Let your terminal notify you when a turn finishes or a…
Notify when Unavailable Turn on notifications to choose when they appear
› Test notification Send in 5s Send in 5 seconds so you can switch apps before the no…
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter test · esc clear search/close
@@ -1,24 +1,23 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 227
expression: "render(&panel, 50)"
---
Configuration
Changes save automatically; some apply after
restarting Codex.
Failed to save status line settings:
permission denied while writing config.toml
Appearance Notifications Input ›
› Theme Automatic › Cho…
Animations [ ] Off Ani…
Composer stars Unavailable Tur…
Tips [ ] Off Sho…
Status line colors [ ] Off Use…
Status line fields Configure › Cho…
Terminal title fields Configure › Cho…
› Theme Automatic ›
Effects Configure ›
Tips [ ] Off
Status line colors [ ] Off
Status line display Configure ›
Terminal title display Configure ›
left/right group · / search · enter edit setting
· esc close
Choose a syntax color palette with a live
preview
left/right group · / search · enter open · esc
clear search/close
@@ -4,12 +4,11 @@ expression: "render(&picker, 100)"
---
Resume directory
Choose which working directory to use when the current directory and saved session differ. A
directory passed with --cd takes precedence for that Codex session.
Choose the working directory when resuming, with --cd taking precedence for this session
› 1. Ask (current)
Current directory (disabled) Requires --cd for remote sessions.
Current directory (disabled) Requires --cd for remote sessions
2. Session directory
enter select · esc back
@@ -2,8 +2,9 @@
source: tui/src/config_panel/config_panel_tests.rs
expression: "render(&picker, 100)"
---
Right-click paste
Paste when nothing is selected. Auto follows the platform default; On also enables macOS.
Paste when nothing is selected, with Auto following platform defaults and On also enabling macOS
› 1. Auto (default) (current)
@@ -1,21 +1,18 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 290
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
Copy on select Auto · On › Copy transcript selections on release. Auto uses terminal defaults.
Deselect after copy [ ] Off Remove the selection highlight after Copy on select succeeds.
› Right-click paste On · Unavailable › Unavailable in inline mode. Use your terminal's paste command.
Copy on select Auto · On › Copy transcript selections on release, with Auto following terminal defaults
Deselect after copy [ ] Off Remove the selection highlight after Copy on select succeeds
› Right-click paste On · Unavailable › Unavailable in inline mode; use your terminal's paste command
left/right group · / search · enter edit setting · esc close
left/right group · / search · enter open · esc clear search/close
@@ -2,8 +2,9 @@
source: tui/src/config_panel/config_panel_tests.rs
expression: "render(&picker, 100)"
---
Session picker layout
Choose tighter rows or a roomier list when resuming and forking conversations.
Choose tighter rows or a roomier list when resuming and forking conversations
› 1. Dense (current)
@@ -1,23 +1,24 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 96
expression: "render(&panel, 140)"
assertion_line: 128
expression: "render(&panel, 50)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Settings in this group: 3.
Changes save automatically
Appearance Notifications Input Sessions Text selection
‹ Input Sessions Text selection
Search › deselect after copy
› Deselect after copy Unavailable Turn on Copy on select to deselect after copying.
› Deselect after copy Unavailable
Text selection · Turn on Copy on select to
deselect after copying
left/right group · / search · enter edit setting · esc close
esc clear search
@@ -0,0 +1,14 @@
---
source: tui/src/config_panel/config_panel_tests.rs
expression: "render(&view, 80)"
---
Effects
Configure visual effects
› 1. [ ] Animations Animate the welcome screen, working
indicator, and transitions
[-] Composer stars (disabled) Turn on Animations to show Astra task stars
enter toggle · esc back
@@ -1,23 +1,21 @@
---
source: tui/src/config_panel/config_panel_tests.rs
assertion_line: 212
assertion_line: 477
expression: "render(&panel, 140)"
---
Configuration
Changes save automatically; some apply after restarting Codex.
Saved · restart Codex to apply
Test sent · check terminal/system settings
Appearance Notifications Input Sessions Text selection
Search › animations
Search › tips
› Animations [x] On Animate the welcome screen, working indicator, and transitions. Restart Codex to apply.
Composer stars [x] On Animate stars in the composer. Requires animations and a restart.
› Tips [x] On Appearance · Show helpful shortcuts and suggestions
/ search · enter edit setting · esc close
enter toggle · esc clear search
-18
View File
@@ -89,24 +89,6 @@ impl ConfigPreference {
}
impl ConfigToggle {
const RESTART_REQUIRED: [Self; 5] = [
Self::Animations,
Self::AutomaticRecaps,
Self::ComposerStars,
Self::PasteDetection,
Self::Tips,
];
pub(crate) fn requires_restart(self) -> bool {
Self::RESTART_REQUIRED.contains(&self)
}
pub(crate) fn preserve_restart_required(target: &mut Tui, source: &Tui) {
for setting in Self::RESTART_REQUIRED {
setting.adopt(target, source);
}
}
pub(crate) fn edit(self, enabled: bool) -> ConfigEdit {
let (key, value) = match self {
Self::ComposerStars => {
+7 -2
View File
@@ -7,7 +7,6 @@
//! Effective animations also respect the TUI host's launch-time accessibility preference.
//! The selected transcript ownership and alternate-screen restrictions survive local reloads.
use crate::config_panel::ConfigToggle;
use crate::legacy_core::config::Config;
use crate::legacy_core::config::ConfigTomlLoadResult;
use crate::legacy_core::config::TerminalResizeReflowConfig;
@@ -208,7 +207,13 @@ impl LocalSettings {
let mut settings = Self::from(config);
settings.transcript_mode = self.transcript_mode;
settings.tui.alternate_screen = self.tui.alternate_screen;
ConfigToggle::preserve_restart_required(&mut settings.tui, &self.tui);
// Generic reloads do not update these values in the live UI. The config panel updates
// both copies when the user changes one.
settings.tui.animations = self.tui.animations;
settings.tui.auto_recap = self.tui.auto_recap;
settings.tui.effects.starfield = self.tui.effects.starfield;
settings.tui.disable_paste_burst = self.tui.disable_paste_burst;
settings.tui.show_tooltips = self.tui.show_tooltips;
settings
}
+4
View File
@@ -180,6 +180,10 @@ impl AmbientPet {
self.animation_started_at = Instant::now();
}
pub(crate) fn set_animations_enabled(&mut self, enabled: bool) {
self.animations_enabled = enabled;
}
pub(crate) fn image_enabled(&self) -> bool {
self.support.protocol().is_some()
}
@@ -115,6 +115,11 @@ impl StatusIndicatorWidget {
self.app_event_tx.interrupt();
}
pub(crate) fn set_motion_preferences(&mut self, tui: &codex_config::types::Tui) {
self.animations_enabled = tui.animations;
self.effects = tui.effects;
}
/// Update the animated header label (left of the brackets).
pub(crate) fn update_header(&mut self, header: String) {
if self.header != header {