terminal_settings.rs

  1use alacritty_terminal::vte::ansi::{
  2    CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
  3};
  4use collections::HashMap;
  5use gpui::{
  6    AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, SharedString, px,
  7};
  8use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema};
  9use serde_derive::{Deserialize, Serialize};
 10use settings::{SettingsJsonSchemaParams, SettingsSources, add_references_to_properties};
 11use std::path::PathBuf;
 12use task::Shell;
 13
 14#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 15#[serde(rename_all = "snake_case")]
 16pub enum TerminalDockPosition {
 17    Left,
 18    Bottom,
 19    Right,
 20}
 21
 22#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 23pub struct Toolbar {
 24    pub breadcrumbs: bool,
 25}
 26
 27#[derive(Clone, Debug, Deserialize)]
 28pub struct TerminalSettings {
 29    pub shell: Shell,
 30    pub working_directory: WorkingDirectory,
 31    pub font_size: Option<Pixels>,
 32    pub font_family: Option<SharedString>,
 33    pub font_fallbacks: Option<FontFallbacks>,
 34    pub font_features: Option<FontFeatures>,
 35    pub font_weight: Option<FontWeight>,
 36    pub line_height: TerminalLineHeight,
 37    pub env: HashMap<String, String>,
 38    pub cursor_shape: Option<CursorShape>,
 39    pub blinking: TerminalBlink,
 40    pub alternate_scroll: AlternateScroll,
 41    pub option_as_meta: bool,
 42    pub copy_on_select: bool,
 43    pub button: bool,
 44    pub dock: TerminalDockPosition,
 45    pub default_width: Pixels,
 46    pub default_height: Pixels,
 47    pub detect_venv: VenvSettings,
 48    pub max_scroll_history_lines: Option<usize>,
 49    pub toolbar: Toolbar,
 50    pub scrollbar: ScrollbarSettings,
 51}
 52
 53#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 54pub struct ScrollbarSettings {
 55    /// When to show the scrollbar in the terminal.
 56    ///
 57    /// Default: inherits editor scrollbar settings
 58    pub show: Option<ShowScrollbar>,
 59}
 60
 61#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 62pub struct ScrollbarSettingsContent {
 63    /// When to show the scrollbar in the terminal.
 64    ///
 65    /// Default: inherits editor scrollbar settings
 66    pub show: Option<Option<ShowScrollbar>>,
 67}
 68
 69/// When to show the scrollbar in the terminal.
 70///
 71/// Default: auto
 72#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 73#[serde(rename_all = "snake_case")]
 74pub enum ShowScrollbar {
 75    /// Show the scrollbar if there's important information or
 76    /// follow the system's configured behavior.
 77    Auto,
 78    /// Match the system's configured behavior.
 79    System,
 80    /// Always show the scrollbar.
 81    Always,
 82    /// Never show the scrollbar.
 83    Never,
 84}
 85
 86#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 87#[serde(rename_all = "snake_case")]
 88pub enum VenvSettings {
 89    #[default]
 90    Off,
 91    On {
 92        /// Default directories to search for virtual environments, relative
 93        /// to the current working directory. We recommend overriding this
 94        /// in your project's settings, rather than globally.
 95        activate_script: Option<ActivateScript>,
 96        directories: Option<Vec<PathBuf>>,
 97    },
 98}
 99
100pub struct VenvSettingsContent<'a> {
101    pub activate_script: ActivateScript,
102    pub directories: &'a [PathBuf],
103}
104
105impl VenvSettings {
106    pub fn as_option(&self) -> Option<VenvSettingsContent> {
107        match self {
108            VenvSettings::Off => None,
109            VenvSettings::On {
110                activate_script,
111                directories,
112            } => Some(VenvSettingsContent {
113                activate_script: activate_script.unwrap_or(ActivateScript::Default),
114                directories: directories.as_deref().unwrap_or(&[]),
115            }),
116        }
117    }
118}
119
120#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
121#[serde(rename_all = "snake_case")]
122pub enum ActivateScript {
123    #[default]
124    Default,
125    Csh,
126    Fish,
127    Nushell,
128    PowerShell,
129}
130
131#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
132pub struct TerminalSettingsContent {
133    /// What shell to use when opening a terminal.
134    ///
135    /// Default: system
136    pub shell: Option<Shell>,
137    /// What working directory to use when launching the terminal
138    ///
139    /// Default: current_project_directory
140    pub working_directory: Option<WorkingDirectory>,
141    /// Sets the terminal's font size.
142    ///
143    /// If this option is not included,
144    /// the terminal will default to matching the buffer's font size.
145    pub font_size: Option<f32>,
146    /// Sets the terminal's font family.
147    ///
148    /// If this option is not included,
149    /// the terminal will default to matching the buffer's font family.
150    pub font_family: Option<String>,
151
152    /// Sets the terminal's font fallbacks.
153    ///
154    /// If this option is not included,
155    /// the terminal will default to matching the buffer's font fallbacks.
156    pub font_fallbacks: Option<Vec<String>>,
157
158    /// Sets the terminal's line height.
159    ///
160    /// Default: comfortable
161    pub line_height: Option<TerminalLineHeight>,
162    pub font_features: Option<FontFeatures>,
163    /// Sets the terminal's font weight in CSS weight units 0-900.
164    pub font_weight: Option<f32>,
165    /// Any key-value pairs added to this list will be added to the terminal's
166    /// environment. Use `:` to separate multiple values.
167    ///
168    /// Default: {}
169    pub env: Option<HashMap<String, String>>,
170    /// Default cursor shape for the terminal.
171    /// Can be "bar", "block", "underline", or "hollow".
172    ///
173    /// Default: None
174    pub cursor_shape: Option<CursorShape>,
175    /// Sets the cursor blinking behavior in the terminal.
176    ///
177    /// Default: terminal_controlled
178    pub blinking: Option<TerminalBlink>,
179    /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
180    /// Alternate Scroll mode converts mouse scroll events into up / down key
181    /// presses when in the alternate screen (e.g. when running applications
182    /// like vim or  less). The terminal can still set and unset this mode.
183    ///
184    /// Default: on
185    pub alternate_scroll: Option<AlternateScroll>,
186    /// Sets whether the option key behaves as the meta key.
187    ///
188    /// Default: false
189    pub option_as_meta: Option<bool>,
190    /// Whether or not selecting text in the terminal will automatically
191    /// copy to the system clipboard.
192    ///
193    /// Default: false
194    pub copy_on_select: Option<bool>,
195    /// Whether to show the terminal button in the status bar.
196    ///
197    /// Default: true
198    pub button: Option<bool>,
199    pub dock: Option<TerminalDockPosition>,
200    /// Default width when the terminal is docked to the left or right.
201    ///
202    /// Default: 640
203    pub default_width: Option<f32>,
204    /// Default height when the terminal is docked to the bottom.
205    ///
206    /// Default: 320
207    pub default_height: Option<f32>,
208    /// Activates the python virtual environment, if one is found, in the
209    /// terminal's working directory (as resolved by the working_directory
210    /// setting). Set this to "off" to disable this behavior.
211    ///
212    /// Default: on
213    pub detect_venv: Option<VenvSettings>,
214    /// The maximum number of lines to keep in the scrollback history.
215    /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
216    /// 0 disables the scrolling.
217    /// Existing terminals will not pick up this change until they are recreated.
218    /// 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.
219    ///
220    /// Default: 10_000
221    pub max_scroll_history_lines: Option<usize>,
222    /// Toolbar related settings
223    pub toolbar: Option<ToolbarContent>,
224    /// Scrollbar-related settings
225    pub scrollbar: Option<ScrollbarSettingsContent>,
226}
227
228impl settings::Settings for TerminalSettings {
229    const KEY: Option<&'static str> = Some("terminal");
230
231    type FileContent = TerminalSettingsContent;
232
233    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
234        sources.json_merge()
235    }
236
237    fn json_schema(
238        generator: &mut SchemaGenerator,
239        params: &SettingsJsonSchemaParams,
240        _: &App,
241    ) -> RootSchema {
242        let mut root_schema = generator.root_schema_for::<Self::FileContent>();
243        root_schema.definitions.extend([
244            ("FontFamilies".into(), params.font_family_schema()),
245            ("FontFallbacks".into(), params.font_fallback_schema()),
246        ]);
247
248        add_references_to_properties(
249            &mut root_schema,
250            &[
251                ("font_family", "#/definitions/FontFamilies"),
252                ("font_fallbacks", "#/definitions/FontFallbacks"),
253            ],
254        );
255
256        root_schema
257    }
258
259    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
260        let name = |s| format!("terminal.integrated.{s}");
261
262        vscode.f32_setting(&name("fontSize"), &mut current.font_size);
263        vscode.string_setting(&name("fontFamily"), &mut current.font_family);
264        vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
265        vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
266        vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
267        match vscode.read_bool(&name("cursorBlinking")) {
268            Some(true) => current.blinking = Some(TerminalBlink::On),
269            Some(false) => current.blinking = Some(TerminalBlink::Off),
270            None => {}
271        }
272        vscode.enum_setting(
273            &name("cursorStyle"),
274            &mut current.cursor_shape,
275            |s| match s {
276                "block" => Some(CursorShape::Block),
277                "line" => Some(CursorShape::Bar),
278                "underline" => Some(CursorShape::Underline),
279                _ => None,
280            },
281        );
282        // they also have "none" and "outline" as options but just for the "Inactive" variant
283        if let Some(height) = vscode
284            .read_value(&name("lineHeight"))
285            .and_then(|v| v.as_f64())
286        {
287            current.line_height = Some(TerminalLineHeight::Custom(height as f32))
288        }
289
290        #[cfg(target_os = "windows")]
291        let platform = "windows";
292        #[cfg(target_os = "linux")]
293        let platform = "linux";
294        #[cfg(target_os = "macos")]
295        let platform = "osx";
296        #[cfg(target_os = "freebsd")]
297        let platform = "freebsd";
298
299        // TODO: handle arguments
300        let shell_name = format!("{platform}Exec");
301        if let Some(s) = vscode.read_string(&name(&shell_name)) {
302            current.shell = Some(Shell::Program(s.to_owned()))
303        }
304
305        if let Some(env) = vscode
306            .read_value(&name(&format!("env.{platform}")))
307            .and_then(|v| v.as_object())
308        {
309            for (k, v) in env {
310                if v.is_null() {
311                    if let Some(zed_env) = current.env.as_mut() {
312                        zed_env.remove(k);
313                    }
314                }
315                let Some(v) = v.as_str() else { continue };
316                if let Some(zed_env) = current.env.as_mut() {
317                    zed_env.insert(k.clone(), v.to_owned());
318                } else {
319                    current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
320                }
321            }
322        }
323    }
324}
325
326#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
327#[serde(rename_all = "snake_case")]
328pub enum TerminalLineHeight {
329    /// Use a line height that's comfortable for reading, 1.618
330    #[default]
331    Comfortable,
332    /// Use a standard line height, 1.3. This option is useful for TUIs,
333    /// particularly if they use box characters
334    Standard,
335    /// Use a custom line height.
336    Custom(f32),
337}
338
339impl TerminalLineHeight {
340    pub fn value(&self) -> AbsoluteLength {
341        let value = match self {
342            TerminalLineHeight::Comfortable => 1.618,
343            TerminalLineHeight::Standard => 1.3,
344            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
345        };
346        px(value).into()
347    }
348}
349
350#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
351#[serde(rename_all = "snake_case")]
352pub enum TerminalBlink {
353    /// Never blink the cursor, ignoring the terminal mode.
354    Off,
355    /// Default the cursor blink to off, but allow the terminal to
356    /// set blinking.
357    TerminalControlled,
358    /// Always blink the cursor, ignoring the terminal mode.
359    On,
360}
361
362#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
363#[serde(rename_all = "snake_case")]
364pub enum AlternateScroll {
365    On,
366    Off,
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
370#[serde(rename_all = "snake_case")]
371pub enum WorkingDirectory {
372    /// Use the current file's project directory.  Will Fallback to the
373    /// first project directory strategy if unsuccessful.
374    CurrentProjectDirectory,
375    /// Use the first project in this workspace's directory.
376    FirstProjectDirectory,
377    /// Always use this platform's home directory (if it can be found).
378    AlwaysHome,
379    /// Always use a specific directory. This value will be shell expanded.
380    /// If this path is not a valid directory the terminal will default to
381    /// this platform's home directory  (if it can be found).
382    Always { directory: String },
383}
384
385// Toolbar related settings
386#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
387pub struct ToolbarContent {
388    /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
389    /// Only shown if the terminal title is not empty.
390    ///
391    /// The shell running in the terminal needs to be configured to emit the title.
392    /// Example: `echo -e "\e]2;New Title\007";`
393    ///
394    /// Default: true
395    pub breadcrumbs: Option<bool>,
396}
397
398#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
399#[serde(rename_all = "snake_case")]
400pub enum CursorShape {
401    /// Cursor is a block like `█`.
402    #[default]
403    Block,
404    /// Cursor is an underscore like `_`.
405    Underline,
406    /// Cursor is a vertical bar like `⎸`.
407    Bar,
408    /// Cursor is a hollow box like `▯`.
409    Hollow,
410}
411
412impl From<CursorShape> for AlacCursorShape {
413    fn from(value: CursorShape) -> Self {
414        match value {
415            CursorShape::Block => AlacCursorShape::Block,
416            CursorShape::Underline => AlacCursorShape::Underline,
417            CursorShape::Bar => AlacCursorShape::Beam,
418            CursorShape::Hollow => AlacCursorShape::HollowBlock,
419        }
420    }
421}
422
423impl From<CursorShape> for AlacCursorStyle {
424    fn from(value: CursorShape) -> Self {
425        AlacCursorStyle {
426            shape: value.into(),
427            blinking: false,
428        }
429    }
430}