1use std::path::PathBuf;
2
3use collections::HashMap;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use settings_macros::{MergeFrom, with_fallible_options};
7
8use crate::{FontFamilyName, FontFeaturesContent, FontSize, FontWeightContent};
9
10#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
11pub struct ProjectTerminalSettingsContent {
12 /// What shell to use when opening a terminal.
13 ///
14 /// Default: system
15 pub shell: Option<Shell>,
16 /// What working directory to use when launching the terminal
17 ///
18 /// Default: current_project_directory
19 pub working_directory: Option<WorkingDirectory>,
20 /// Any key-value pairs added to this list will be added to the terminal's
21 /// environment. Use `:` to separate multiple values.
22 ///
23 /// Default: {}
24 pub env: Option<HashMap<String, String>>,
25 /// Activates the python virtual environment, if one is found, in the
26 /// terminal's working directory (as resolved by the working_directory
27 /// setting). Set this to "off" to disable this behavior.
28 ///
29 /// Default: on
30 pub detect_venv: Option<VenvSettings>,
31 /// Regexes used to identify paths for hyperlink navigation.
32 ///
33 /// Default: [
34 /// // Python-style diagnostics
35 /// "File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
36 /// // Common path syntax with optional line, column, description, trailing punctuation, or
37 /// // surrounding symbols or quotes
38 /// [
39 /// "(?x)",
40 /// "# optionally starts with 0-2 opening prefix symbols",
41 /// "[({\\[<]{0,2}",
42 /// "# which may be followed by an opening quote",
43 /// "(?<quote>[\"'`])?",
44 /// "# `path` is the shortest sequence of any non-space character",
45 /// "(?<link>(?<path>[^ ]+?",
46 /// " # which may end with a line and optionally a column,",
47 /// " (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
48 /// "))",
49 /// "# which must be followed by a matching quote",
50 /// "(?(<quote>)\\k<quote>)",
51 /// "# and optionally a single closing symbol",
52 /// "[)}\\]>]?",
53 /// "# if line/column matched, may be followed by a description",
54 /// "(?(<line_column>):[^ 0-9][^ ]*)?",
55 /// "# which may be followed by trailing punctuation",
56 /// "[.,:)}\\]>]*",
57 /// "# and always includes trailing whitespace or end of line",
58 /// "([ ]+|$)"
59 /// ]
60 /// ]
61 pub path_hyperlink_regexes: Option<Vec<PathHyperlinkRegex>>,
62 /// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds.
63 ///
64 /// Default: 1
65 pub path_hyperlink_timeout_ms: Option<u64>,
66 /// Sandbox settings for the terminal.
67 pub sandbox: Option<SandboxSettingsContent>,
68}
69
70#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
71pub struct SandboxSettingsContent {
72 /// Whether terminal sandboxing is enabled.
73 ///
74 /// Default: false
75 pub enabled: Option<bool>,
76
77 /// Which terminal types get sandboxed.
78 /// - "terminal": only the user's interactive terminal panel
79 /// - "tool": only the agent's terminal tool
80 /// - "both": both
81 /// - "neither": sandbox settings are defined but not applied
82 ///
83 /// Default: "both"
84 pub apply_to: Option<SandboxApplyTo>,
85
86 /// System paths the shell needs to function. These have OS-specific
87 /// defaults built into Zed. Set a category to an explicit array to
88 /// replace the default. Set to `[]` to deny all access of that type.
89 /// Leave as `null` to use the OS-specific default.
90 pub system_paths: Option<SystemPathsSettingsContent>,
91
92 /// Additional directories to allow read+execute access to (binaries, toolchains).
93 /// These are for user-specific tool directories, not system paths.
94 pub additional_executable_paths: Option<Vec<String>>,
95
96 /// Additional directories to allow read-only access to.
97 pub additional_read_only_paths: Option<Vec<String>>,
98
99 /// Additional directories to allow read+write access to.
100 pub additional_read_write_paths: Option<Vec<String>>,
101
102 /// Whether to allow network access from the sandboxed terminal.
103 ///
104 /// Default: true
105 pub allow_network: Option<bool>,
106
107 /// Environment variables to pass through to the sandboxed terminal.
108 /// All other env vars from the parent process are stripped.
109 ///
110 /// Default: ["PATH", "HOME", "USER", "SHELL", "LANG", "TERM", "TERM_PROGRAM",
111 /// "CARGO_HOME", "RUSTUP_HOME", "GOPATH", "EDITOR", "VISUAL",
112 /// "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_RUNTIME_DIR",
113 /// "SSH_AUTH_SOCK", "GPG_TTY", "COLORTERM"]
114 pub allowed_env_vars: Option<Vec<String>>,
115}
116
117#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
118pub struct SystemPathsSettingsContent {
119 /// Paths with read+execute access (binaries, shared libraries).
120 pub executable: Option<Vec<String>>,
121
122 /// Paths with read-only access (config files, data, certificates).
123 pub read_only: Option<Vec<String>>,
124
125 /// Paths with read+write access (devices, temp directories, IPC sockets).
126 pub read_write: Option<Vec<String>>,
127}
128
129#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
130#[serde(rename_all = "snake_case")]
131pub enum SandboxApplyTo {
132 /// Only the user's interactive terminal panel
133 Terminal,
134 /// Only the agent's terminal tool
135 Tool,
136 /// Both terminal panel and agent terminal tool
137 #[default]
138 Both,
139 /// Sandbox settings are defined but not applied
140 Neither,
141}
142
143#[with_fallible_options]
144#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
145pub struct TerminalSettingsContent {
146 #[serde(flatten)]
147 pub project: ProjectTerminalSettingsContent,
148 /// Sets the terminal's font size.
149 ///
150 /// If this option is not included,
151 /// the terminal will default to matching the buffer's font size.
152 pub font_size: Option<FontSize>,
153 /// Sets the terminal's font family.
154 ///
155 /// If this option is not included,
156 /// the terminal will default to matching the buffer's font family.
157 pub font_family: Option<FontFamilyName>,
158
159 /// Sets the terminal's font fallbacks.
160 ///
161 /// If this option is not included,
162 /// the terminal will default to matching the buffer's font fallbacks.
163 #[schemars(extend("uniqueItems" = true))]
164 pub font_fallbacks: Option<Vec<FontFamilyName>>,
165
166 /// Sets the terminal's line height.
167 ///
168 /// Default: comfortable
169 pub line_height: Option<TerminalLineHeight>,
170 pub font_features: Option<FontFeaturesContent>,
171 /// Sets the terminal's font weight in CSS weight units 0-900.
172 pub font_weight: Option<FontWeightContent>,
173 /// Default cursor shape for the terminal.
174 /// Can be "bar", "block", "underline", or "hollow".
175 ///
176 /// Default: "block"
177 pub cursor_shape: Option<CursorShapeContent>,
178 /// Sets the cursor blinking behavior in the terminal.
179 ///
180 /// Default: terminal_controlled
181 pub blinking: Option<TerminalBlink>,
182 /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
183 /// Alternate Scroll mode converts mouse scroll events into up / down key
184 /// presses when in the alternate screen (e.g. when running applications
185 /// like vim or less). The terminal can still set and unset this mode.
186 ///
187 /// Default: on
188 pub alternate_scroll: Option<AlternateScroll>,
189 /// Sets whether the option key behaves as the meta key.
190 ///
191 /// Default: false
192 pub option_as_meta: Option<bool>,
193 /// Whether or not selecting text in the terminal will automatically
194 /// copy to the system clipboard.
195 ///
196 /// Default: false
197 pub copy_on_select: Option<bool>,
198 /// Whether to keep the text selection after copying it to the clipboard.
199 ///
200 /// Default: true
201 pub keep_selection_on_copy: Option<bool>,
202 /// Whether to show the terminal button in the status bar.
203 ///
204 /// Default: true
205 pub button: Option<bool>,
206 pub dock: Option<TerminalDockPosition>,
207 /// Default width when the terminal is docked to the left or right.
208 ///
209 /// Default: 640
210 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
211 pub default_width: Option<f32>,
212 /// Default height when the terminal is docked to the bottom.
213 ///
214 /// Default: 320
215 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
216 pub default_height: Option<f32>,
217 /// The maximum number of lines to keep in the scrollback history.
218 /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
219 /// 0 disables the scrolling.
220 /// Existing terminals will not pick up this change until they are recreated.
221 /// 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.
222 ///
223 /// Default: 10_000
224 pub max_scroll_history_lines: Option<usize>,
225 /// The multiplier for scrolling with the mouse wheel.
226 ///
227 /// Default: 1.0
228 pub scroll_multiplier: Option<f32>,
229 /// Toolbar related settings
230 pub toolbar: Option<TerminalToolbarContent>,
231 /// Scrollbar-related settings
232 pub scrollbar: Option<ScrollbarSettingsContent>,
233 /// The minimum APCA perceptual contrast between foreground and background colors.
234 ///
235 /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
236 /// especially for dark mode. Values range from 0 to 106.
237 ///
238 /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
239 /// https://readtech.org/ARC/tests/bronze-simple-mode/
240 /// - 0: No contrast adjustment
241 /// - 45: Minimum for large fluent text (36px+)
242 /// - 60: Minimum for other content text
243 /// - 75: Minimum for body text
244 /// - 90: Preferred for body text
245 ///
246 /// Default: 45
247 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
248 pub minimum_contrast: Option<f32>,
249}
250
251/// Shell configuration to open the terminal with.
252#[derive(
253 Clone,
254 Debug,
255 Default,
256 Serialize,
257 Deserialize,
258 PartialEq,
259 Eq,
260 JsonSchema,
261 MergeFrom,
262 strum::EnumDiscriminants,
263)]
264#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
265#[serde(rename_all = "snake_case")]
266pub enum Shell {
267 /// Use the system's default terminal configuration in /etc/passwd
268 #[default]
269 System,
270 /// Use a specific program with no arguments.
271 Program(String),
272 /// Use a specific program with arguments.
273 WithArguments {
274 /// The program to run.
275 program: String,
276 /// The arguments to pass to the program.
277 args: Vec<String>,
278 /// An optional string to override the title of the terminal tab
279 title_override: Option<String>,
280 },
281}
282
283#[derive(
284 Clone,
285 Debug,
286 Serialize,
287 Deserialize,
288 PartialEq,
289 Eq,
290 JsonSchema,
291 MergeFrom,
292 strum::EnumDiscriminants,
293)]
294#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
295#[serde(rename_all = "snake_case")]
296pub enum WorkingDirectory {
297 /// Use the current file's directory, falling back to the project directory,
298 /// then the first project in the workspace.
299 CurrentFileDirectory,
300 /// Use the current file's project directory. Fallback to the
301 /// first project directory strategy if unsuccessful.
302 CurrentProjectDirectory,
303 /// Use the first project in this workspace's directory. Fallback to using
304 /// this platform's home directory.
305 FirstProjectDirectory,
306 /// Always use this platform's home directory (if it can be found).
307 AlwaysHome,
308 /// Always use a specific directory. This value will be shell expanded.
309 /// If this path is not a valid directory the terminal will default to
310 /// this platform's home directory (if it can be found).
311 Always { directory: String },
312}
313
314#[with_fallible_options]
315#[derive(
316 Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
317)]
318pub struct ScrollbarSettingsContent {
319 /// When to show the scrollbar in the terminal.
320 ///
321 /// Default: inherits editor scrollbar settings
322 pub show: Option<ShowScrollbar>,
323}
324
325#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
326#[serde(rename_all = "snake_case")]
327pub enum TerminalLineHeight {
328 /// Use a line height that's comfortable for reading, 1.618
329 #[default]
330 Comfortable,
331 /// Use a standard line height, 1.3. This option is useful for TUIs,
332 /// particularly if they use box characters
333 Standard,
334 /// Use a custom line height.
335 Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32),
336}
337
338impl TerminalLineHeight {
339 pub fn value(&self) -> f32 {
340 match self {
341 TerminalLineHeight::Comfortable => 1.618,
342 TerminalLineHeight::Standard => 1.3,
343 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
344 }
345 }
346}
347
348/// When to show the scrollbar.
349///
350/// Default: auto
351#[derive(
352 Copy,
353 Clone,
354 Debug,
355 Default,
356 Serialize,
357 Deserialize,
358 JsonSchema,
359 MergeFrom,
360 PartialEq,
361 Eq,
362 strum::VariantArray,
363 strum::VariantNames,
364)]
365#[serde(rename_all = "snake_case")]
366pub enum ShowScrollbar {
367 /// Show the scrollbar if there's important information or
368 /// follow the system's configured behavior.
369 #[default]
370 Auto,
371 /// Match the system's configured behavior.
372 System,
373 /// Always show the scrollbar.
374 Always,
375 /// Never show the scrollbar.
376 Never,
377}
378
379#[derive(
380 Clone,
381 Copy,
382 Debug,
383 Default,
384 Serialize,
385 Deserialize,
386 PartialEq,
387 Eq,
388 JsonSchema,
389 MergeFrom,
390 strum::VariantArray,
391 strum::VariantNames,
392)]
393#[serde(rename_all = "snake_case")]
394// todo() -> combine with CursorShape
395pub enum CursorShapeContent {
396 /// Cursor is a block like `█`.
397 #[default]
398 Block,
399 /// Cursor is an underscore like `_`.
400 Underline,
401 /// Cursor is a vertical bar like `⎸`.
402 Bar,
403 /// Cursor is a hollow box like `▯`.
404 Hollow,
405}
406
407#[derive(
408 Copy,
409 Clone,
410 Debug,
411 Serialize,
412 Deserialize,
413 PartialEq,
414 Eq,
415 JsonSchema,
416 MergeFrom,
417 strum::VariantArray,
418 strum::VariantNames,
419)]
420#[serde(rename_all = "snake_case")]
421pub enum TerminalBlink {
422 /// Never blink the cursor, ignoring the terminal mode.
423 Off,
424 /// Default the cursor blink to off, but allow the terminal to
425 /// set blinking.
426 TerminalControlled,
427 /// Always blink the cursor, ignoring the terminal mode.
428 On,
429}
430
431#[derive(
432 Clone,
433 Copy,
434 Debug,
435 Serialize,
436 Deserialize,
437 PartialEq,
438 Eq,
439 JsonSchema,
440 MergeFrom,
441 strum::VariantArray,
442 strum::VariantNames,
443)]
444#[serde(rename_all = "snake_case")]
445pub enum AlternateScroll {
446 On,
447 Off,
448}
449
450// Toolbar related settings
451#[with_fallible_options]
452#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
453pub struct TerminalToolbarContent {
454 /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
455 /// Only shown if the terminal title is not empty.
456 ///
457 /// The shell running in the terminal needs to be configured to emit the title.
458 /// Example: `echo -e "\e]2;New Title\007";`
459 ///
460 /// Default: true
461 pub breadcrumbs: Option<bool>,
462}
463
464#[derive(
465 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
466)]
467#[serde(rename_all = "snake_case")]
468pub enum CondaManager {
469 /// Automatically detect the conda manager
470 #[default]
471 Auto,
472 /// Use conda
473 Conda,
474 /// Use mamba
475 Mamba,
476 /// Use micromamba
477 Micromamba,
478}
479
480#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
481#[serde(rename_all = "snake_case")]
482pub enum VenvSettings {
483 #[default]
484 Off,
485 On {
486 /// Default directories to search for virtual environments, relative
487 /// to the current working directory. We recommend overriding this
488 /// in your project's settings, rather than globally.
489 activate_script: Option<ActivateScript>,
490 venv_name: Option<String>,
491 directories: Option<Vec<PathBuf>>,
492 /// Preferred Conda manager to use when activating Conda environments.
493 ///
494 /// Default: auto
495 conda_manager: Option<CondaManager>,
496 },
497}
498#[with_fallible_options]
499pub struct VenvSettingsContent<'a> {
500 pub activate_script: ActivateScript,
501 pub venv_name: &'a str,
502 pub directories: &'a [PathBuf],
503 pub conda_manager: CondaManager,
504}
505
506impl VenvSettings {
507 pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
508 match self {
509 VenvSettings::Off => None,
510 VenvSettings::On {
511 activate_script,
512 venv_name,
513 directories,
514 conda_manager,
515 } => Some(VenvSettingsContent {
516 activate_script: activate_script.unwrap_or(ActivateScript::Default),
517 venv_name: venv_name.as_deref().unwrap_or(""),
518 directories: directories.as_deref().unwrap_or(&[]),
519 conda_manager: conda_manager.unwrap_or(CondaManager::Auto),
520 }),
521 }
522 }
523}
524
525#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
526#[serde(untagged)]
527pub enum PathHyperlinkRegex {
528 SingleLine(String),
529 MultiLine(Vec<String>),
530}
531
532#[derive(
533 Copy,
534 Clone,
535 Debug,
536 Serialize,
537 Deserialize,
538 JsonSchema,
539 MergeFrom,
540 PartialEq,
541 Eq,
542 strum::VariantArray,
543 strum::VariantNames,
544)]
545#[serde(rename_all = "snake_case")]
546pub enum TerminalDockPosition {
547 Left,
548 Bottom,
549 Right,
550}
551
552#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
553#[serde(rename_all = "snake_case")]
554pub enum ActivateScript {
555 #[default]
556 Default,
557 Csh,
558 Fish,
559 Nushell,
560 PowerShell,
561 Pyenv,
562}
563
564#[cfg(test)]
565mod test {
566 use serde_json::json;
567
568 use crate::{ProjectSettingsContent, Shell};
569
570 #[test]
571 #[ignore]
572 fn test_project_settings() {
573 let project_content =
574 json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
575
576 let _user_content =
577 json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
578
579 let project_settings =
580 serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
581
582 assert_eq!(
583 project_settings.terminal.unwrap().shell,
584 Some(Shell::Program("/bin/project".to_owned()))
585 );
586 }
587}