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