terminal_settings.rs

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