mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+13
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
+8
-11
@@ -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
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
+5
-8
@@ -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
-1
@@ -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)
|
||||
|
||||
+21
@@ -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
-1
@@ -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)
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
+11
-12
@@ -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
|
||||
|
||||
+2
-3
@@ -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
-1
@@ -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)
|
||||
|
||||
+5
-8
@@ -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
-1
@@ -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)
|
||||
|
||||
+8
-7
@@ -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
|
||||
|
||||
+14
@@ -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
|
||||
+5
-7
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user