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