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;
 16use util::MergeFrom;
 17
 18#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 19pub struct Toolbar {
 20    pub breadcrumbs: bool,
 21}
 22
 23#[derive(Clone, Debug, Deserialize)]
 24pub struct TerminalSettings {
 25    pub shell: Shell,
 26    pub working_directory: WorkingDirectory,
 27    pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
 28    pub font_family: Option<FontFamilyName>,
 29    pub font_fallbacks: Option<FontFallbacks>,
 30    pub font_features: Option<FontFeatures>,
 31    pub font_weight: Option<FontWeight>,
 32    pub line_height: TerminalLineHeight,
 33    pub env: HashMap<String, String>,
 34    pub cursor_shape: Option<CursorShape>,
 35    pub blinking: TerminalBlink,
 36    pub alternate_scroll: AlternateScroll,
 37    pub option_as_meta: bool,
 38    pub copy_on_select: bool,
 39    pub keep_selection_on_copy: 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    pub scrollbar: ScrollbarSettings,
 48    pub minimum_contrast: f32,
 49}
 50
 51#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 52pub struct ScrollbarSettings {
 53    /// When to show the scrollbar in the terminal.
 54    ///
 55    /// Default: inherits editor scrollbar settings
 56    pub show: Option<ShowScrollbar>,
 57}
 58
 59fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
 60    match shell {
 61        settings::Shell::System => Shell::System,
 62        settings::Shell::Program(program) => Shell::Program(program),
 63        settings::Shell::WithArguments {
 64            program,
 65            args,
 66            title_override,
 67        } => Shell::WithArguments {
 68            program,
 69            args,
 70            title_override,
 71        },
 72    }
 73}
 74
 75impl settings::Settings for TerminalSettings {
 76    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
 77        let content = content.terminal.clone().unwrap();
 78        TerminalSettings {
 79            shell: settings_shell_to_task_shell(content.shell.unwrap()),
 80            working_directory: content.working_directory.unwrap(),
 81            font_size: content.font_size.map(px),
 82            font_family: content.font_family,
 83            font_fallbacks: content.font_fallbacks.map(|fallbacks| {
 84                FontFallbacks::from_fonts(
 85                    fallbacks
 86                        .into_iter()
 87                        .map(|family| family.0.to_string())
 88                        .collect(),
 89                )
 90            }),
 91            font_features: content.font_features,
 92            font_weight: content.font_weight.map(FontWeight),
 93            line_height: content.line_height.unwrap(),
 94            env: content.env.unwrap(),
 95            cursor_shape: content.cursor_shape.map(Into::into),
 96            blinking: content.blinking.unwrap(),
 97            alternate_scroll: content.alternate_scroll.unwrap(),
 98            option_as_meta: content.option_as_meta.unwrap(),
 99            copy_on_select: content.copy_on_select.unwrap(),
100            keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
101            button: content.button.unwrap(),
102            dock: content.dock.unwrap(),
103            default_width: px(content.default_width.unwrap()),
104            default_height: px(content.default_height.unwrap()),
105            detect_venv: content.detect_venv.unwrap(),
106            max_scroll_history_lines: content.max_scroll_history_lines,
107            toolbar: Toolbar {
108                breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
109            },
110            scrollbar: ScrollbarSettings {
111                show: content.scrollbar.unwrap().show.flatten(),
112            },
113            minimum_contrast: content.minimum_contrast.unwrap(),
114        }
115    }
116
117    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
118        let Some(content) = &content.terminal else {
119            return;
120        };
121        self.shell
122            .merge_from(&content.shell.clone().map(settings_shell_to_task_shell));
123        self.working_directory
124            .merge_from(&content.working_directory);
125        if let Some(font_size) = content.font_size.map(px) {
126            self.font_size = Some(font_size)
127        }
128        if let Some(font_family) = content.font_family.clone() {
129            self.font_family = Some(font_family);
130        }
131        if let Some(fallbacks) = content.font_fallbacks.clone() {
132            self.font_fallbacks = Some(FontFallbacks::from_fonts(
133                fallbacks
134                    .into_iter()
135                    .map(|family| family.0.to_string())
136                    .collect(),
137            ))
138        }
139        if let Some(font_features) = content.font_features.clone() {
140            self.font_features = Some(font_features)
141        }
142        if let Some(font_weight) = content.font_weight {
143            self.font_weight = Some(FontWeight(font_weight));
144        }
145        self.line_height.merge_from(&content.line_height);
146        if let Some(env) = &content.env {
147            for (key, value) in env {
148                self.env.insert(key.clone(), value.clone());
149            }
150        }
151        if let Some(cursor_shape) = content.cursor_shape {
152            self.cursor_shape = Some(cursor_shape.into())
153        }
154        self.blinking.merge_from(&content.blinking);
155        self.alternate_scroll.merge_from(&content.alternate_scroll);
156        self.option_as_meta.merge_from(&content.option_as_meta);
157        self.copy_on_select.merge_from(&content.copy_on_select);
158        self.keep_selection_on_copy
159            .merge_from(&content.keep_selection_on_copy);
160        self.button.merge_from(&content.button);
161        self.dock.merge_from(&content.dock);
162        self.default_width
163            .merge_from(&content.default_width.map(px));
164        self.default_height
165            .merge_from(&content.default_height.map(px));
166        self.detect_venv.merge_from(&content.detect_venv);
167        if let Some(max_scroll_history_lines) = content.max_scroll_history_lines {
168            self.max_scroll_history_lines = Some(max_scroll_history_lines)
169        }
170        self.toolbar.breadcrumbs.merge_from(
171            &content
172                .toolbar
173                .as_ref()
174                .and_then(|toolbar| toolbar.breadcrumbs),
175        );
176        self.scrollbar.show.merge_from(
177            &content
178                .scrollbar
179                .as_ref()
180                .and_then(|scrollbar| scrollbar.show),
181        );
182        self.minimum_contrast.merge_from(&content.minimum_contrast);
183    }
184
185    fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
186        let mut default = TerminalSettingsContent::default();
187        let current = content.terminal.as_mut().unwrap_or(&mut default);
188        let name = |s| format!("terminal.integrated.{s}");
189
190        vscode.f32_setting(&name("fontSize"), &mut current.font_size);
191        if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
192            current.font_family = Some(FontFamilyName(font_family.into()));
193        }
194        vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
195        vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
196        vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
197        match vscode.read_bool(&name("cursorBlinking")) {
198            Some(true) => current.blinking = Some(TerminalBlink::On),
199            Some(false) => current.blinking = Some(TerminalBlink::Off),
200            None => {}
201        }
202        vscode.enum_setting(
203            &name("cursorStyle"),
204            &mut current.cursor_shape,
205            |s| match s {
206                "block" => Some(CursorShapeContent::Block),
207                "line" => Some(CursorShapeContent::Bar),
208                "underline" => Some(CursorShapeContent::Underline),
209                _ => None,
210            },
211        );
212        // they also have "none" and "outline" as options but just for the "Inactive" variant
213        if let Some(height) = vscode
214            .read_value(&name("lineHeight"))
215            .and_then(|v| v.as_f64())
216        {
217            current.line_height = Some(TerminalLineHeight::Custom(height as f32))
218        }
219
220        #[cfg(target_os = "windows")]
221        let platform = "windows";
222        #[cfg(target_os = "linux")]
223        let platform = "linux";
224        #[cfg(target_os = "macos")]
225        let platform = "osx";
226        #[cfg(target_os = "freebsd")]
227        let platform = "freebsd";
228
229        // TODO: handle arguments
230        let shell_name = format!("{platform}Exec");
231        if let Some(s) = vscode.read_string(&name(&shell_name)) {
232            current.shell = Some(settings::Shell::Program(s.to_owned()))
233        }
234
235        if let Some(env) = vscode
236            .read_value(&name(&format!("env.{platform}")))
237            .and_then(|v| v.as_object())
238        {
239            for (k, v) in env {
240                if v.is_null()
241                    && let Some(zed_env) = current.env.as_mut()
242                {
243                    zed_env.remove(k);
244                }
245                let Some(v) = v.as_str() else { continue };
246                if let Some(zed_env) = current.env.as_mut() {
247                    zed_env.insert(k.clone(), v.to_owned());
248                } else {
249                    current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
250                }
251            }
252        }
253        if content.terminal.is_none() && default != TerminalSettingsContent::default() {
254            content.terminal = Some(default)
255        }
256    }
257}
258
259#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
260#[serde(rename_all = "snake_case")]
261pub enum CursorShape {
262    /// Cursor is a block like `█`.
263    #[default]
264    Block,
265    /// Cursor is an underscore like `_`.
266    Underline,
267    /// Cursor is a vertical bar like `⎸`.
268    Bar,
269    /// Cursor is a hollow box like `▯`.
270    Hollow,
271}
272
273impl From<settings::CursorShapeContent> for CursorShape {
274    fn from(value: settings::CursorShapeContent) -> Self {
275        match value {
276            settings::CursorShapeContent::Block => CursorShape::Block,
277            settings::CursorShapeContent::Underline => CursorShape::Underline,
278            settings::CursorShapeContent::Bar => CursorShape::Bar,
279            settings::CursorShapeContent::Hollow => CursorShape::Hollow,
280        }
281    }
282}
283
284impl From<CursorShape> for AlacCursorShape {
285    fn from(value: CursorShape) -> Self {
286        match value {
287            CursorShape::Block => AlacCursorShape::Block,
288            CursorShape::Underline => AlacCursorShape::Underline,
289            CursorShape::Bar => AlacCursorShape::Beam,
290            CursorShape::Hollow => AlacCursorShape::HollowBlock,
291        }
292    }
293}
294
295impl From<CursorShape> for AlacCursorStyle {
296    fn from(value: CursorShape) -> Self {
297        AlacCursorStyle {
298            shape: value.into(),
299            blinking: false,
300        }
301    }
302}