1use alacritty_terminal::vte::ansi::{
2 CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
3};
4use collections::HashMap;
5use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
6use schemars::JsonSchema;
7use serde_derive::{Deserialize, Serialize};
8
9use settings::{SettingsKey, SettingsSources, SettingsUi};
10use std::path::PathBuf;
11use task::Shell;
12use theme::FontFamilyName;
13
14#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum TerminalDockPosition {
17 Left,
18 Bottom,
19 Right,
20}
21
22#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
23pub struct Toolbar {
24 pub breadcrumbs: bool,
25}
26
27#[derive(Clone, Debug, Deserialize)]
28pub struct TerminalSettings {
29 pub shell: Shell,
30 pub working_directory: WorkingDirectory,
31 pub font_size: Option<Pixels>,
32 pub font_family: Option<FontFamilyName>,
33 pub font_fallbacks: Option<FontFallbacks>,
34 pub font_features: Option<FontFeatures>,
35 pub font_weight: Option<FontWeight>,
36 pub line_height: TerminalLineHeight,
37 pub env: HashMap<String, String>,
38 pub cursor_shape: Option<CursorShape>,
39 pub blinking: TerminalBlink,
40 pub alternate_scroll: AlternateScroll,
41 pub option_as_meta: bool,
42 pub copy_on_select: bool,
43 pub keep_selection_on_copy: bool,
44 pub button: bool,
45 pub dock: TerminalDockPosition,
46 pub default_width: Pixels,
47 pub default_height: Pixels,
48 pub detect_venv: VenvSettings,
49 pub max_scroll_history_lines: Option<usize>,
50 pub toolbar: Toolbar,
51 pub scrollbar: ScrollbarSettings,
52 pub minimum_contrast: f32,
53}
54
55#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
56pub struct ScrollbarSettings {
57 /// When to show the scrollbar in the terminal.
58 ///
59 /// Default: inherits editor scrollbar settings
60 pub show: Option<ShowScrollbar>,
61}
62
63#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
64pub struct ScrollbarSettingsContent {
65 /// When to show the scrollbar in the terminal.
66 ///
67 /// Default: inherits editor scrollbar settings
68 pub show: Option<Option<ShowScrollbar>>,
69}
70
71/// When to show the scrollbar in the terminal.
72///
73/// Default: auto
74#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
75#[serde(rename_all = "snake_case")]
76pub enum ShowScrollbar {
77 /// Show the scrollbar if there's important information or
78 /// follow the system's configured behavior.
79 Auto,
80 /// Match the system's configured behavior.
81 System,
82 /// Always show the scrollbar.
83 Always,
84 /// Never show the scrollbar.
85 Never,
86}
87
88#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
89#[serde(rename_all = "snake_case")]
90pub enum VenvSettings {
91 #[default]
92 Off,
93 On {
94 /// Default directories to search for virtual environments, relative
95 /// to the current working directory. We recommend overriding this
96 /// in your project's settings, rather than globally.
97 activate_script: Option<ActivateScript>,
98 venv_name: Option<String>,
99 directories: Option<Vec<PathBuf>>,
100 },
101}
102
103pub struct VenvSettingsContent<'a> {
104 pub activate_script: ActivateScript,
105 pub venv_name: &'a str,
106 pub directories: &'a [PathBuf],
107}
108
109impl VenvSettings {
110 pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
111 match self {
112 VenvSettings::Off => None,
113 VenvSettings::On {
114 activate_script,
115 venv_name,
116 directories,
117 } => Some(VenvSettingsContent {
118 activate_script: activate_script.unwrap_or(ActivateScript::Default),
119 venv_name: venv_name.as_deref().unwrap_or(""),
120 directories: directories.as_deref().unwrap_or(&[]),
121 }),
122 }
123 }
124}
125
126#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
127#[serde(rename_all = "snake_case")]
128pub enum ActivateScript {
129 #[default]
130 Default,
131 Csh,
132 Fish,
133 Nushell,
134 PowerShell,
135 Pyenv,
136}
137
138#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
139#[settings_key(key = "terminal")]
140pub struct TerminalSettingsContent {
141 /// What shell to use when opening a terminal.
142 ///
143 /// Default: system
144 pub shell: Option<Shell>,
145 /// What working directory to use when launching the terminal
146 ///
147 /// Default: current_project_directory
148 pub working_directory: Option<WorkingDirectory>,
149 /// Sets the terminal's font size.
150 ///
151 /// If this option is not included,
152 /// the terminal will default to matching the buffer's font size.
153 pub font_size: Option<f32>,
154 /// Sets the terminal's font family.
155 ///
156 /// If this option is not included,
157 /// the terminal will default to matching the buffer's font family.
158 pub font_family: Option<FontFamilyName>,
159
160 /// Sets the terminal's font fallbacks.
161 ///
162 /// If this option is not included,
163 /// the terminal will default to matching the buffer's font fallbacks.
164 #[schemars(extend("uniqueItems" = true))]
165 pub font_fallbacks: Option<Vec<FontFamilyName>>,
166
167 /// Sets the terminal's line height.
168 ///
169 /// Default: comfortable
170 pub line_height: Option<TerminalLineHeight>,
171 pub font_features: Option<FontFeatures>,
172 /// Sets the terminal's font weight in CSS weight units 0-900.
173 pub font_weight: Option<f32>,
174 /// Any key-value pairs added to this list will be added to the terminal's
175 /// environment. Use `:` to separate multiple values.
176 ///
177 /// Default: {}
178 pub env: Option<HashMap<String, String>>,
179 /// Default cursor shape for the terminal.
180 /// Can be "bar", "block", "underline", or "hollow".
181 ///
182 /// Default: None
183 pub cursor_shape: Option<CursorShape>,
184 /// Sets the cursor blinking behavior in the terminal.
185 ///
186 /// Default: terminal_controlled
187 pub blinking: Option<TerminalBlink>,
188 /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
189 /// Alternate Scroll mode converts mouse scroll events into up / down key
190 /// presses when in the alternate screen (e.g. when running applications
191 /// like vim or less). The terminal can still set and unset this mode.
192 ///
193 /// Default: on
194 pub alternate_scroll: Option<AlternateScroll>,
195 /// Sets whether the option key behaves as the meta key.
196 ///
197 /// Default: false
198 pub option_as_meta: Option<bool>,
199 /// Whether or not selecting text in the terminal will automatically
200 /// copy to the system clipboard.
201 ///
202 /// Default: false
203 pub copy_on_select: Option<bool>,
204 /// Whether to keep the text selection after copying it to the clipboard.
205 ///
206 /// Default: false
207 pub keep_selection_on_copy: Option<bool>,
208 /// Whether to show the terminal button in the status bar.
209 ///
210 /// Default: true
211 pub button: Option<bool>,
212 pub dock: Option<TerminalDockPosition>,
213 /// Default width when the terminal is docked to the left or right.
214 ///
215 /// Default: 640
216 pub default_width: Option<f32>,
217 /// Default height when the terminal is docked to the bottom.
218 ///
219 /// Default: 320
220 pub default_height: Option<f32>,
221 /// Activates the python virtual environment, if one is found, in the
222 /// terminal's working directory (as resolved by the working_directory
223 /// setting). Set this to "off" to disable this behavior.
224 ///
225 /// Default: on
226 pub detect_venv: Option<VenvSettings>,
227 /// The maximum number of lines to keep in the scrollback history.
228 /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
229 /// 0 disables the scrolling.
230 /// Existing terminals will not pick up this change until they are recreated.
231 /// 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.
232 ///
233 /// Default: 10_000
234 pub max_scroll_history_lines: Option<usize>,
235 /// Toolbar related settings
236 pub toolbar: Option<ToolbarContent>,
237 /// Scrollbar-related settings
238 pub scrollbar: Option<ScrollbarSettingsContent>,
239 /// The minimum APCA perceptual contrast between foreground and background colors.
240 ///
241 /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
242 /// especially for dark mode. Values range from 0 to 106.
243 ///
244 /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
245 /// https://readtech.org/ARC/tests/bronze-simple-mode/
246 /// - 0: No contrast adjustment
247 /// - 45: Minimum for large fluent text (36px+)
248 /// - 60: Minimum for other content text
249 /// - 75: Minimum for body text
250 /// - 90: Preferred for body text
251 ///
252 /// Default: 45
253 pub minimum_contrast: Option<f32>,
254}
255
256impl settings::Settings for TerminalSettings {
257 type FileContent = TerminalSettingsContent;
258
259 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
260 let settings: Self = sources.json_merge()?;
261
262 // Validate minimum_contrast for APCA
263 if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 {
264 anyhow::bail!(
265 "terminal.minimum_contrast must be between 0 and 106, but got {}. \
266 APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.",
267 settings.minimum_contrast
268 );
269 }
270
271 Ok(settings)
272 }
273
274 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
275 let name = |s| format!("terminal.integrated.{s}");
276
277 vscode.f32_setting(&name("fontSize"), &mut current.font_size);
278 if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
279 current.font_family = Some(FontFamilyName(font_family.into()));
280 }
281 vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
282 vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
283 vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
284 match vscode.read_bool(&name("cursorBlinking")) {
285 Some(true) => current.blinking = Some(TerminalBlink::On),
286 Some(false) => current.blinking = Some(TerminalBlink::Off),
287 None => {}
288 }
289 vscode.enum_setting(
290 &name("cursorStyle"),
291 &mut current.cursor_shape,
292 |s| match s {
293 "block" => Some(CursorShape::Block),
294 "line" => Some(CursorShape::Bar),
295 "underline" => Some(CursorShape::Underline),
296 _ => None,
297 },
298 );
299 // they also have "none" and "outline" as options but just for the "Inactive" variant
300 if let Some(height) = vscode
301 .read_value(&name("lineHeight"))
302 .and_then(|v| v.as_f64())
303 {
304 current.line_height = Some(TerminalLineHeight::Custom(height as f32))
305 }
306
307 #[cfg(target_os = "windows")]
308 let platform = "windows";
309 #[cfg(target_os = "linux")]
310 let platform = "linux";
311 #[cfg(target_os = "macos")]
312 let platform = "osx";
313 #[cfg(target_os = "freebsd")]
314 let platform = "freebsd";
315
316 // TODO: handle arguments
317 let shell_name = format!("{platform}Exec");
318 if let Some(s) = vscode.read_string(&name(&shell_name)) {
319 current.shell = Some(Shell::Program(s.to_owned()))
320 }
321
322 if let Some(env) = vscode
323 .read_value(&name(&format!("env.{platform}")))
324 .and_then(|v| v.as_object())
325 {
326 for (k, v) in env {
327 if v.is_null()
328 && let Some(zed_env) = current.env.as_mut()
329 {
330 zed_env.remove(k);
331 }
332 let Some(v) = v.as_str() else { continue };
333 if let Some(zed_env) = current.env.as_mut() {
334 zed_env.insert(k.clone(), v.to_owned());
335 } else {
336 current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
337 }
338 }
339 }
340 }
341}
342
343#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
344#[serde(rename_all = "snake_case")]
345pub enum TerminalLineHeight {
346 /// Use a line height that's comfortable for reading, 1.618
347 #[default]
348 Comfortable,
349 /// Use a standard line height, 1.3. This option is useful for TUIs,
350 /// particularly if they use box characters
351 Standard,
352 /// Use a custom line height.
353 Custom(f32),
354}
355
356impl TerminalLineHeight {
357 pub fn value(&self) -> AbsoluteLength {
358 let value = match self {
359 TerminalLineHeight::Comfortable => 1.618,
360 TerminalLineHeight::Standard => 1.3,
361 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
362 };
363 px(value).into()
364 }
365}
366
367#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
368#[serde(rename_all = "snake_case")]
369pub enum TerminalBlink {
370 /// Never blink the cursor, ignoring the terminal mode.
371 Off,
372 /// Default the cursor blink to off, but allow the terminal to
373 /// set blinking.
374 TerminalControlled,
375 /// Always blink the cursor, ignoring the terminal mode.
376 On,
377}
378
379#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
380#[serde(rename_all = "snake_case")]
381pub enum AlternateScroll {
382 On,
383 Off,
384}
385
386#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
387#[serde(rename_all = "snake_case")]
388pub enum WorkingDirectory {
389 /// Use the current file's project directory. Will Fallback to the
390 /// first project directory strategy if unsuccessful.
391 CurrentProjectDirectory,
392 /// Use the first project in this workspace's directory.
393 FirstProjectDirectory,
394 /// Always use this platform's home directory (if it can be found).
395 AlwaysHome,
396 /// Always use a specific directory. This value will be shell expanded.
397 /// If this path is not a valid directory the terminal will default to
398 /// this platform's home directory (if it can be found).
399 Always { directory: String },
400}
401
402// Toolbar related settings
403#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
404pub struct ToolbarContent {
405 /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
406 /// Only shown if the terminal title is not empty.
407 ///
408 /// The shell running in the terminal needs to be configured to emit the title.
409 /// Example: `echo -e "\e]2;New Title\007";`
410 ///
411 /// Default: true
412 pub breadcrumbs: Option<bool>,
413}
414
415#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
416#[serde(rename_all = "snake_case")]
417pub enum CursorShape {
418 /// Cursor is a block like `█`.
419 #[default]
420 Block,
421 /// Cursor is an underscore like `_`.
422 Underline,
423 /// Cursor is a vertical bar like `⎸`.
424 Bar,
425 /// Cursor is a hollow box like `▯`.
426 Hollow,
427}
428
429impl From<CursorShape> for AlacCursorShape {
430 fn from(value: CursorShape) -> Self {
431 match value {
432 CursorShape::Block => AlacCursorShape::Block,
433 CursorShape::Underline => AlacCursorShape::Underline,
434 CursorShape::Bar => AlacCursorShape::Beam,
435 CursorShape::Hollow => AlacCursorShape::HollowBlock,
436 }
437 }
438}
439
440impl From<CursorShape> for AlacCursorStyle {
441 fn from(value: CursorShape) -> Self {
442 AlacCursorStyle {
443 shape: value.into(),
444 blinking: false,
445 }
446 }
447}