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