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}
183
184/// Shell configuration to open the terminal with.
185#[derive(
186 Clone,
187 Debug,
188 Default,
189 Serialize,
190 Deserialize,
191 PartialEq,
192 Eq,
193 JsonSchema,
194 MergeFrom,
195 strum::EnumDiscriminants,
196)]
197#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
198#[serde(rename_all = "snake_case")]
199pub enum Shell {
200 /// Use the system's default terminal configuration in /etc/passwd
201 #[default]
202 System,
203 /// Use a specific program with no arguments.
204 Program(String),
205 /// Use a specific program with arguments.
206 WithArguments {
207 /// The program to run.
208 program: String,
209 /// The arguments to pass to the program.
210 args: Vec<String>,
211 /// An optional string to override the title of the terminal tab
212 title_override: Option<String>,
213 },
214}
215
216#[derive(
217 Clone,
218 Debug,
219 Serialize,
220 Deserialize,
221 PartialEq,
222 Eq,
223 JsonSchema,
224 MergeFrom,
225 strum::EnumDiscriminants,
226)]
227#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
228#[serde(rename_all = "snake_case")]
229pub enum WorkingDirectory {
230 /// Use the current file's directory, falling back to the project directory,
231 /// then the first project in the workspace.
232 CurrentFileDirectory,
233 /// Use the current file's project directory. Fallback to the
234 /// first project directory strategy if unsuccessful.
235 CurrentProjectDirectory,
236 /// Use the first project in this workspace's directory. Fallback to using
237 /// this platform's home directory.
238 FirstProjectDirectory,
239 /// Always use this platform's home directory (if it can be found).
240 AlwaysHome,
241 /// Always use a specific directory. This value will be shell expanded.
242 /// If this path is not a valid directory the terminal will default to
243 /// this platform's home directory (if it can be found).
244 Always { directory: String },
245}
246
247#[with_fallible_options]
248#[derive(
249 Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
250)]
251pub struct ScrollbarSettingsContent {
252 /// When to show the scrollbar in the terminal.
253 ///
254 /// Default: inherits editor scrollbar settings
255 pub show: Option<ShowScrollbar>,
256}
257
258#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
259#[serde(rename_all = "snake_case")]
260pub enum TerminalLineHeight {
261 /// Use a line height that's comfortable for reading, 1.618
262 #[default]
263 Comfortable,
264 /// Use a standard line height, 1.3. This option is useful for TUIs,
265 /// particularly if they use box characters
266 Standard,
267 /// Use a custom line height.
268 Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32),
269}
270
271impl TerminalLineHeight {
272 pub fn value(&self) -> f32 {
273 match self {
274 TerminalLineHeight::Comfortable => 1.618,
275 TerminalLineHeight::Standard => 1.3,
276 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
277 }
278 }
279}
280
281/// When to show the scrollbar.
282///
283/// Default: auto
284#[derive(
285 Copy,
286 Clone,
287 Debug,
288 Default,
289 Serialize,
290 Deserialize,
291 JsonSchema,
292 MergeFrom,
293 PartialEq,
294 Eq,
295 strum::VariantArray,
296 strum::VariantNames,
297)]
298#[serde(rename_all = "snake_case")]
299pub enum ShowScrollbar {
300 /// Show the scrollbar if there's important information or
301 /// follow the system's configured behavior.
302 #[default]
303 Auto,
304 /// Match the system's configured behavior.
305 System,
306 /// Always show the scrollbar.
307 Always,
308 /// Never show the scrollbar.
309 Never,
310}
311
312#[derive(
313 Clone,
314 Copy,
315 Debug,
316 Default,
317 Serialize,
318 Deserialize,
319 PartialEq,
320 Eq,
321 JsonSchema,
322 MergeFrom,
323 strum::VariantArray,
324 strum::VariantNames,
325)]
326#[serde(rename_all = "snake_case")]
327// todo() -> combine with CursorShape
328pub enum CursorShapeContent {
329 /// Cursor is a block like `█`.
330 #[default]
331 Block,
332 /// Cursor is an underscore like `_`.
333 Underline,
334 /// Cursor is a vertical bar like `⎸`.
335 Bar,
336 /// Cursor is a hollow box like `▯`.
337 Hollow,
338}
339
340#[derive(
341 Copy,
342 Clone,
343 Debug,
344 Serialize,
345 Deserialize,
346 PartialEq,
347 Eq,
348 JsonSchema,
349 MergeFrom,
350 strum::VariantArray,
351 strum::VariantNames,
352)]
353#[serde(rename_all = "snake_case")]
354pub enum TerminalBlink {
355 /// Never blink the cursor, ignoring the terminal mode.
356 Off,
357 /// Default the cursor blink to off, but allow the terminal to
358 /// set blinking.
359 TerminalControlled,
360 /// Always blink the cursor, ignoring the terminal mode.
361 On,
362}
363
364#[derive(
365 Clone,
366 Copy,
367 Debug,
368 Serialize,
369 Deserialize,
370 PartialEq,
371 Eq,
372 JsonSchema,
373 MergeFrom,
374 strum::VariantArray,
375 strum::VariantNames,
376)]
377#[serde(rename_all = "snake_case")]
378pub enum AlternateScroll {
379 On,
380 Off,
381}
382
383// Toolbar related settings
384#[with_fallible_options]
385#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
386pub struct TerminalToolbarContent {
387 /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
388 /// Only shown if the terminal title is not empty.
389 ///
390 /// The shell running in the terminal needs to be configured to emit the title.
391 /// Example: `echo -e "\e]2;New Title\007";`
392 ///
393 /// Default: true
394 pub breadcrumbs: Option<bool>,
395}
396
397#[derive(
398 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
399)]
400#[serde(rename_all = "snake_case")]
401pub enum CondaManager {
402 /// Automatically detect the conda manager
403 #[default]
404 Auto,
405 /// Use conda
406 Conda,
407 /// Use mamba
408 Mamba,
409 /// Use micromamba
410 Micromamba,
411}
412
413#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
414#[serde(rename_all = "snake_case")]
415pub enum VenvSettings {
416 #[default]
417 Off,
418 On {
419 /// Default directories to search for virtual environments, relative
420 /// to the current working directory. We recommend overriding this
421 /// in your project's settings, rather than globally.
422 activate_script: Option<ActivateScript>,
423 venv_name: Option<String>,
424 directories: Option<Vec<PathBuf>>,
425 /// Preferred Conda manager to use when activating Conda environments.
426 ///
427 /// Default: auto
428 conda_manager: Option<CondaManager>,
429 },
430}
431#[with_fallible_options]
432pub struct VenvSettingsContent<'a> {
433 pub activate_script: ActivateScript,
434 pub venv_name: &'a str,
435 pub directories: &'a [PathBuf],
436 pub conda_manager: CondaManager,
437}
438
439impl VenvSettings {
440 pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
441 match self {
442 VenvSettings::Off => None,
443 VenvSettings::On {
444 activate_script,
445 venv_name,
446 directories,
447 conda_manager,
448 } => Some(VenvSettingsContent {
449 activate_script: activate_script.unwrap_or(ActivateScript::Default),
450 venv_name: venv_name.as_deref().unwrap_or(""),
451 directories: directories.as_deref().unwrap_or(&[]),
452 conda_manager: conda_manager.unwrap_or(CondaManager::Auto),
453 }),
454 }
455 }
456}
457
458#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
459#[serde(untagged)]
460pub enum PathHyperlinkRegex {
461 SingleLine(String),
462 MultiLine(Vec<String>),
463}
464
465#[derive(
466 Copy,
467 Clone,
468 Debug,
469 Serialize,
470 Deserialize,
471 JsonSchema,
472 MergeFrom,
473 PartialEq,
474 Eq,
475 strum::VariantArray,
476 strum::VariantNames,
477)]
478#[serde(rename_all = "snake_case")]
479pub enum TerminalDockPosition {
480 Left,
481 Bottom,
482 Right,
483}
484
485#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
486#[serde(rename_all = "snake_case")]
487pub enum ActivateScript {
488 #[default]
489 Default,
490 Csh,
491 Fish,
492 Nushell,
493 PowerShell,
494 Pyenv,
495}
496
497#[cfg(test)]
498mod test {
499 use serde_json::json;
500
501 use crate::{ProjectSettingsContent, Shell};
502
503 #[test]
504 #[ignore]
505 fn test_project_settings() {
506 let project_content =
507 json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
508
509 let _user_content =
510 json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
511
512 let project_settings =
513 serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
514
515 assert_eq!(
516 project_settings.terminal.unwrap().shell,
517 Some(Shell::Program("/bin/project".to_owned()))
518 );
519 }
520}