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    merge_from::MergeFrom,
 14};
 15use task::Shell;
 16use theme::FontFamilyName;
 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_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
 77        let user_content = content.terminal.clone().unwrap();
 78        // Note: we allow a subset of "terminal" settings in the project files.
 79        let mut project_content = user_content.project.clone();
 80        project_content.merge_from_option(content.project.terminal.as_ref());
 81        TerminalSettings {
 82            shell: settings_shell_to_task_shell(project_content.shell.unwrap()),
 83            working_directory: project_content.working_directory.unwrap(),
 84            font_size: user_content.font_size.map(px),
 85            font_family: user_content.font_family,
 86            font_fallbacks: user_content.font_fallbacks.map(|fallbacks| {
 87                FontFallbacks::from_fonts(
 88                    fallbacks
 89                        .into_iter()
 90                        .map(|family| family.0.to_string())
 91                        .collect(),
 92                )
 93            }),
 94            font_features: user_content.font_features,
 95            font_weight: user_content.font_weight.map(FontWeight),
 96            line_height: user_content.line_height.unwrap(),
 97            env: project_content.env.unwrap(),
 98            cursor_shape: user_content.cursor_shape.map(Into::into),
 99            blinking: user_content.blinking.unwrap(),
100            alternate_scroll: user_content.alternate_scroll.unwrap(),
101            option_as_meta: user_content.option_as_meta.unwrap(),
102            copy_on_select: user_content.copy_on_select.unwrap(),
103            keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(),
104            button: user_content.button.unwrap(),
105            dock: user_content.dock.unwrap(),
106            default_width: px(user_content.default_width.unwrap()),
107            default_height: px(user_content.default_height.unwrap()),
108            detect_venv: project_content.detect_venv.unwrap(),
109            max_scroll_history_lines: user_content.max_scroll_history_lines,
110            toolbar: Toolbar {
111                breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(),
112            },
113            scrollbar: ScrollbarSettings {
114                show: user_content.scrollbar.unwrap().show,
115            },
116            minimum_contrast: user_content.minimum_contrast.unwrap(),
117        }
118    }
119
120    fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
121        let mut default = TerminalSettingsContent::default();
122        let current = content.terminal.as_mut().unwrap_or(&mut default);
123        let name = |s| format!("terminal.integrated.{s}");
124
125        vscode.f32_setting(&name("fontSize"), &mut current.font_size);
126        if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
127            current.font_family = Some(FontFamilyName(font_family.into()));
128        }
129        vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
130        vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
131        vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
132        match vscode.read_bool(&name("cursorBlinking")) {
133            Some(true) => current.blinking = Some(TerminalBlink::On),
134            Some(false) => current.blinking = Some(TerminalBlink::Off),
135            None => {}
136        }
137        vscode.enum_setting(
138            &name("cursorStyle"),
139            &mut current.cursor_shape,
140            |s| match s {
141                "block" => Some(CursorShapeContent::Block),
142                "line" => Some(CursorShapeContent::Bar),
143                "underline" => Some(CursorShapeContent::Underline),
144                _ => None,
145            },
146        );
147        // they also have "none" and "outline" as options but just for the "Inactive" variant
148        if let Some(height) = vscode
149            .read_value(&name("lineHeight"))
150            .and_then(|v| v.as_f64())
151        {
152            current.line_height = Some(TerminalLineHeight::Custom(height as f32))
153        }
154
155        #[cfg(target_os = "windows")]
156        let platform = "windows";
157        #[cfg(target_os = "linux")]
158        let platform = "linux";
159        #[cfg(target_os = "macos")]
160        let platform = "osx";
161        #[cfg(target_os = "freebsd")]
162        let platform = "freebsd";
163
164        // TODO: handle arguments
165        let shell_name = format!("{platform}Exec");
166        if let Some(s) = vscode.read_string(&name(&shell_name)) {
167            current.project.shell = Some(settings::Shell::Program(s.to_owned()))
168        }
169
170        if let Some(env) = vscode
171            .read_value(&name(&format!("env.{platform}")))
172            .and_then(|v| v.as_object())
173        {
174            for (k, v) in env {
175                if v.is_null()
176                    && let Some(zed_env) = current.project.env.as_mut()
177                {
178                    zed_env.remove(k);
179                }
180                let Some(v) = v.as_str() else { continue };
181                if let Some(zed_env) = current.project.env.as_mut() {
182                    zed_env.insert(k.clone(), v.to_owned());
183                } else {
184                    current.project.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
185                }
186            }
187        }
188        if content.terminal.is_none() && default != TerminalSettingsContent::default() {
189            content.terminal = Some(default)
190        }
191    }
192}
193
194#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
195#[serde(rename_all = "snake_case")]
196pub enum CursorShape {
197    /// Cursor is a block like `█`.
198    #[default]
199    Block,
200    /// Cursor is an underscore like `_`.
201    Underline,
202    /// Cursor is a vertical bar like `⎸`.
203    Bar,
204    /// Cursor is a hollow box like `▯`.
205    Hollow,
206}
207
208impl From<settings::CursorShapeContent> for CursorShape {
209    fn from(value: settings::CursorShapeContent) -> Self {
210        match value {
211            settings::CursorShapeContent::Block => CursorShape::Block,
212            settings::CursorShapeContent::Underline => CursorShape::Underline,
213            settings::CursorShapeContent::Bar => CursorShape::Bar,
214            settings::CursorShapeContent::Hollow => CursorShape::Hollow,
215        }
216    }
217}
218
219impl From<CursorShape> for AlacCursorShape {
220    fn from(value: CursorShape) -> Self {
221        match value {
222            CursorShape::Block => AlacCursorShape::Block,
223            CursorShape::Underline => AlacCursorShape::Underline,
224            CursorShape::Bar => AlacCursorShape::Beam,
225            CursorShape::Hollow => AlacCursorShape::HollowBlock,
226        }
227    }
228}
229
230impl From<CursorShape> for AlacCursorStyle {
231    fn from(value: CursorShape) -> Self {
232        AlacCursorStyle {
233            shape: value.into(),
234            blinking: false,
235        }
236    }
237}