terminal_settings.rs

  1use alacritty_terminal::vte::ansi::{
  2    CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
  3};
  4use collections::HashMap;
  5use gpui::{App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8
  9pub use settings::AlternateScroll;
 10use settings::{
 11    CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
 12    TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
 13};
 14use task::Shell;
 15use theme::FontFamilyName;
 16
 17#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 18pub struct Toolbar {
 19    pub breadcrumbs: bool,
 20}
 21
 22#[derive(Clone, Debug, Deserialize)]
 23pub struct TerminalSettings {
 24    pub shell: Shell,
 25    pub working_directory: WorkingDirectory,
 26    pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
 27    pub font_family: Option<FontFamilyName>,
 28    pub font_fallbacks: Option<FontFallbacks>,
 29    pub font_features: Option<FontFeatures>,
 30    pub font_weight: Option<FontWeight>,
 31    pub line_height: TerminalLineHeight,
 32    pub env: HashMap<String, String>,
 33    pub cursor_shape: Option<CursorShape>,
 34    pub blinking: TerminalBlink,
 35    pub alternate_scroll: AlternateScroll,
 36    pub option_as_meta: bool,
 37    pub copy_on_select: bool,
 38    pub keep_selection_on_copy: bool,
 39    pub button: bool,
 40    pub dock: TerminalDockPosition,
 41    pub default_width: Pixels,
 42    pub default_height: Pixels,
 43    pub detect_venv: VenvSettings,
 44    pub max_scroll_history_lines: Option<usize>,
 45    pub toolbar: Toolbar,
 46    pub scrollbar: ScrollbarSettings,
 47    pub minimum_contrast: f32,
 48}
 49
 50#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 51pub struct ScrollbarSettings {
 52    /// When to show the scrollbar in the terminal.
 53    ///
 54    /// Default: inherits editor scrollbar settings
 55    pub show: Option<ShowScrollbar>,
 56}
 57
 58fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
 59    match shell {
 60        settings::Shell::System => Shell::System,
 61        settings::Shell::Program(program) => Shell::Program(program),
 62        settings::Shell::WithArguments {
 63            program,
 64            args,
 65            title_override,
 66        } => Shell::WithArguments {
 67            program,
 68            args,
 69            title_override,
 70        },
 71    }
 72}
 73
 74impl settings::Settings for TerminalSettings {
 75    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
 76        let content = content.terminal.clone().unwrap();
 77        TerminalSettings {
 78            shell: settings_shell_to_task_shell(content.shell.unwrap()),
 79            working_directory: content.working_directory.unwrap(),
 80            font_size: content.font_size.map(px),
 81            font_family: content.font_family,
 82            font_fallbacks: content.font_fallbacks.map(|fallbacks| {
 83                FontFallbacks::from_fonts(
 84                    fallbacks
 85                        .into_iter()
 86                        .map(|family| family.0.to_string())
 87                        .collect(),
 88                )
 89            }),
 90            font_features: content.font_features,
 91            font_weight: content.font_weight.map(FontWeight),
 92            line_height: content.line_height.unwrap(),
 93            env: content.env.unwrap(),
 94            cursor_shape: content.cursor_shape.map(Into::into),
 95            blinking: content.blinking.unwrap(),
 96            alternate_scroll: content.alternate_scroll.unwrap(),
 97            option_as_meta: content.option_as_meta.unwrap(),
 98            copy_on_select: content.copy_on_select.unwrap(),
 99            keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
100            button: content.button.unwrap(),
101            dock: content.dock.unwrap(),
102            default_width: px(content.default_width.unwrap()),
103            default_height: px(content.default_height.unwrap()),
104            detect_venv: content.detect_venv.unwrap(),
105            max_scroll_history_lines: content.max_scroll_history_lines,
106            toolbar: Toolbar {
107                breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
108            },
109            scrollbar: ScrollbarSettings {
110                show: content.scrollbar.unwrap().show,
111            },
112            minimum_contrast: content.minimum_contrast.unwrap(),
113        }
114    }
115
116    fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
117        let mut default = TerminalSettingsContent::default();
118        let current = content.terminal.as_mut().unwrap_or(&mut default);
119        let name = |s| format!("terminal.integrated.{s}");
120
121        vscode.f32_setting(&name("fontSize"), &mut current.font_size);
122        if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
123            current.font_family = Some(FontFamilyName(font_family.into()));
124        }
125        vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
126        vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
127        vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
128        match vscode.read_bool(&name("cursorBlinking")) {
129            Some(true) => current.blinking = Some(TerminalBlink::On),
130            Some(false) => current.blinking = Some(TerminalBlink::Off),
131            None => {}
132        }
133        vscode.enum_setting(
134            &name("cursorStyle"),
135            &mut current.cursor_shape,
136            |s| match s {
137                "block" => Some(CursorShapeContent::Block),
138                "line" => Some(CursorShapeContent::Bar),
139                "underline" => Some(CursorShapeContent::Underline),
140                _ => None,
141            },
142        );
143        // they also have "none" and "outline" as options but just for the "Inactive" variant
144        if let Some(height) = vscode
145            .read_value(&name("lineHeight"))
146            .and_then(|v| v.as_f64())
147        {
148            current.line_height = Some(TerminalLineHeight::Custom(height as f32))
149        }
150
151        #[cfg(target_os = "windows")]
152        let platform = "windows";
153        #[cfg(target_os = "linux")]
154        let platform = "linux";
155        #[cfg(target_os = "macos")]
156        let platform = "osx";
157        #[cfg(target_os = "freebsd")]
158        let platform = "freebsd";
159
160        // TODO: handle arguments
161        let shell_name = format!("{platform}Exec");
162        if let Some(s) = vscode.read_string(&name(&shell_name)) {
163            current.shell = Some(settings::Shell::Program(s.to_owned()))
164        }
165
166        if let Some(env) = vscode
167            .read_value(&name(&format!("env.{platform}")))
168            .and_then(|v| v.as_object())
169        {
170            for (k, v) in env {
171                if v.is_null()
172                    && let Some(zed_env) = current.env.as_mut()
173                {
174                    zed_env.remove(k);
175                }
176                let Some(v) = v.as_str() else { continue };
177                if let Some(zed_env) = current.env.as_mut() {
178                    zed_env.insert(k.clone(), v.to_owned());
179                } else {
180                    current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
181                }
182            }
183        }
184        if content.terminal.is_none() && default != TerminalSettingsContent::default() {
185            content.terminal = Some(default)
186        }
187    }
188}
189
190#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
191#[serde(rename_all = "snake_case")]
192pub enum CursorShape {
193    /// Cursor is a block like `█`.
194    #[default]
195    Block,
196    /// Cursor is an underscore like `_`.
197    Underline,
198    /// Cursor is a vertical bar like `⎸`.
199    Bar,
200    /// Cursor is a hollow box like `▯`.
201    Hollow,
202}
203
204impl From<settings::CursorShapeContent> for CursorShape {
205    fn from(value: settings::CursorShapeContent) -> Self {
206        match value {
207            settings::CursorShapeContent::Block => CursorShape::Block,
208            settings::CursorShapeContent::Underline => CursorShape::Underline,
209            settings::CursorShapeContent::Bar => CursorShape::Bar,
210            settings::CursorShapeContent::Hollow => CursorShape::Hollow,
211        }
212    }
213}
214
215impl From<CursorShape> for AlacCursorShape {
216    fn from(value: CursorShape) -> Self {
217        match value {
218            CursorShape::Block => AlacCursorShape::Block,
219            CursorShape::Underline => AlacCursorShape::Underline,
220            CursorShape::Bar => AlacCursorShape::Beam,
221            CursorShape::Hollow => AlacCursorShape::HollowBlock,
222        }
223    }
224}
225
226impl From<CursorShape> for AlacCursorStyle {
227    fn from(value: CursorShape) -> Self {
228        AlacCursorStyle {
229            shape: value.into(),
230            blinking: false,
231        }
232    }
233}