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}
 67
 68#[with_fallible_options]
 69#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 70pub struct TerminalSettingsContent {
 71    #[serde(flatten)]
 72    pub project: ProjectTerminalSettingsContent,
 73    /// Sets the terminal's font size.
 74    ///
 75    /// If this option is not included,
 76    /// the terminal will default to matching the buffer's font size.
 77    pub font_size: Option<FontSize>,
 78    /// Sets the terminal's font family.
 79    ///
 80    /// If this option is not included,
 81    /// the terminal will default to matching the buffer's font family.
 82    pub font_family: Option<FontFamilyName>,
 83
 84    /// Sets the terminal's font fallbacks.
 85    ///
 86    /// If this option is not included,
 87    /// the terminal will default to matching the buffer's font fallbacks.
 88    #[schemars(extend("uniqueItems" = true))]
 89    pub font_fallbacks: Option<Vec<FontFamilyName>>,
 90
 91    /// Sets the terminal's line height.
 92    ///
 93    /// Default: comfortable
 94    pub line_height: Option<TerminalLineHeight>,
 95    pub font_features: Option<FontFeaturesContent>,
 96    /// Sets the terminal's font weight in CSS weight units 0-900.
 97    pub font_weight: Option<FontWeightContent>,
 98    /// Default cursor shape for the terminal.
 99    /// Can be "bar", "block", "underline", or "hollow".
100    ///
101    /// Default: "block"
102    pub cursor_shape: Option<CursorShapeContent>,
103    /// Sets the cursor blinking behavior in the terminal.
104    ///
105    /// Default: terminal_controlled
106    pub blinking: Option<TerminalBlink>,
107    /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
108    /// Alternate Scroll mode converts mouse scroll events into up / down key
109    /// presses when in the alternate screen (e.g. when running applications
110    /// like vim or  less). The terminal can still set and unset this mode.
111    ///
112    /// Default: on
113    pub alternate_scroll: Option<AlternateScroll>,
114    /// Sets whether the option key behaves as the meta key.
115    ///
116    /// Default: false
117    pub option_as_meta: Option<bool>,
118    /// Whether or not selecting text in the terminal will automatically
119    /// copy to the system clipboard.
120    ///
121    /// Default: false
122    pub copy_on_select: Option<bool>,
123    /// Whether to keep the text selection after copying it to the clipboard.
124    ///
125    /// Default: true
126    pub keep_selection_on_copy: Option<bool>,
127    /// Whether to show the terminal button in the status bar.
128    ///
129    /// Default: true
130    pub button: Option<bool>,
131    pub dock: Option<TerminalDockPosition>,
132    /// Whether the terminal panel should use flexible (proportional) sizing.
133    ///
134    /// Default: true
135    pub flexible: Option<bool>,
136    /// Default width when the terminal is docked to the left or right.
137    ///
138    /// Default: 640
139    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
140    pub default_width: Option<f32>,
141    /// Default height when the terminal is docked to the bottom.
142    ///
143    /// Default: 320
144    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
145    pub default_height: Option<f32>,
146    /// The maximum number of lines to keep in the scrollback history.
147    /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
148    /// 0 disables the scrolling.
149    /// Existing terminals will not pick up this change until they are recreated.
150    /// 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.
151    ///
152    /// Default: 10_000
153    pub max_scroll_history_lines: Option<usize>,
154    /// The multiplier for scrolling with the mouse wheel.
155    ///
156    /// Default: 1.0
157    pub scroll_multiplier: Option<f32>,
158    /// Toolbar related settings
159    pub toolbar: Option<TerminalToolbarContent>,
160    /// Scrollbar-related settings
161    pub scrollbar: Option<ScrollbarSettingsContent>,
162    /// The minimum APCA perceptual contrast between foreground and background colors.
163    ///
164    /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
165    /// especially for dark mode. Values range from 0 to 106.
166    ///
167    /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
168    /// https://readtech.org/ARC/tests/bronze-simple-mode/
169    /// - 0: No contrast adjustment
170    /// - 45: Minimum for large fluent text (36px+)
171    /// - 60: Minimum for other content text
172    /// - 75: Minimum for body text
173    /// - 90: Preferred for body text
174    ///
175    /// Default: 45
176    #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
177    pub minimum_contrast: Option<f32>,
178    /// Whether to show a badge on the terminal panel icon with the count of open terminals.
179    ///
180    /// Default: false
181    pub show_count_badge: Option<bool>,
182    /// What to do when the `BEL` character (`\a`) is printed to terminal.
183    ///
184    /// Default: "system"
185    pub bell: Option<TerminalBell>,
186}
187
188/// Shell configuration to open the terminal with.
189#[derive(
190    Clone,
191    Debug,
192    Default,
193    Serialize,
194    Deserialize,
195    PartialEq,
196    Eq,
197    JsonSchema,
198    MergeFrom,
199    strum::EnumDiscriminants,
200)]
201#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
202#[serde(rename_all = "snake_case")]
203pub enum Shell {
204    /// Use the system's default terminal configuration in /etc/passwd
205    #[default]
206    System,
207    /// Use a specific program with no arguments.
208    Program(String),
209    /// Use a specific program with arguments.
210    WithArguments {
211        /// The program to run.
212        program: String,
213        /// The arguments to pass to the program.
214        args: Vec<String>,
215        /// An optional string to override the title of the terminal tab
216        title_override: Option<String>,
217    },
218}
219
220#[derive(
221    Clone,
222    Debug,
223    Serialize,
224    Deserialize,
225    PartialEq,
226    Eq,
227    JsonSchema,
228    MergeFrom,
229    strum::EnumDiscriminants,
230)]
231#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
232#[serde(rename_all = "snake_case")]
233pub enum WorkingDirectory {
234    /// Use the current file's directory, falling back to the project directory,
235    /// then the first project in the workspace.
236    CurrentFileDirectory,
237    /// Use the current file's project directory. Fallback to the
238    /// first project directory strategy if unsuccessful.
239    CurrentProjectDirectory,
240    /// Use the first project in this workspace's directory. Fallback to using
241    /// this platform's home directory.
242    FirstProjectDirectory,
243    /// Always use this platform's home directory (if it can be found).
244    AlwaysHome,
245    /// Always use a specific directory. This value will be shell expanded.
246    /// If this path is not a valid directory the terminal will default to
247    /// this platform's home directory  (if it can be found).
248    Always { directory: String },
249}
250
251#[with_fallible_options]
252#[derive(
253    Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
254)]
255pub struct ScrollbarSettingsContent {
256    /// When to show the scrollbar in the terminal.
257    ///
258    /// Default: inherits editor scrollbar settings
259    pub show: Option<ShowScrollbar>,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
263#[serde(rename_all = "snake_case")]
264pub enum TerminalLineHeight {
265    /// Use a line height that's comfortable for reading, 1.618
266    #[default]
267    Comfortable,
268    /// Use a standard line height, 1.3. This option is useful for TUIs,
269    /// particularly if they use box characters
270    Standard,
271    /// Use a custom line height.
272    Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32),
273}
274
275impl TerminalLineHeight {
276    pub fn value(&self) -> f32 {
277        match self {
278            TerminalLineHeight::Comfortable => 1.618,
279            TerminalLineHeight::Standard => 1.3,
280            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
281        }
282    }
283}
284
285/// When to show the scrollbar.
286///
287/// Default: auto
288#[derive(
289    Copy,
290    Clone,
291    Debug,
292    Default,
293    Serialize,
294    Deserialize,
295    JsonSchema,
296    MergeFrom,
297    PartialEq,
298    Eq,
299    strum::VariantArray,
300    strum::VariantNames,
301)]
302#[serde(rename_all = "snake_case")]
303pub enum ShowScrollbar {
304    /// Show the scrollbar if there's important information or
305    /// follow the system's configured behavior.
306    #[default]
307    Auto,
308    /// Match the system's configured behavior.
309    System,
310    /// Always show the scrollbar.
311    Always,
312    /// Never show the scrollbar.
313    Never,
314}
315
316#[derive(
317    Clone,
318    Copy,
319    Debug,
320    Default,
321    Serialize,
322    Deserialize,
323    PartialEq,
324    Eq,
325    JsonSchema,
326    MergeFrom,
327    strum::VariantArray,
328    strum::VariantNames,
329)]
330#[serde(rename_all = "snake_case")]
331// todo() -> combine with CursorShape
332pub enum CursorShapeContent {
333    /// Cursor is a block like `█`.
334    #[default]
335    Block,
336    /// Cursor is an underscore like `_`.
337    Underline,
338    /// Cursor is a vertical bar like `⎸`.
339    Bar,
340    /// Cursor is a hollow box like `▯`.
341    Hollow,
342}
343
344#[derive(
345    Copy,
346    Clone,
347    Debug,
348    Serialize,
349    Deserialize,
350    PartialEq,
351    Eq,
352    JsonSchema,
353    MergeFrom,
354    strum::VariantArray,
355    strum::VariantNames,
356)]
357#[serde(rename_all = "snake_case")]
358pub enum TerminalBlink {
359    /// Never blink the cursor, ignoring the terminal mode.
360    Off,
361    /// Default the cursor blink to off, but allow the terminal to
362    /// set blinking.
363    TerminalControlled,
364    /// Always blink the cursor, ignoring the terminal mode.
365    On,
366}
367
368#[derive(
369    Clone,
370    Copy,
371    Debug,
372    Serialize,
373    Deserialize,
374    PartialEq,
375    Eq,
376    JsonSchema,
377    MergeFrom,
378    strum::VariantArray,
379    strum::VariantNames,
380)]
381#[serde(rename_all = "snake_case")]
382pub enum AlternateScroll {
383    On,
384    Off,
385}
386
387// Toolbar related settings
388#[with_fallible_options]
389#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
390pub struct TerminalToolbarContent {
391    /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
392    /// Only shown if the terminal title is not empty.
393    ///
394    /// The shell running in the terminal needs to be configured to emit the title.
395    /// Example: `echo -e "\e]2;New Title\007";`
396    ///
397    /// Default: true
398    pub breadcrumbs: Option<bool>,
399}
400
401#[derive(
402    Copy,
403    Clone,
404    Debug,
405    Default,
406    PartialEq,
407    Eq,
408    Serialize,
409    Deserialize,
410    JsonSchema,
411    MergeFrom,
412    strum::VariantArray,
413    strum::VariantNames,
414)]
415#[serde(rename_all = "snake_case")]
416pub enum TerminalBell {
417    /// Play an OS-specific alert sound.
418    #[default]
419    System,
420    /// Do not play any sound.
421    Off,
422}
423
424#[derive(
425    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
426)]
427#[serde(rename_all = "snake_case")]
428pub enum CondaManager {
429    /// Automatically detect the conda manager
430    #[default]
431    Auto,
432    /// Use conda
433    Conda,
434    /// Use mamba
435    Mamba,
436    /// Use micromamba
437    Micromamba,
438}
439
440#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
441#[serde(rename_all = "snake_case")]
442pub enum VenvSettings {
443    #[default]
444    Off,
445    On {
446        /// Default directories to search for virtual environments, relative
447        /// to the current working directory. We recommend overriding this
448        /// in your project's settings, rather than globally.
449        activate_script: Option<ActivateScript>,
450        venv_name: Option<String>,
451        directories: Option<Vec<PathBuf>>,
452        /// Preferred Conda manager to use when activating Conda environments.
453        ///
454        /// Default: auto
455        conda_manager: Option<CondaManager>,
456    },
457}
458#[with_fallible_options]
459pub struct VenvSettingsContent<'a> {
460    pub activate_script: ActivateScript,
461    pub venv_name: &'a str,
462    pub directories: &'a [PathBuf],
463    pub conda_manager: CondaManager,
464}
465
466impl VenvSettings {
467    pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
468        match self {
469            VenvSettings::Off => None,
470            VenvSettings::On {
471                activate_script,
472                venv_name,
473                directories,
474                conda_manager,
475            } => Some(VenvSettingsContent {
476                activate_script: activate_script.unwrap_or(ActivateScript::Default),
477                venv_name: venv_name.as_deref().unwrap_or(""),
478                directories: directories.as_deref().unwrap_or(&[]),
479                conda_manager: conda_manager.unwrap_or(CondaManager::Auto),
480            }),
481        }
482    }
483}
484
485#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
486#[serde(untagged)]
487pub enum PathHyperlinkRegex {
488    SingleLine(String),
489    MultiLine(Vec<String>),
490}
491
492#[derive(
493    Copy,
494    Clone,
495    Debug,
496    Serialize,
497    Deserialize,
498    JsonSchema,
499    MergeFrom,
500    PartialEq,
501    Eq,
502    strum::VariantArray,
503    strum::VariantNames,
504)]
505#[serde(rename_all = "snake_case")]
506pub enum TerminalDockPosition {
507    Left,
508    Bottom,
509    Right,
510}
511
512#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
513#[serde(rename_all = "snake_case")]
514pub enum ActivateScript {
515    #[default]
516    Default,
517    Csh,
518    Fish,
519    Nushell,
520    PowerShell,
521    Pyenv,
522}
523
524#[cfg(test)]
525mod test {
526    use serde_json::json;
527
528    use crate::{ProjectSettingsContent, Shell};
529
530    #[test]
531    #[ignore]
532    fn test_project_settings() {
533        let project_content =
534            json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
535
536        let _user_content =
537            json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
538
539        let project_settings =
540            serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
541
542        assert_eq!(
543            project_settings.terminal.unwrap().shell,
544            Some(Shell::Program("/bin/project".to_owned()))
545        );
546    }
547}