1use std::path::PathBuf;
2
3use collections::HashMap;
4use gpui::{AbsoluteLength, FontFeatures, SharedString, px};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use settings_macros::{MergeFrom, with_fallible_options};
8
9use crate::FontFamilyName;
10
11#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
12pub struct ProjectTerminalSettingsContent {
13 /// What shell to use when opening a terminal.
14 ///
15 /// Default: system
16 pub shell: Option<Shell>,
17 /// What working directory to use when launching the terminal
18 ///
19 /// Default: current_project_directory
20 pub working_directory: Option<WorkingDirectory>,
21 /// Any key-value pairs added to this list will be added to the terminal's
22 /// environment. Use `:` to separate multiple values.
23 ///
24 /// Default: {}
25 pub env: Option<HashMap<String, String>>,
26 /// Activates the python virtual environment, if one is found, in the
27 /// terminal's working directory (as resolved by the working_directory
28 /// setting). Set this to "off" to disable this behavior.
29 ///
30 /// Default: on
31 pub detect_venv: Option<VenvSettings>,
32}
33
34#[with_fallible_options]
35#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
36pub struct TerminalSettingsContent {
37 #[serde(flatten)]
38 pub project: ProjectTerminalSettingsContent,
39 /// Sets the terminal's font size.
40 ///
41 /// If this option is not included,
42 /// the terminal will default to matching the buffer's font size.
43 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
44 pub font_size: Option<f32>,
45 /// Sets the terminal's font family.
46 ///
47 /// If this option is not included,
48 /// the terminal will default to matching the buffer's font family.
49 pub font_family: Option<FontFamilyName>,
50
51 /// Sets the terminal's font fallbacks.
52 ///
53 /// If this option is not included,
54 /// the terminal will default to matching the buffer's font fallbacks.
55 #[schemars(extend("uniqueItems" = true))]
56 pub font_fallbacks: Option<Vec<FontFamilyName>>,
57
58 /// Sets the terminal's line height.
59 ///
60 /// Default: comfortable
61 pub line_height: Option<TerminalLineHeight>,
62 pub font_features: Option<FontFeatures>,
63 /// Sets the terminal's font weight in CSS weight units 0-900.
64 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
65 pub font_weight: Option<f32>,
66 /// Default cursor shape for the terminal.
67 /// Can be "bar", "block", "underline", or "hollow".
68 ///
69 /// Default: "block"
70 pub cursor_shape: Option<CursorShapeContent>,
71 /// Sets the cursor blinking behavior in the terminal.
72 ///
73 /// Default: terminal_controlled
74 pub blinking: Option<TerminalBlink>,
75 /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
76 /// Alternate Scroll mode converts mouse scroll events into up / down key
77 /// presses when in the alternate screen (e.g. when running applications
78 /// like vim or less). The terminal can still set and unset this mode.
79 ///
80 /// Default: on
81 pub alternate_scroll: Option<AlternateScroll>,
82 /// Sets whether the option key behaves as the meta key.
83 ///
84 /// Default: false
85 pub option_as_meta: Option<bool>,
86 /// Whether or not selecting text in the terminal will automatically
87 /// copy to the system clipboard.
88 ///
89 /// Default: false
90 pub copy_on_select: Option<bool>,
91 /// Whether to keep the text selection after copying it to the clipboard.
92 ///
93 /// Default: true
94 pub keep_selection_on_copy: Option<bool>,
95 /// Whether to show the terminal button in the status bar.
96 ///
97 /// Default: true
98 pub button: Option<bool>,
99 pub dock: Option<TerminalDockPosition>,
100 /// Default width when the terminal is docked to the left or right.
101 ///
102 /// Default: 640
103 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
104 pub default_width: Option<f32>,
105 /// Default height when the terminal is docked to the bottom.
106 ///
107 /// Default: 320
108 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
109 pub default_height: Option<f32>,
110 /// The maximum number of lines to keep in the scrollback history.
111 /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
112 /// 0 disables the scrolling.
113 /// Existing terminals will not pick up this change until they are recreated.
114 /// See <a href="https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213">Alacritty documentation</a> for more information.
115 ///
116 /// Default: 10_000
117 pub max_scroll_history_lines: Option<usize>,
118 /// The multiplier for scrolling with the mouse wheel.
119 ///
120 /// Default: 1.0
121 pub scroll_multiplier: Option<f32>,
122 /// Toolbar related settings
123 pub toolbar: Option<TerminalToolbarContent>,
124 /// Scrollbar-related settings
125 pub scrollbar: Option<ScrollbarSettingsContent>,
126 /// The minimum APCA perceptual contrast between foreground and background colors.
127 ///
128 /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
129 /// especially for dark mode. Values range from 0 to 106.
130 ///
131 /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
132 /// https://readtech.org/ARC/tests/bronze-simple-mode/
133 /// - 0: No contrast adjustment
134 /// - 45: Minimum for large fluent text (36px+)
135 /// - 60: Minimum for other content text
136 /// - 75: Minimum for body text
137 /// - 90: Preferred for body text
138 ///
139 /// Default: 45
140 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
141 pub minimum_contrast: Option<f32>,
142}
143
144/// Shell configuration to open the terminal with.
145#[derive(
146 Clone,
147 Debug,
148 Default,
149 Serialize,
150 Deserialize,
151 PartialEq,
152 Eq,
153 JsonSchema,
154 MergeFrom,
155 strum::EnumDiscriminants,
156)]
157#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
158#[serde(rename_all = "snake_case")]
159pub enum Shell {
160 /// Use the system's default terminal configuration in /etc/passwd
161 #[default]
162 System,
163 /// Use a specific program with no arguments.
164 Program(String),
165 /// Use a specific program with arguments.
166 WithArguments {
167 /// The program to run.
168 program: String,
169 /// The arguments to pass to the program.
170 args: Vec<String>,
171 /// An optional string to override the title of the terminal tab
172 title_override: Option<SharedString>,
173 },
174}
175
176#[derive(
177 Clone,
178 Debug,
179 Serialize,
180 Deserialize,
181 PartialEq,
182 Eq,
183 JsonSchema,
184 MergeFrom,
185 strum::EnumDiscriminants,
186)]
187#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
188#[serde(rename_all = "snake_case")]
189pub enum WorkingDirectory {
190 /// Use the current file's project directory. Will Fallback to the
191 /// first project directory strategy if unsuccessful.
192 CurrentProjectDirectory,
193 /// Use the first project in this workspace's directory.
194 FirstProjectDirectory,
195 /// Always use this platform's home directory (if it can be found).
196 AlwaysHome,
197 /// Always use a specific directory. This value will be shell expanded.
198 /// If this path is not a valid directory the terminal will default to
199 /// this platform's home directory (if it can be found).
200 Always { directory: String },
201}
202
203#[with_fallible_options]
204#[derive(
205 Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
206)]
207pub struct ScrollbarSettingsContent {
208 /// When to show the scrollbar in the terminal.
209 ///
210 /// Default: inherits editor scrollbar settings
211 pub show: Option<ShowScrollbar>,
212}
213
214#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
215#[serde(rename_all = "snake_case")]
216pub enum TerminalLineHeight {
217 /// Use a line height that's comfortable for reading, 1.618
218 #[default]
219 Comfortable,
220 /// Use a standard line height, 1.3. This option is useful for TUIs,
221 /// particularly if they use box characters
222 Standard,
223 /// Use a custom line height.
224 Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32),
225}
226
227impl TerminalLineHeight {
228 pub fn value(&self) -> AbsoluteLength {
229 let value = match self {
230 TerminalLineHeight::Comfortable => 1.618,
231 TerminalLineHeight::Standard => 1.3,
232 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
233 };
234 px(value).into()
235 }
236}
237
238/// When to show the scrollbar.
239///
240/// Default: auto
241#[derive(
242 Copy,
243 Clone,
244 Debug,
245 Default,
246 Serialize,
247 Deserialize,
248 JsonSchema,
249 MergeFrom,
250 PartialEq,
251 Eq,
252 strum::VariantArray,
253 strum::VariantNames,
254)]
255#[serde(rename_all = "snake_case")]
256pub enum ShowScrollbar {
257 /// Show the scrollbar if there's important information or
258 /// follow the system's configured behavior.
259 #[default]
260 Auto,
261 /// Match the system's configured behavior.
262 System,
263 /// Always show the scrollbar.
264 Always,
265 /// Never show the scrollbar.
266 Never,
267}
268
269#[derive(
270 Clone,
271 Copy,
272 Debug,
273 Default,
274 Serialize,
275 Deserialize,
276 PartialEq,
277 Eq,
278 JsonSchema,
279 MergeFrom,
280 strum::VariantArray,
281 strum::VariantNames,
282)]
283#[serde(rename_all = "snake_case")]
284// todo() -> combine with CursorShape
285pub enum CursorShapeContent {
286 /// Cursor is a block like `█`.
287 #[default]
288 Block,
289 /// Cursor is an underscore like `_`.
290 Underline,
291 /// Cursor is a vertical bar like `⎸`.
292 Bar,
293 /// Cursor is a hollow box like `▯`.
294 Hollow,
295}
296
297#[derive(
298 Copy,
299 Clone,
300 Debug,
301 Serialize,
302 Deserialize,
303 PartialEq,
304 Eq,
305 JsonSchema,
306 MergeFrom,
307 strum::VariantArray,
308 strum::VariantNames,
309)]
310#[serde(rename_all = "snake_case")]
311pub enum TerminalBlink {
312 /// Never blink the cursor, ignoring the terminal mode.
313 Off,
314 /// Default the cursor blink to off, but allow the terminal to
315 /// set blinking.
316 TerminalControlled,
317 /// Always blink the cursor, ignoring the terminal mode.
318 On,
319}
320
321#[derive(
322 Clone,
323 Copy,
324 Debug,
325 Serialize,
326 Deserialize,
327 PartialEq,
328 Eq,
329 JsonSchema,
330 MergeFrom,
331 strum::VariantArray,
332 strum::VariantNames,
333)]
334#[serde(rename_all = "snake_case")]
335pub enum AlternateScroll {
336 On,
337 Off,
338}
339
340// Toolbar related settings
341#[with_fallible_options]
342#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
343pub struct TerminalToolbarContent {
344 /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
345 /// Only shown if the terminal title is not empty.
346 ///
347 /// The shell running in the terminal needs to be configured to emit the title.
348 /// Example: `echo -e "\e]2;New Title\007";`
349 ///
350 /// Default: true
351 pub breadcrumbs: Option<bool>,
352}
353
354#[derive(
355 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
356)]
357#[serde(rename_all = "snake_case")]
358pub enum CondaManager {
359 /// Automatically detect the conda manager
360 #[default]
361 Auto,
362 /// Use conda
363 Conda,
364 /// Use mamba
365 Mamba,
366 /// Use micromamba
367 Micromamba,
368}
369
370#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
371#[serde(rename_all = "snake_case")]
372pub enum VenvSettings {
373 #[default]
374 Off,
375 On {
376 /// Default directories to search for virtual environments, relative
377 /// to the current working directory. We recommend overriding this
378 /// in your project's settings, rather than globally.
379 activate_script: Option<ActivateScript>,
380 venv_name: Option<String>,
381 directories: Option<Vec<PathBuf>>,
382 /// Preferred Conda manager to use when activating Conda environments.
383 ///
384 /// Default: auto
385 conda_manager: Option<CondaManager>,
386 },
387}
388#[with_fallible_options]
389pub struct VenvSettingsContent<'a> {
390 pub activate_script: ActivateScript,
391 pub venv_name: &'a str,
392 pub directories: &'a [PathBuf],
393 pub conda_manager: CondaManager,
394}
395
396impl VenvSettings {
397 pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
398 match self {
399 VenvSettings::Off => None,
400 VenvSettings::On {
401 activate_script,
402 venv_name,
403 directories,
404 conda_manager,
405 } => Some(VenvSettingsContent {
406 activate_script: activate_script.unwrap_or(ActivateScript::Default),
407 venv_name: venv_name.as_deref().unwrap_or(""),
408 directories: directories.as_deref().unwrap_or(&[]),
409 conda_manager: conda_manager.unwrap_or(CondaManager::Auto),
410 }),
411 }
412 }
413}
414
415#[derive(
416 Copy,
417 Clone,
418 Debug,
419 Serialize,
420 Deserialize,
421 JsonSchema,
422 MergeFrom,
423 PartialEq,
424 Eq,
425 strum::VariantArray,
426 strum::VariantNames,
427)]
428#[serde(rename_all = "snake_case")]
429pub enum TerminalDockPosition {
430 Left,
431 Bottom,
432 Right,
433}
434
435#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
436#[serde(rename_all = "snake_case")]
437pub enum ActivateScript {
438 #[default]
439 Default,
440 Csh,
441 Fish,
442 Nushell,
443 PowerShell,
444 Pyenv,
445}
446
447#[cfg(test)]
448mod test {
449 use serde_json::json;
450
451 use crate::{ProjectSettingsContent, Shell, UserSettingsContent};
452
453 #[test]
454 fn test_project_settings() {
455 let project_content =
456 json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
457
458 let user_content =
459 json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
460
461 let user_settings = serde_json::from_value::<UserSettingsContent>(user_content).unwrap();
462 let project_settings =
463 serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
464
465 assert_eq!(
466 user_settings.content.terminal.unwrap().project.shell,
467 Some(Shell::Program("/bin/user".to_owned()))
468 );
469 assert_eq!(user_settings.content.project.terminal, None);
470 assert_eq!(
471 project_settings.terminal.unwrap().shell,
472 Some(Shell::Program("/bin/project".to_owned()))
473 );
474 }
475}