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, 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}