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