settings_ui: Add terminal settings (#39874)

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle
2025-10-09 10:59:08 -04:00
committed by GitHub
parent c58931ac04
commit c543709d5f
7 changed files with 582 additions and 18 deletions
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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,
@@ -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<CursorShapeContent>,
/// 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,
+513
View File
@@ -3916,6 +3916,519 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
}),
],
},
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,
}),
],
},
]
}
+22 -4
View File
@@ -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::LspInsertMode>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::AlternateScroll>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::TerminalBlink>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::CursorShapeContent>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<f32>(|settings_field, file, _, window, cx| {
render_number_field(*settings_field, file, window, cx)
})
+2 -2
View File
@@ -31,7 +31,7 @@ pub struct TerminalSettings {
pub font_weight: Option<FontWeight>,
pub line_height: TerminalLineHeight,
pub env: HashMap<String, String>,
pub cursor_shape: Option<CursorShape>,
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(),
+2 -4
View File
@@ -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;