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