terminal.rs

  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}