terminal.rs

  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}