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