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 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<FontFamilyName>,
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    #[schemars(extend("uniqueItems" = true))]
157    pub font_fallbacks: Option<Vec<FontFamilyName>>,
158
159    /// Sets the terminal's line height.
160    ///
161    /// Default: comfortable
162    pub line_height: Option<TerminalLineHeight>,
163    pub font_features: Option<FontFeatures>,
164    /// Sets the terminal's font weight in CSS weight units 0-900.
165    pub font_weight: Option<f32>,
166    /// Any key-value pairs added to this list will be added to the terminal's
167    /// environment. Use `:` to separate multiple values.
168    ///
169    /// Default: {}
170    pub env: Option<HashMap<String, String>>,
171    /// Default cursor shape for the terminal.
172    /// Can be "bar", "block", "underline", or "hollow".
173    ///
174    /// Default: None
175    pub cursor_shape: Option<CursorShape>,
176    /// Sets the cursor blinking behavior in the terminal.
177    ///
178    /// Default: terminal_controlled
179    pub blinking: Option<TerminalBlink>,
180    /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
181    /// Alternate Scroll mode converts mouse scroll events into up / down key
182    /// presses when in the alternate screen (e.g. when running applications
183    /// like vim or  less). The terminal can still set and unset this mode.
184    ///
185    /// Default: on
186    pub alternate_scroll: Option<AlternateScroll>,
187    /// Sets whether the option key behaves as the meta key.
188    ///
189    /// Default: false
190    pub option_as_meta: Option<bool>,
191    /// Whether or not selecting text in the terminal will automatically
192    /// copy to the system clipboard.
193    ///
194    /// Default: false
195    pub copy_on_select: Option<bool>,
196    /// Whether to show the terminal button in the status bar.
197    ///
198    /// Default: true
199    pub button: Option<bool>,
200    pub dock: Option<TerminalDockPosition>,
201    /// Default width when the terminal is docked to the left or right.
202    ///
203    /// Default: 640
204    pub default_width: Option<f32>,
205    /// Default height when the terminal is docked to the bottom.
206    ///
207    /// Default: 320
208    pub default_height: Option<f32>,
209    /// Activates the python virtual environment, if one is found, in the
210    /// terminal's working directory (as resolved by the working_directory
211    /// setting). Set this to "off" to disable this behavior.
212    ///
213    /// Default: on
214    pub detect_venv: Option<VenvSettings>,
215    /// The maximum number of lines to keep in the scrollback history.
216    /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
217    /// 0 disables the scrolling.
218    /// Existing terminals will not pick up this change until they are recreated.
219    /// 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.
220    ///
221    /// Default: 10_000
222    pub max_scroll_history_lines: Option<usize>,
223    /// Toolbar related settings
224    pub toolbar: Option<ToolbarContent>,
225    /// Scrollbar-related settings
226    pub scrollbar: Option<ScrollbarSettingsContent>,
227}
228
229impl settings::Settings for TerminalSettings {
230    const KEY: Option<&'static str> = Some("terminal");
231
232    type FileContent = TerminalSettingsContent;
233
234    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
235        sources.json_merge()
236    }
237
238    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
239        let name = |s| format!("terminal.integrated.{s}");
240
241        vscode.f32_setting(&name("fontSize"), &mut current.font_size);
242        if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
243            current.font_family = Some(FontFamilyName(font_family.into()));
244        }
245        vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
246        vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
247        vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
248        match vscode.read_bool(&name("cursorBlinking")) {
249            Some(true) => current.blinking = Some(TerminalBlink::On),
250            Some(false) => current.blinking = Some(TerminalBlink::Off),
251            None => {}
252        }
253        vscode.enum_setting(
254            &name("cursorStyle"),
255            &mut current.cursor_shape,
256            |s| match s {
257                "block" => Some(CursorShape::Block),
258                "line" => Some(CursorShape::Bar),
259                "underline" => Some(CursorShape::Underline),
260                _ => None,
261            },
262        );
263        // they also have "none" and "outline" as options but just for the "Inactive" variant
264        if let Some(height) = vscode
265            .read_value(&name("lineHeight"))
266            .and_then(|v| v.as_f64())
267        {
268            current.line_height = Some(TerminalLineHeight::Custom(height as f32))
269        }
270
271        #[cfg(target_os = "windows")]
272        let platform = "windows";
273        #[cfg(target_os = "linux")]
274        let platform = "linux";
275        #[cfg(target_os = "macos")]
276        let platform = "osx";
277        #[cfg(target_os = "freebsd")]
278        let platform = "freebsd";
279
280        // TODO: handle arguments
281        let shell_name = format!("{platform}Exec");
282        if let Some(s) = vscode.read_string(&name(&shell_name)) {
283            current.shell = Some(Shell::Program(s.to_owned()))
284        }
285
286        if let Some(env) = vscode
287            .read_value(&name(&format!("env.{platform}")))
288            .and_then(|v| v.as_object())
289        {
290            for (k, v) in env {
291                if v.is_null() {
292                    if let Some(zed_env) = current.env.as_mut() {
293                        zed_env.remove(k);
294                    }
295                }
296                let Some(v) = v.as_str() else { continue };
297                if let Some(zed_env) = current.env.as_mut() {
298                    zed_env.insert(k.clone(), v.to_owned());
299                } else {
300                    current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
301                }
302            }
303        }
304    }
305}
306
307#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
308#[serde(rename_all = "snake_case")]
309pub enum TerminalLineHeight {
310    /// Use a line height that's comfortable for reading, 1.618
311    #[default]
312    Comfortable,
313    /// Use a standard line height, 1.3. This option is useful for TUIs,
314    /// particularly if they use box characters
315    Standard,
316    /// Use a custom line height.
317    Custom(f32),
318}
319
320impl TerminalLineHeight {
321    pub fn value(&self) -> AbsoluteLength {
322        let value = match self {
323            TerminalLineHeight::Comfortable => 1.618,
324            TerminalLineHeight::Standard => 1.3,
325            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
326        };
327        px(value).into()
328    }
329}
330
331#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
332#[serde(rename_all = "snake_case")]
333pub enum TerminalBlink {
334    /// Never blink the cursor, ignoring the terminal mode.
335    Off,
336    /// Default the cursor blink to off, but allow the terminal to
337    /// set blinking.
338    TerminalControlled,
339    /// Always blink the cursor, ignoring the terminal mode.
340    On,
341}
342
343#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
344#[serde(rename_all = "snake_case")]
345pub enum AlternateScroll {
346    On,
347    Off,
348}
349
350#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
351#[serde(rename_all = "snake_case")]
352pub enum WorkingDirectory {
353    /// Use the current file's project directory.  Will Fallback to the
354    /// first project directory strategy if unsuccessful.
355    CurrentProjectDirectory,
356    /// Use the first project in this workspace's directory.
357    FirstProjectDirectory,
358    /// Always use this platform's home directory (if it can be found).
359    AlwaysHome,
360    /// Always use a specific directory. This value will be shell expanded.
361    /// If this path is not a valid directory the terminal will default to
362    /// this platform's home directory  (if it can be found).
363    Always { directory: String },
364}
365
366// Toolbar related settings
367#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
368pub struct ToolbarContent {
369    /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
370    /// Only shown if the terminal title is not empty.
371    ///
372    /// The shell running in the terminal needs to be configured to emit the title.
373    /// Example: `echo -e "\e]2;New Title\007";`
374    ///
375    /// Default: true
376    pub breadcrumbs: Option<bool>,
377}
378
379#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
380#[serde(rename_all = "snake_case")]
381pub enum CursorShape {
382    /// Cursor is a block like `█`.
383    #[default]
384    Block,
385    /// Cursor is an underscore like `_`.
386    Underline,
387    /// Cursor is a vertical bar like `⎸`.
388    Bar,
389    /// Cursor is a hollow box like `▯`.
390    Hollow,
391}
392
393impl From<CursorShape> for AlacCursorShape {
394    fn from(value: CursorShape) -> Self {
395        match value {
396            CursorShape::Block => AlacCursorShape::Block,
397            CursorShape::Underline => AlacCursorShape::Underline,
398            CursorShape::Bar => AlacCursorShape::Beam,
399            CursorShape::Hollow => AlacCursorShape::HollowBlock,
400        }
401    }
402}
403
404impl From<CursorShape> for AlacCursorStyle {
405    fn from(value: CursorShape) -> Self {
406        AlacCursorStyle {
407            shape: value.into(),
408            blinking: false,
409        }
410    }
411}