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::SettingsSources;
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
138impl ActivateScript {
139 pub fn by_shell(shell: &str) -> Option<Self> {
140 Some(match shell {
141 "fish" => ActivateScript::Fish,
142 "tcsh" => ActivateScript::Csh,
143 "nu" => ActivateScript::Nushell,
144 "powershell" | "pwsh" => ActivateScript::PowerShell,
145 "sh" => ActivateScript::Default,
146 _ => return None,
147 })
148 }
149
150 pub fn by_env() -> Option<Self> {
151 let shell = std::env::var("SHELL").ok()?;
152 let shell = std::path::Path::new(&shell)
153 .file_name()
154 .and_then(|name| name.to_str())?;
155 Self::by_shell(shell)
156 }
157}
158
159#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
160pub struct TerminalSettingsContent {
161 /// What shell to use when opening a terminal.
162 ///
163 /// Default: system
164 pub shell: Option<Shell>,
165 /// What working directory to use when launching the terminal
166 ///
167 /// Default: current_project_directory
168 pub working_directory: Option<WorkingDirectory>,
169 /// Sets the terminal's font size.
170 ///
171 /// If this option is not included,
172 /// the terminal will default to matching the buffer's font size.
173 pub font_size: Option<f32>,
174 /// Sets the terminal's font family.
175 ///
176 /// If this option is not included,
177 /// the terminal will default to matching the buffer's font family.
178 pub font_family: Option<FontFamilyName>,
179
180 /// Sets the terminal's font fallbacks.
181 ///
182 /// If this option is not included,
183 /// the terminal will default to matching the buffer's font fallbacks.
184 #[schemars(extend("uniqueItems" = true))]
185 pub font_fallbacks: Option<Vec<FontFamilyName>>,
186
187 /// Sets the terminal's line height.
188 ///
189 /// Default: comfortable
190 pub line_height: Option<TerminalLineHeight>,
191 pub font_features: Option<FontFeatures>,
192 /// Sets the terminal's font weight in CSS weight units 0-900.
193 pub font_weight: Option<f32>,
194 /// Any key-value pairs added to this list will be added to the terminal's
195 /// environment. Use `:` to separate multiple values.
196 ///
197 /// Default: {}
198 pub env: Option<HashMap<String, String>>,
199 /// Default cursor shape for the terminal.
200 /// Can be "bar", "block", "underline", or "hollow".
201 ///
202 /// Default: None
203 pub cursor_shape: Option<CursorShape>,
204 /// Sets the cursor blinking behavior in the terminal.
205 ///
206 /// Default: terminal_controlled
207 pub blinking: Option<TerminalBlink>,
208 /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
209 /// Alternate Scroll mode converts mouse scroll events into up / down key
210 /// presses when in the alternate screen (e.g. when running applications
211 /// like vim or less). The terminal can still set and unset this mode.
212 ///
213 /// Default: on
214 pub alternate_scroll: Option<AlternateScroll>,
215 /// Sets whether the option key behaves as the meta key.
216 ///
217 /// Default: false
218 pub option_as_meta: Option<bool>,
219 /// Whether or not selecting text in the terminal will automatically
220 /// copy to the system clipboard.
221 ///
222 /// Default: false
223 pub copy_on_select: Option<bool>,
224 /// Whether to keep the text selection after copying it to the clipboard.
225 ///
226 /// Default: false
227 pub keep_selection_on_copy: Option<bool>,
228 /// Whether to show the terminal button in the status bar.
229 ///
230 /// Default: true
231 pub button: Option<bool>,
232 pub dock: Option<TerminalDockPosition>,
233 /// Default width when the terminal is docked to the left or right.
234 ///
235 /// Default: 640
236 pub default_width: Option<f32>,
237 /// Default height when the terminal is docked to the bottom.
238 ///
239 /// Default: 320
240 pub default_height: Option<f32>,
241 /// Activates the python virtual environment, if one is found, in the
242 /// terminal's working directory (as resolved by the working_directory
243 /// setting). Set this to "off" to disable this behavior.
244 ///
245 /// Default: on
246 pub detect_venv: Option<VenvSettings>,
247 /// The maximum number of lines to keep in the scrollback history.
248 /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
249 /// 0 disables the scrolling.
250 /// Existing terminals will not pick up this change until they are recreated.
251 /// 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.
252 ///
253 /// Default: 10_000
254 pub max_scroll_history_lines: Option<usize>,
255 /// Toolbar related settings
256 pub toolbar: Option<ToolbarContent>,
257 /// Scrollbar-related settings
258 pub scrollbar: Option<ScrollbarSettingsContent>,
259 /// The minimum APCA perceptual contrast between foreground and background colors.
260 ///
261 /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
262 /// especially for dark mode. Values range from 0 to 106.
263 ///
264 /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
265 /// https://readtech.org/ARC/tests/bronze-simple-mode/
266 /// - 0: No contrast adjustment
267 /// - 45: Minimum for large fluent text (36px+)
268 /// - 60: Minimum for other content text
269 /// - 75: Minimum for body text
270 /// - 90: Preferred for body text
271 ///
272 /// Default: 45
273 pub minimum_contrast: Option<f32>,
274}
275
276impl settings::Settings for TerminalSettings {
277 const KEY: Option<&'static str> = Some("terminal");
278
279 type FileContent = TerminalSettingsContent;
280
281 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
282 let settings: Self = sources.json_merge()?;
283
284 // Validate minimum_contrast for APCA
285 if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 {
286 anyhow::bail!(
287 "terminal.minimum_contrast must be between 0 and 106, but got {}. \
288 APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.",
289 settings.minimum_contrast
290 );
291 }
292
293 Ok(settings)
294 }
295
296 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
297 let name = |s| format!("terminal.integrated.{s}");
298
299 vscode.f32_setting(&name("fontSize"), &mut current.font_size);
300 if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
301 current.font_family = Some(FontFamilyName(font_family.into()));
302 }
303 vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
304 vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
305 vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
306 match vscode.read_bool(&name("cursorBlinking")) {
307 Some(true) => current.blinking = Some(TerminalBlink::On),
308 Some(false) => current.blinking = Some(TerminalBlink::Off),
309 None => {}
310 }
311 vscode.enum_setting(
312 &name("cursorStyle"),
313 &mut current.cursor_shape,
314 |s| match s {
315 "block" => Some(CursorShape::Block),
316 "line" => Some(CursorShape::Bar),
317 "underline" => Some(CursorShape::Underline),
318 _ => None,
319 },
320 );
321 // they also have "none" and "outline" as options but just for the "Inactive" variant
322 if let Some(height) = vscode
323 .read_value(&name("lineHeight"))
324 .and_then(|v| v.as_f64())
325 {
326 current.line_height = Some(TerminalLineHeight::Custom(height as f32))
327 }
328
329 #[cfg(target_os = "windows")]
330 let platform = "windows";
331 #[cfg(target_os = "linux")]
332 let platform = "linux";
333 #[cfg(target_os = "macos")]
334 let platform = "osx";
335 #[cfg(target_os = "freebsd")]
336 let platform = "freebsd";
337
338 // TODO: handle arguments
339 let shell_name = format!("{platform}Exec");
340 if let Some(s) = vscode.read_string(&name(&shell_name)) {
341 current.shell = Some(Shell::Program(s.to_owned()))
342 }
343
344 if let Some(env) = vscode
345 .read_value(&name(&format!("env.{platform}")))
346 .and_then(|v| v.as_object())
347 {
348 for (k, v) in env {
349 if v.is_null() {
350 if let Some(zed_env) = current.env.as_mut() {
351 zed_env.remove(k);
352 }
353 }
354 let Some(v) = v.as_str() else { continue };
355 if let Some(zed_env) = current.env.as_mut() {
356 zed_env.insert(k.clone(), v.to_owned());
357 } else {
358 current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
359 }
360 }
361 }
362 }
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
366#[serde(rename_all = "snake_case")]
367pub enum TerminalLineHeight {
368 /// Use a line height that's comfortable for reading, 1.618
369 #[default]
370 Comfortable,
371 /// Use a standard line height, 1.3. This option is useful for TUIs,
372 /// particularly if they use box characters
373 Standard,
374 /// Use a custom line height.
375 Custom(f32),
376}
377
378impl TerminalLineHeight {
379 pub fn value(&self) -> AbsoluteLength {
380 let value = match self {
381 TerminalLineHeight::Comfortable => 1.618,
382 TerminalLineHeight::Standard => 1.3,
383 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
384 };
385 px(value).into()
386 }
387}
388
389#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
390#[serde(rename_all = "snake_case")]
391pub enum TerminalBlink {
392 /// Never blink the cursor, ignoring the terminal mode.
393 Off,
394 /// Default the cursor blink to off, but allow the terminal to
395 /// set blinking.
396 TerminalControlled,
397 /// Always blink the cursor, ignoring the terminal mode.
398 On,
399}
400
401#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
402#[serde(rename_all = "snake_case")]
403pub enum AlternateScroll {
404 On,
405 Off,
406}
407
408#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
409#[serde(rename_all = "snake_case")]
410pub enum WorkingDirectory {
411 /// Use the current file's project directory. Will Fallback to the
412 /// first project directory strategy if unsuccessful.
413 CurrentProjectDirectory,
414 /// Use the first project in this workspace's directory.
415 FirstProjectDirectory,
416 /// Always use this platform's home directory (if it can be found).
417 AlwaysHome,
418 /// Always use a specific directory. This value will be shell expanded.
419 /// If this path is not a valid directory the terminal will default to
420 /// this platform's home directory (if it can be found).
421 Always { directory: String },
422}
423
424// Toolbar related settings
425#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
426pub struct ToolbarContent {
427 /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
428 /// Only shown if the terminal title is not empty.
429 ///
430 /// The shell running in the terminal needs to be configured to emit the title.
431 /// Example: `echo -e "\e]2;New Title\007";`
432 ///
433 /// Default: true
434 pub breadcrumbs: Option<bool>,
435}
436
437#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
438#[serde(rename_all = "snake_case")]
439pub enum CursorShape {
440 /// Cursor is a block like `█`.
441 #[default]
442 Block,
443 /// Cursor is an underscore like `_`.
444 Underline,
445 /// Cursor is a vertical bar like `⎸`.
446 Bar,
447 /// Cursor is a hollow box like `▯`.
448 Hollow,
449}
450
451impl From<CursorShape> for AlacCursorShape {
452 fn from(value: CursorShape) -> Self {
453 match value {
454 CursorShape::Block => AlacCursorShape::Block,
455 CursorShape::Underline => AlacCursorShape::Underline,
456 CursorShape::Bar => AlacCursorShape::Beam,
457 CursorShape::Hollow => AlacCursorShape::HollowBlock,
458 }
459 }
460}
461
462impl From<CursorShape> for AlacCursorStyle {
463 fn from(value: CursorShape) -> Self {
464 AlacCursorStyle {
465 shape: value.into(),
466 blinking: false,
467 }
468 }
469}