From c543709d5fcef9ab02b838696cf07296e4b640a3 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 9 Oct 2025 09:59:08 -0500 Subject: [PATCH] settings_ui: Add terminal settings (#39874) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/settings/default.json | 4 +- crates/project/src/terminals.rs | 4 +- .../settings/src/settings_content/terminal.rs | 43 +- crates/settings_ui/src/page_data.rs | 513 ++++++++++++++++++ crates/settings_ui/src/settings_ui.rs | 26 +- crates/terminal/src/terminal_settings.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 6 +- 7 files changed, 582 insertions(+), 18 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 14f649cace93b58c7dbffea1b1c79fecedc3a7cb..4b1a3e7a989f470962033c3721a06427814ecdbf 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1404,8 +1404,8 @@ // 4. A box drawn around the following character // "hollow" // - // Default: not set, defaults to "block" - "cursor_shape": null, + // Default: "block" + "cursor_shape": "block", // Set whether Alternate Scroll mode (code: ?1007) is active by default. // Alternate Scroll mode converts mouse scroll events into up / down key // presses when in the alternate screen (e.g. when running applications diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index db1b8197cfd9849c8eef0575fc08aedcce76547a..dc4224a7ff6b867ecdc959b2e4be1030cfc24aba 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -244,7 +244,7 @@ impl Project { task_state, shell, env, - settings.cursor_shape.unwrap_or_default(), + settings.cursor_shape, settings.alternate_scroll, settings.max_scroll_history_lines, is_via_remote, @@ -374,7 +374,7 @@ impl Project { None, shell, env, - settings.cursor_shape.unwrap_or_default(), + settings.cursor_shape, settings.alternate_scroll, settings.max_scroll_history_lines, is_via_remote, diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index 4c8b299c314d0a5900034f5b8237562ee2e2b8d2..e5d3ba60b52073963115934afdd368c582ccfff2 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -65,7 +65,7 @@ pub struct TerminalSettingsContent { /// Default cursor shape for the terminal. /// Can be "bar", "block", "underline", or "hollow". /// - /// Default: None + /// Default: "block" pub cursor_shape: Option, /// Sets the cursor blinking behavior in the terminal. /// @@ -236,7 +236,18 @@ pub enum ShowScrollbar { } #[derive( - Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, + Clone, + Copy, + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, )] #[serde(rename_all = "snake_case")] // todo() -> combine with CursorShape @@ -252,7 +263,19 @@ pub enum CursorShapeContent { Hollow, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum TerminalBlink { /// Never blink the cursor, ignoring the terminal mode. @@ -264,7 +287,19 @@ pub enum TerminalBlink { On, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Clone, + Copy, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum AlternateScroll { On, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 431f49cf02f3564463fec3646996682eb17f8967..a447146ce13fcc37f7efd16bf54e3166d47247e8 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -3916,6 +3916,519 @@ pub(crate) fn settings_data() -> Vec { }), ], }, + SettingsPage { + title: "Terminal", + items: vec![ + SettingsPageItem::SectionHeader("Environment"), + SettingsPageItem::SettingItem(SettingItem { + title: "Shell", + description: "What shell to use when opening a terminal", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.project.shell + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .project + .shell + }, + } + .unimplemented(), + ), + metadata: None, + files: USER | LOCAL, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Working Directory", + description: "What working directory to use when launching the terminal", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.project.working_directory + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .project + .working_directory + }, + } + .unimplemented(), + ), + metadata: None, + files: USER | LOCAL, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Environment Variables", + description: "Key-value pairs to add to the terminal's environment", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.project.env + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .project + .env + }, + } + .unimplemented(), + ), + metadata: None, + files: USER | LOCAL, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Detect Virtual Environment", + description: "Activates the python virtual environment, if one is found, in the terminal's working directory", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.project.detect_venv + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .project + .detect_venv + }, + } + .unimplemented(), + ), + metadata: None, + files: USER | LOCAL, + }), + SettingsPageItem::SectionHeader("Font"), + SettingsPageItem::SettingItem(SettingItem { + title: "Font Size", + description: "Font size for terminal text. If not set, defaults to buffer font size", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.font_size + } else if settings_content.theme.buffer_font_size.is_some() { + &settings_content.theme.buffer_font_size + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content.terminal.get_or_insert_default().font_size + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Font Family", + description: "Font family for terminal text. If not set, defaults to buffer font family", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal + && terminal.font_family.is_some() + { + &terminal.font_family + } else if settings_content.theme.buffer_font_family.is_some() { + &settings_content.theme.buffer_font_family + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .font_family + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Font Fallbacks", + description: "Font fallbacks for terminal text. If not set, defaults to buffer font fallbacks", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.font_fallbacks + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .font_fallbacks + }, + } + .unimplemented(), + ), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Font Weight", + description: "Font weight for terminal text in CSS weight units (100-900)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.font_weight + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .font_weight + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Font Features", + description: "Font features for terminal text", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.font_features + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .font_features + }, + } + .unimplemented(), + ), + metadata: None, + files: USER, + }), + SettingsPageItem::SectionHeader("Display Settings"), + SettingsPageItem::SettingItem(SettingItem { + title: "Line Height", + description: "Line height for terminal text", + field: Box::new( + SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.line_height + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .line_height + }, + } + .unimplemented(), + ), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Shape", + description: "Default cursor shape for the terminal (bar, block, underline, or hollow)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.cursor_shape + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .cursor_shape + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Blinking", + description: "Sets the cursor blinking behavior in the terminal", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.blinking + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content.terminal.get_or_insert_default().blinking + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Alternate Scroll", + description: "Whether Alternate Scroll mode is active by default (converts mouse scroll to arrow keys in apps like vim)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.alternate_scroll + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .alternate_scroll + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Minimum Contrast", + description: "The minimum APCA perceptual contrast between foreground and background colors (0-106)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.minimum_contrast + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .minimum_contrast + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SectionHeader("Behavior Settings"), + SettingsPageItem::SettingItem(SettingItem { + title: "Option As Meta", + description: "Whether the option key behaves as the meta key", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.option_as_meta + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .option_as_meta + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Copy On Select", + description: "Whether selecting text in the terminal automatically copies to the system clipboard", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.copy_on_select + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .copy_on_select + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Keep Selection On Copy", + description: "Whether to keep the text selection after copying it to the clipboard", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.keep_selection_on_copy + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .keep_selection_on_copy + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SectionHeader("Layout Settings"), + SettingsPageItem::SettingItem(SettingItem { + title: "Default Width", + description: "Default width when the terminal is docked to the left or right (in pixels)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.default_width + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .default_width + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Default Height", + description: "Default height when the terminal is docked to the bottom (in pixels)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.default_height + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .default_height + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SectionHeader("Advanced Settings"), + SettingsPageItem::SettingItem(SettingItem { + title: "Max Scroll History Lines", + description: "Maximum number of lines to keep in scrollback history (max: 100,000; 0 disables scrolling)", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + &terminal.max_scroll_history_lines + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .max_scroll_history_lines + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SectionHeader("Toolbar"), + SettingsPageItem::SettingItem(SettingItem { + title: "Breadcrumbs", + description: "Whether to display the terminal title in breadcrumbs inside the terminal pane", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal { + if let Some(toolbar) = &terminal.toolbar { + &toolbar.breadcrumbs + } else { + &None + } + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .toolbar + .get_or_insert_default() + .breadcrumbs + }, + }), + metadata: None, + files: USER, + }), + SettingsPageItem::SectionHeader("Scrollbar"), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Scrollbar", + description: "When to show the scrollbar in the terminal", + field: Box::new(SettingField { + pick: |settings_content| { + if let Some(terminal) = &settings_content.terminal + && let Some(scrollbar) = &terminal.scrollbar + && scrollbar.show.is_some() + { + &scrollbar.show + } else if let Some(scrollbar) = &settings_content.editor.scrollbar { + &scrollbar.show + } else { + &None + } + }, + pick_mut: |settings_content| { + &mut settings_content + .terminal + .get_or_insert_default() + .scrollbar + .get_or_insert_default() + .show + }, + }), + metadata: None, + files: USER, + }), + ], + }, ] } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 75e3951cfd49b53896b57771dd36763ac9a2c1dc..4d161a68075ad2665765d0523e92d94bd815d535 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -200,10 +200,19 @@ impl SettingFieldRenderer { if let Some(renderer) = self.renderers.borrow().get(&key) { renderer(any_setting_field, settings_file, metadata, window, cx) } else { - panic!( - "No renderer found for type: {}", - any_setting_field.type_name() - ) + Button::new("no-renderer", "NO RENDERER") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .icon(Some(IconName::XCircle)) + .icon_position(IconPosition::Start) + .icon_color(Color::Error) + .tab_index(0_isize) + .tooltip(Tooltip::text(any_setting_field.type_name())) + .into_any_element() + // panic!( + // "No renderer found for type: {}", + // any_setting_field.type_name() + // ) } } } @@ -409,6 +418,15 @@ fn init_renderers(cx: &mut App) { .add_renderer::(|settings_field, file, _, window, cx| { render_dropdown(*settings_field, file, window, cx) }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) .add_renderer::(|settings_field, file, _, window, cx| { render_number_field(*settings_field, file, window, cx) }) diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index cc5dbc13ad80628bca355d3e85f860ef45177ed3..27cccea126fecd7d015b21cec6d18809b756bdf8 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -31,7 +31,7 @@ pub struct TerminalSettings { pub font_weight: Option, pub line_height: TerminalLineHeight, pub env: HashMap, - pub cursor_shape: Option, + pub cursor_shape: CursorShape, pub blinking: TerminalBlink, pub alternate_scroll: AlternateScroll, pub option_as_meta: bool, @@ -95,7 +95,7 @@ impl settings::Settings for TerminalSettings { font_weight: user_content.font_weight.map(FontWeight), line_height: user_content.line_height.unwrap(), env: project_content.env.unwrap(), - cursor_shape: user_content.cursor_shape.map(Into::into), + cursor_shape: user_content.cursor_shape.unwrap().into(), blinking: user_content.blinking.unwrap(), alternate_scroll: user_content.alternate_scroll.unwrap(), option_as_meta: user_content.option_as_meta.unwrap(), diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cbb3ad92c58ae980698f135b7fa10bb9f68a4dd1..0f4f745b877bd6871fadd78c2c6136a268e51ded 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -234,9 +234,7 @@ impl TerminalView { terminal_view.focus_out(window, cx); }, ); - let cursor_shape = TerminalSettings::get_global(cx) - .cursor_shape - .unwrap_or_default(); + let cursor_shape = TerminalSettings::get_global(cx).cursor_shape; let scroll_handle = TerminalScrollHandle::new(terminal.read(cx)); @@ -427,7 +425,7 @@ impl TerminalView { let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs; self.show_breadcrumbs = settings.toolbar.breadcrumbs; - let new_cursor_shape = settings.cursor_shape.unwrap_or_default(); + let new_cursor_shape = settings.cursor_shape; let old_cursor_shape = self.cursor_shape; if old_cursor_shape != new_cursor_shape { self.cursor_shape = new_cursor_shape;