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