terminal_settings.rs

  1use collections::HashMap;
  2use gpui::{px, AbsoluteLength, AppContext, FontFeatures, FontWeight, Pixels};
  3use schemars::{
  4    gen::SchemaGenerator,
  5    schema::{InstanceType, RootSchema, Schema, SchemaObject},
  6    JsonSchema,
  7};
  8use serde_derive::{Deserialize, Serialize};
  9use serde_json::Value;
 10use settings::{SettingsJsonSchemaParams, SettingsSources};
 11use std::path::PathBuf;
 12
 13#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 14#[serde(rename_all = "snake_case")]
 15pub enum TerminalDockPosition {
 16    Left,
 17    Bottom,
 18    Right,
 19}
 20
 21#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 22pub struct Toolbar {
 23    pub title: bool,
 24}
 25
 26#[derive(Deserialize)]
 27pub struct TerminalSettings {
 28    pub shell: Shell,
 29    pub working_directory: WorkingDirectory,
 30    pub font_size: Option<Pixels>,
 31    pub font_family: Option<String>,
 32    pub line_height: TerminalLineHeight,
 33    pub font_features: Option<FontFeatures>,
 34    pub font_weight: Option<FontWeight>,
 35    pub env: HashMap<String, String>,
 36    pub blinking: TerminalBlink,
 37    pub alternate_scroll: AlternateScroll,
 38    pub option_as_meta: bool,
 39    pub copy_on_select: bool,
 40    pub button: bool,
 41    pub dock: TerminalDockPosition,
 42    pub default_width: Pixels,
 43    pub default_height: Pixels,
 44    pub detect_venv: VenvSettings,
 45    pub max_scroll_history_lines: Option<usize>,
 46    pub toolbar: Toolbar,
 47}
 48
 49#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 50#[serde(rename_all = "snake_case")]
 51pub enum VenvSettings {
 52    #[default]
 53    Off,
 54    On {
 55        /// Default directories to search for virtual environments, relative
 56        /// to the current working directory. We recommend overriding this
 57        /// in your project's settings, rather than globally.
 58        activate_script: Option<ActivateScript>,
 59        directories: Option<Vec<PathBuf>>,
 60    },
 61}
 62
 63pub struct VenvSettingsContent<'a> {
 64    pub activate_script: ActivateScript,
 65    pub directories: &'a [PathBuf],
 66}
 67
 68impl VenvSettings {
 69    pub fn as_option(&self) -> Option<VenvSettingsContent> {
 70        match self {
 71            VenvSettings::Off => None,
 72            VenvSettings::On {
 73                activate_script,
 74                directories,
 75            } => Some(VenvSettingsContent {
 76                activate_script: activate_script.unwrap_or(ActivateScript::Default),
 77                directories: directories.as_deref().unwrap_or(&[]),
 78            }),
 79        }
 80    }
 81}
 82
 83#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 84#[serde(rename_all = "snake_case")]
 85pub enum ActivateScript {
 86    #[default]
 87    Default,
 88    Csh,
 89    Fish,
 90    Nushell,
 91}
 92
 93#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 94pub struct TerminalSettingsContent {
 95    /// What shell to use when opening a terminal.
 96    ///
 97    /// Default: system
 98    pub shell: Option<Shell>,
 99    /// What working directory to use when launching the terminal
100    ///
101    /// Default: current_project_directory
102    pub working_directory: Option<WorkingDirectory>,
103    /// Sets the terminal's font size.
104    ///
105    /// If this option is not included,
106    /// the terminal will default to matching the buffer's font size.
107    pub font_size: Option<f32>,
108    /// Sets the terminal's font family.
109    ///
110    /// If this option is not included,
111    /// the terminal will default to matching the buffer's font family.
112    pub font_family: Option<String>,
113    /// Sets the terminal's line height.
114    ///
115    /// Default: comfortable
116    pub line_height: Option<TerminalLineHeight>,
117    pub font_features: Option<FontFeatures>,
118    /// Sets the terminal's font weight in CSS weight units 0-900.
119    pub font_weight: Option<f32>,
120    /// Any key-value pairs added to this list will be added to the terminal's
121    /// environment. Use `:` to separate multiple values.
122    ///
123    /// Default: {}
124    pub env: Option<HashMap<String, String>>,
125    /// Sets the cursor blinking behavior in the terminal.
126    ///
127    /// Default: terminal_controlled
128    pub blinking: Option<TerminalBlink>,
129    /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
130    /// Alternate Scroll mode converts mouse scroll events into up / down key
131    /// presses when in the alternate screen (e.g. when running applications
132    /// like vim or  less). The terminal can still set and unset this mode.
133    ///
134    /// Default: off
135    pub alternate_scroll: Option<AlternateScroll>,
136    /// Sets whether the option key behaves as the meta key.
137    ///
138    /// Default: false
139    pub option_as_meta: Option<bool>,
140    /// Whether or not selecting text in the terminal will automatically
141    /// copy to the system clipboard.
142    ///
143    /// Default: false
144    pub copy_on_select: Option<bool>,
145    /// Whether to show the terminal button in the status bar.
146    ///
147    /// Default: true
148    pub button: Option<bool>,
149    pub dock: Option<TerminalDockPosition>,
150    /// Default width when the terminal is docked to the left or right.
151    ///
152    /// Default: 640
153    pub default_width: Option<f32>,
154    /// Default height when the terminal is docked to the bottom.
155    ///
156    /// Default: 320
157    pub default_height: Option<f32>,
158    /// Activates the python virtual environment, if one is found, in the
159    /// terminal's working directory (as resolved by the working_directory
160    /// setting). Set this to "off" to disable this behavior.
161    ///
162    /// Default: on
163    pub detect_venv: Option<VenvSettings>,
164    /// The maximum number of lines to keep in the scrollback history.
165    /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
166    /// 0 disables the scrolling.
167    /// Existing terminals will not pick up this change until they are recreated.
168    /// 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.
169    ///
170    /// Default: 10_000
171    pub max_scroll_history_lines: Option<usize>,
172    /// Toolbar related settings
173    pub toolbar: Option<ToolbarContent>,
174}
175
176impl settings::Settings for TerminalSettings {
177    const KEY: Option<&'static str> = Some("terminal");
178
179    type FileContent = TerminalSettingsContent;
180
181    fn load(
182        sources: SettingsSources<Self::FileContent>,
183        _: &mut AppContext,
184    ) -> anyhow::Result<Self> {
185        sources.json_merge()
186    }
187
188    fn json_schema(
189        generator: &mut SchemaGenerator,
190        params: &SettingsJsonSchemaParams,
191        _: &AppContext,
192    ) -> RootSchema {
193        let mut root_schema = generator.root_schema_for::<Self::FileContent>();
194        let available_fonts = params
195            .font_names
196            .iter()
197            .cloned()
198            .map(Value::String)
199            .collect();
200        let fonts_schema = SchemaObject {
201            instance_type: Some(InstanceType::String.into()),
202            enum_values: Some(available_fonts),
203            ..Default::default()
204        };
205        root_schema
206            .definitions
207            .extend([("FontFamilies".into(), fonts_schema.into())]);
208        root_schema
209            .schema
210            .object
211            .as_mut()
212            .unwrap()
213            .properties
214            .extend([(
215                "font_family".to_owned(),
216                Schema::new_ref("#/definitions/FontFamilies".into()),
217            )]);
218
219        root_schema
220    }
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
224#[serde(rename_all = "snake_case")]
225pub enum TerminalLineHeight {
226    /// Use a line height that's comfortable for reading, 1.618
227    #[default]
228    Comfortable,
229    /// Use a standard line height, 1.3. This option is useful for TUIs,
230    /// particularly if they use box characters
231    Standard,
232    /// Use a custom line height.
233    Custom(f32),
234}
235
236impl TerminalLineHeight {
237    pub fn value(&self) -> AbsoluteLength {
238        let value = match self {
239            TerminalLineHeight::Comfortable => 1.618,
240            TerminalLineHeight::Standard => 1.3,
241            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
242        };
243        px(value).into()
244    }
245}
246
247#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
248#[serde(rename_all = "snake_case")]
249pub enum TerminalBlink {
250    /// Never blink the cursor, ignoring the terminal mode.
251    Off,
252    /// Default the cursor blink to off, but allow the terminal to
253    /// set blinking.
254    TerminalControlled,
255    /// Always blink the cursor, ignoring the terminal mode.
256    On,
257}
258
259#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
260#[serde(rename_all = "snake_case")]
261pub enum Shell {
262    /// Use the system's default terminal configuration in /etc/passwd
263    System,
264    Program(String),
265    WithArguments {
266        program: String,
267        args: Vec<String>,
268    },
269}
270
271impl Shell {
272    pub fn retrieve_system_shell() -> Option<String> {
273        #[cfg(not(target_os = "windows"))]
274        {
275            use anyhow::Context;
276            use util::ResultExt;
277
278            return std::env::var("SHELL")
279                .context("Error finding SHELL in env.")
280                .log_err();
281        }
282        // `alacritty_terminal` uses this as default on Windows. See:
283        // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
284        #[cfg(target_os = "windows")]
285        return Some("powershell".to_owned());
286    }
287
288    /// Convert unix-shell variable syntax to windows-shell syntax.
289    /// `powershell` and `cmd` are considered valid here.
290    #[cfg(target_os = "windows")]
291    pub fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
292        match shell_type {
293            WindowsShellType::Powershell => to_powershell_variable(input),
294            WindowsShellType::Cmd => to_cmd_variable(input),
295            WindowsShellType::Other => input,
296        }
297    }
298
299    #[cfg(target_os = "windows")]
300    pub fn to_windows_shell_type(shell: &str) -> WindowsShellType {
301        if shell == "powershell" || shell.ends_with("powershell.exe") {
302            WindowsShellType::Powershell
303        } else if shell == "cmd" || shell.ends_with("cmd.exe") {
304            WindowsShellType::Cmd
305        } else {
306            // Someother shell detected, the user might install and use a
307            // unix-like shell.
308            WindowsShellType::Other
309        }
310    }
311}
312
313#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
314#[serde(rename_all = "snake_case")]
315pub enum AlternateScroll {
316    On,
317    Off,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
321#[serde(rename_all = "snake_case")]
322pub enum WorkingDirectory {
323    /// Use the current file's project directory.  Will Fallback to the
324    /// first project directory strategy if unsuccessful.
325    CurrentProjectDirectory,
326    /// Use the first project in this workspace's directory.
327    FirstProjectDirectory,
328    /// Always use this platform's home directory (if it can be found).
329    AlwaysHome,
330    /// Always use a specific directory. This value will be shell expanded.
331    /// If this path is not a valid directory the terminal will default to
332    /// this platform's home directory  (if it can be found).
333    Always { directory: String },
334}
335
336// Toolbar related settings
337#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
338pub struct ToolbarContent {
339    /// Whether to display the terminal title in its toolbar.
340    ///
341    /// Default: true
342    pub title: Option<bool>,
343}
344
345#[cfg(target_os = "windows")]
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum WindowsShellType {
348    Powershell,
349    Cmd,
350    Other,
351}
352
353/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
354#[inline]
355#[cfg(target_os = "windows")]
356fn to_cmd_variable(input: String) -> String {
357    if let Some(var_str) = input.strip_prefix("${") {
358        if var_str.find(':').is_none() {
359            // If the input starts with "${", remove the trailing "}"
360            format!("%{}%", &var_str[..var_str.len() - 1])
361        } else {
362            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
363            // which will result in the task failing to run in such cases.
364            input
365        }
366    } else if let Some(var_str) = input.strip_prefix('$') {
367        // If the input starts with "$", directly append to "$env:"
368        format!("%{}%", var_str)
369    } else {
370        // If no prefix is found, return the input as is
371        input
372    }
373}
374
375/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
376#[inline]
377#[cfg(target_os = "windows")]
378fn to_powershell_variable(input: String) -> String {
379    if let Some(var_str) = input.strip_prefix("${") {
380        if var_str.find(':').is_none() {
381            // If the input starts with "${", remove the trailing "}"
382            format!("$env:{}", &var_str[..var_str.len() - 1])
383        } else {
384            // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
385            // which will result in the task failing to run in such cases.
386            input
387        }
388    } else if let Some(var_str) = input.strip_prefix('$') {
389        // If the input starts with "$", directly append to "$env:"
390        format!("$env:{}", var_str)
391    } else {
392        // If no prefix is found, return the input as is
393        input
394    }
395}