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}