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
297        // TODO: handle arguments
298        let shell_name = format!("{platform}Exec");
299        if let Some(s) = vscode.read_string(&name(&shell_name)) {
300            current.shell = Some(Shell::Program(s.to_owned()))
301        }
302
303        if let Some(env) = vscode
304            .read_value(&name(&format!("env.{platform}")))
305            .and_then(|v| v.as_object())
306        {
307            for (k, v) in env {
308                if v.is_null() {
309                    if let Some(zed_env) = current.env.as_mut() {
310                        zed_env.remove(k);
311                    }
312                }
313                let Some(v) = v.as_str() else { continue };
314                if let Some(zed_env) = current.env.as_mut() {
315                    zed_env.insert(k.clone(), v.to_owned());
316                } else {
317                    current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
318                }
319            }
320        }
321    }
322}
323
324#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
325#[serde(rename_all = "snake_case")]
326pub enum TerminalLineHeight {
327    /// Use a line height that's comfortable for reading, 1.618
328    #[default]
329    Comfortable,
330    /// Use a standard line height, 1.3. This option is useful for TUIs,
331    /// particularly if they use box characters
332    Standard,
333    /// Use a custom line height.
334    Custom(f32),
335}
336
337impl TerminalLineHeight {
338    pub fn value(&self) -> AbsoluteLength {
339        let value = match self {
340            TerminalLineHeight::Comfortable => 1.618,
341            TerminalLineHeight::Standard => 1.3,
342            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
343        };
344        px(value).into()
345    }
346}
347
348#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
349#[serde(rename_all = "snake_case")]
350pub enum TerminalBlink {
351    /// Never blink the cursor, ignoring the terminal mode.
352    Off,
353    /// Default the cursor blink to off, but allow the terminal to
354    /// set blinking.
355    TerminalControlled,
356    /// Always blink the cursor, ignoring the terminal mode.
357    On,
358}
359
360#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
361#[serde(rename_all = "snake_case")]
362pub enum AlternateScroll {
363    On,
364    Off,
365}
366
367#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
368#[serde(rename_all = "snake_case")]
369pub enum WorkingDirectory {
370    /// Use the current file's project directory.  Will Fallback to the
371    /// first project directory strategy if unsuccessful.
372    CurrentProjectDirectory,
373    /// Use the first project in this workspace's directory.
374    FirstProjectDirectory,
375    /// Always use this platform's home directory (if it can be found).
376    AlwaysHome,
377    /// Always use a specific directory. This value will be shell expanded.
378    /// If this path is not a valid directory the terminal will default to
379    /// this platform's home directory  (if it can be found).
380    Always { directory: String },
381}
382
383// Toolbar related settings
384#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
385pub struct ToolbarContent {
386    /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
387    /// Only shown if the terminal title is not empty.
388    ///
389    /// The shell running in the terminal needs to be configured to emit the title.
390    /// Example: `echo -e "\e]2;New Title\007";`
391    ///
392    /// Default: true
393    pub breadcrumbs: Option<bool>,
394}
395
396#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
397#[serde(rename_all = "snake_case")]
398pub enum CursorShape {
399    /// Cursor is a block like `█`.
400    #[default]
401    Block,
402    /// Cursor is an underscore like `_`.
403    Underline,
404    /// Cursor is a vertical bar like `⎸`.
405    Bar,
406    /// Cursor is a hollow box like `▯`.
407    Hollow,
408}
409
410impl From<CursorShape> for AlacCursorShape {
411    fn from(value: CursorShape) -> Self {
412        match value {
413            CursorShape::Block => AlacCursorShape::Block,
414            CursorShape::Underline => AlacCursorShape::Underline,
415            CursorShape::Bar => AlacCursorShape::Beam,
416            CursorShape::Hollow => AlacCursorShape::HollowBlock,
417        }
418    }
419}
420
421impl From<CursorShape> for AlacCursorStyle {
422    fn from(value: CursorShape) -> Self {
423        AlacCursorStyle {
424            shape: value.into(),
425            blinking: false,
426        }
427    }
428}