terminal.rs

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