terminal.rs

  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}