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