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