use alacritty_terminal::vte::ansi::{ CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, }; use collections::HashMap; use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::SettingsSources; use std::path::PathBuf; use task::Shell; use theme::FontFamilyName; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TerminalDockPosition { Left, Bottom, Right, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Toolbar { pub breadcrumbs: bool, } #[derive(Clone, Debug, Deserialize)] pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, pub font_size: Option, pub font_family: Option, pub font_fallbacks: Option, pub font_features: Option, pub font_weight: Option, pub line_height: TerminalLineHeight, pub env: HashMap, pub cursor_shape: Option, pub blinking: TerminalBlink, pub alternate_scroll: AlternateScroll, pub option_as_meta: bool, pub copy_on_select: bool, pub keep_selection_on_copy: bool, pub button: bool, pub dock: TerminalDockPosition, pub default_width: Pixels, pub default_height: Pixels, pub detect_venv: VenvSettings, pub max_scroll_history_lines: Option, pub toolbar: Toolbar, pub scrollbar: ScrollbarSettings, pub minimum_contrast: f32, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarSettings { /// When to show the scrollbar in the terminal. /// /// Default: inherits editor scrollbar settings pub show: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarSettingsContent { /// When to show the scrollbar in the terminal. /// /// Default: inherits editor scrollbar settings pub show: Option>, } /// When to show the scrollbar in the terminal. /// /// Default: auto #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ShowScrollbar { /// Show the scrollbar if there's important information or /// follow the system's configured behavior. Auto, /// Match the system's configured behavior. System, /// Always show the scrollbar. Always, /// Never show the scrollbar. Never, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum VenvSettings { #[default] Off, On { /// Default directories to search for virtual environments, relative /// to the current working directory. We recommend overriding this /// in your project's settings, rather than globally. activate_script: Option, directories: Option>, }, } pub struct VenvSettingsContent<'a> { pub activate_script: ActivateScript, pub directories: &'a [PathBuf], } impl VenvSettings { pub fn as_option(&self) -> Option> { match self { VenvSettings::Off => None, VenvSettings::On { activate_script, directories, } => Some(VenvSettingsContent { activate_script: activate_script.unwrap_or(ActivateScript::Default), directories: directories.as_deref().unwrap_or(&[]), }), } } } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ActivateScript { #[default] Default, Csh, Fish, Nushell, PowerShell, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TerminalSettingsContent { /// What shell to use when opening a terminal. /// /// Default: system pub shell: Option, /// What working directory to use when launching the terminal /// /// Default: current_project_directory pub working_directory: Option, /// Sets the terminal's font size. /// /// If this option is not included, /// the terminal will default to matching the buffer's font size. pub font_size: Option, /// Sets the terminal's font family. /// /// If this option is not included, /// the terminal will default to matching the buffer's font family. pub font_family: Option, /// Sets the terminal's font fallbacks. /// /// If this option is not included, /// the terminal will default to matching the buffer's font fallbacks. #[schemars(extend("uniqueItems" = true))] pub font_fallbacks: Option>, /// Sets the terminal's line height. /// /// Default: comfortable pub line_height: Option, pub font_features: Option, /// Sets the terminal's font weight in CSS weight units 0-900. pub font_weight: Option, /// Any key-value pairs added to this list will be added to the terminal's /// environment. Use `:` to separate multiple values. /// /// Default: {} pub env: Option>, /// Default cursor shape for the terminal. /// Can be "bar", "block", "underline", or "hollow". /// /// Default: None pub cursor_shape: Option, /// Sets the cursor blinking behavior in the terminal. /// /// Default: terminal_controlled pub blinking: Option, /// Sets whether Alternate Scroll mode (code: ?1007) is active by default. /// Alternate Scroll mode converts mouse scroll events into up / down key /// presses when in the alternate screen (e.g. when running applications /// like vim or less). The terminal can still set and unset this mode. /// /// Default: on pub alternate_scroll: Option, /// Sets whether the option key behaves as the meta key. /// /// Default: false pub option_as_meta: Option, /// Whether or not selecting text in the terminal will automatically /// copy to the system clipboard. /// /// Default: false pub copy_on_select: Option, /// Whether to keep the text selection after copying it to the clipboard. /// /// Default: false pub keep_selection_on_copy: Option, /// Whether to show the terminal button in the status bar. /// /// Default: true pub button: Option, pub dock: Option, /// Default width when the terminal is docked to the left or right. /// /// Default: 640 pub default_width: Option, /// Default height when the terminal is docked to the bottom. /// /// Default: 320 pub default_height: Option, /// Activates the python virtual environment, if one is found, in the /// terminal's working directory (as resolved by the working_directory /// setting). Set this to "off" to disable this behavior. /// /// Default: on pub detect_venv: Option, /// The maximum number of lines to keep in the scrollback history. /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. /// 0 disables the scrolling. /// Existing terminals will not pick up this change until they are recreated. /// See Alacritty documentation for more information. /// /// Default: 10_000 pub max_scroll_history_lines: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar-related settings pub scrollbar: Option, /// The minimum APCA perceptual contrast between foreground and background colors. /// /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, /// especially for dark mode. Values range from 0 to 106. /// /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode: /// https://readtech.org/ARC/tests/bronze-simple-mode/ /// - 0: No contrast adjustment /// - 45: Minimum for large fluent text (36px+) /// - 60: Minimum for other content text /// - 75: Minimum for body text /// - 90: Preferred for body text /// /// Default: 0 (no adjustment) pub minimum_contrast: Option, } impl settings::Settings for TerminalSettings { const KEY: Option<&'static str> = Some("terminal"); type FileContent = TerminalSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { let settings: Self = sources.json_merge()?; // Validate minimum_contrast for APCA if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 { anyhow::bail!( "terminal.minimum_contrast must be between 0 and 106, but got {}. \ APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.", settings.minimum_contrast ); } Ok(settings) } fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { let name = |s| format!("terminal.integrated.{s}"); vscode.f32_setting(&name("fontSize"), &mut current.font_size); if let Some(font_family) = vscode.read_string(&name("fontFamily")) { current.font_family = Some(FontFamilyName(font_family.into())); } vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select); vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta); vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines); match vscode.read_bool(&name("cursorBlinking")) { Some(true) => current.blinking = Some(TerminalBlink::On), Some(false) => current.blinking = Some(TerminalBlink::Off), None => {} } vscode.enum_setting( &name("cursorStyle"), &mut current.cursor_shape, |s| match s { "block" => Some(CursorShape::Block), "line" => Some(CursorShape::Bar), "underline" => Some(CursorShape::Underline), _ => None, }, ); // they also have "none" and "outline" as options but just for the "Inactive" variant if let Some(height) = vscode .read_value(&name("lineHeight")) .and_then(|v| v.as_f64()) { current.line_height = Some(TerminalLineHeight::Custom(height as f32)) } #[cfg(target_os = "windows")] let platform = "windows"; #[cfg(target_os = "linux")] let platform = "linux"; #[cfg(target_os = "macos")] let platform = "osx"; #[cfg(target_os = "freebsd")] let platform = "freebsd"; // TODO: handle arguments let shell_name = format!("{platform}Exec"); if let Some(s) = vscode.read_string(&name(&shell_name)) { current.shell = Some(Shell::Program(s.to_owned())) } if let Some(env) = vscode .read_value(&name(&format!("env.{platform}"))) .and_then(|v| v.as_object()) { for (k, v) in env { if v.is_null() { if let Some(zed_env) = current.env.as_mut() { zed_env.remove(k); } } let Some(v) = v.as_str() else { continue }; if let Some(zed_env) = current.env.as_mut() { zed_env.insert(k.clone(), v.to_owned()); } else { current.env = Some([(k.clone(), v.to_owned())].into_iter().collect()) } } } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum TerminalLineHeight { /// Use a line height that's comfortable for reading, 1.618 #[default] Comfortable, /// Use a standard line height, 1.3. This option is useful for TUIs, /// particularly if they use box characters Standard, /// Use a custom line height. Custom(f32), } impl TerminalLineHeight { pub fn value(&self) -> AbsoluteLength { let value = match self { TerminalLineHeight::Comfortable => 1.618, TerminalLineHeight::Standard => 1.3, TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), }; px(value).into() } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum TerminalBlink { /// Never blink the cursor, ignoring the terminal mode. Off, /// Default the cursor blink to off, but allow the terminal to /// set blinking. TerminalControlled, /// Always blink the cursor, ignoring the terminal mode. On, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AlternateScroll { On, Off, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum WorkingDirectory { /// Use the current file's project directory. Will Fallback to the /// first project directory strategy if unsuccessful. CurrentProjectDirectory, /// Use the first project in this workspace's directory. FirstProjectDirectory, /// Always use this platform's home directory (if it can be found). AlwaysHome, /// Always use a specific directory. This value will be shell expanded. /// If this path is not a valid directory the terminal will default to /// this platform's home directory (if it can be found). Always { directory: String }, } // Toolbar related settings #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { /// Whether to display the terminal title in breadcrumbs inside the terminal pane. /// Only shown if the terminal title is not empty. /// /// The shell running in the terminal needs to be configured to emit the title. /// Example: `echo -e "\e]2;New Title\007";` /// /// Default: true pub breadcrumbs: Option, } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CursorShape { /// Cursor is a block like `█`. #[default] Block, /// Cursor is an underscore like `_`. Underline, /// Cursor is a vertical bar like `⎸`. Bar, /// Cursor is a hollow box like `▯`. Hollow, } impl From for AlacCursorShape { fn from(value: CursorShape) -> Self { match value { CursorShape::Block => AlacCursorShape::Block, CursorShape::Underline => AlacCursorShape::Underline, CursorShape::Bar => AlacCursorShape::Beam, CursorShape::Hollow => AlacCursorShape::HollowBlock, } } } impl From for AlacCursorStyle { fn from(value: CursorShape) -> Self { AlacCursorStyle { shape: value.into(), blinking: false, } } }