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: None
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(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
157#[serde(rename_all = "snake_case")]
158pub enum WorkingDirectory {
159 /// Use the current file's project directory. Will Fallback to the
160 /// first project directory strategy if unsuccessful.
161 CurrentProjectDirectory,
162 /// Use the first project in this workspace's directory.
163 FirstProjectDirectory,
164 /// Always use this platform's home directory (if it can be found).
165 AlwaysHome,
166 /// Always use a specific directory. This value will be shell expanded.
167 /// If this path is not a valid directory the terminal will default to
168 /// this platform's home directory (if it can be found).
169 Always { directory: String },
170}
171
172#[skip_serializing_none]
173#[derive(
174 Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
175)]
176pub struct ScrollbarSettingsContent {
177 /// When to show the scrollbar in the terminal.
178 ///
179 /// Default: inherits editor scrollbar settings
180 pub show: Option<ShowScrollbar>,
181}
182
183#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
184#[serde(rename_all = "snake_case")]
185pub enum TerminalLineHeight {
186 /// Use a line height that's comfortable for reading, 1.618
187 #[default]
188 Comfortable,
189 /// Use a standard line height, 1.3. This option is useful for TUIs,
190 /// particularly if they use box characters
191 Standard,
192 /// Use a custom line height.
193 Custom(f32),
194}
195
196impl TerminalLineHeight {
197 pub fn value(&self) -> AbsoluteLength {
198 let value = match self {
199 TerminalLineHeight::Comfortable => 1.618,
200 TerminalLineHeight::Standard => 1.3,
201 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
202 };
203 px(value).into()
204 }
205}
206
207/// When to show the scrollbar.
208///
209/// Default: auto
210#[derive(
211 Copy,
212 Clone,
213 Debug,
214 Default,
215 Serialize,
216 Deserialize,
217 JsonSchema,
218 MergeFrom,
219 PartialEq,
220 Eq,
221 strum::VariantArray,
222 strum::VariantNames,
223)]
224#[serde(rename_all = "snake_case")]
225pub enum ShowScrollbar {
226 /// Show the scrollbar if there's important information or
227 /// follow the system's configured behavior.
228 #[default]
229 Auto,
230 /// Match the system's configured behavior.
231 System,
232 /// Always show the scrollbar.
233 Always,
234 /// Never show the scrollbar.
235 Never,
236}
237
238#[derive(
239 Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
240)]
241#[serde(rename_all = "snake_case")]
242// todo() -> combine with CursorShape
243pub enum CursorShapeContent {
244 /// Cursor is a block like `█`.
245 #[default]
246 Block,
247 /// Cursor is an underscore like `_`.
248 Underline,
249 /// Cursor is a vertical bar like `⎸`.
250 Bar,
251 /// Cursor is a hollow box like `▯`.
252 Hollow,
253}
254
255#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
256#[serde(rename_all = "snake_case")]
257pub enum TerminalBlink {
258 /// Never blink the cursor, ignoring the terminal mode.
259 Off,
260 /// Default the cursor blink to off, but allow the terminal to
261 /// set blinking.
262 TerminalControlled,
263 /// Always blink the cursor, ignoring the terminal mode.
264 On,
265}
266
267#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
268#[serde(rename_all = "snake_case")]
269pub enum AlternateScroll {
270 On,
271 Off,
272}
273
274// Toolbar related settings
275#[skip_serializing_none]
276#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
277pub struct TerminalToolbarContent {
278 /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
279 /// Only shown if the terminal title is not empty.
280 ///
281 /// The shell running in the terminal needs to be configured to emit the title.
282 /// Example: `echo -e "\e]2;New Title\007";`
283 ///
284 /// Default: true
285 pub breadcrumbs: Option<bool>,
286}
287
288#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
289#[serde(rename_all = "snake_case")]
290pub enum VenvSettings {
291 #[default]
292 Off,
293 On {
294 /// Default directories to search for virtual environments, relative
295 /// to the current working directory. We recommend overriding this
296 /// in your project's settings, rather than globally.
297 activate_script: Option<ActivateScript>,
298 venv_name: Option<String>,
299 directories: Option<Vec<PathBuf>>,
300 },
301}
302#[skip_serializing_none]
303pub struct VenvSettingsContent<'a> {
304 pub activate_script: ActivateScript,
305 pub venv_name: &'a str,
306 pub directories: &'a [PathBuf],
307}
308
309impl VenvSettings {
310 pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
311 match self {
312 VenvSettings::Off => None,
313 VenvSettings::On {
314 activate_script,
315 venv_name,
316 directories,
317 } => Some(VenvSettingsContent {
318 activate_script: activate_script.unwrap_or(ActivateScript::Default),
319 venv_name: venv_name.as_deref().unwrap_or(""),
320 directories: directories.as_deref().unwrap_or(&[]),
321 }),
322 }
323 }
324}
325
326#[derive(
327 Copy,
328 Clone,
329 Debug,
330 Serialize,
331 Deserialize,
332 JsonSchema,
333 MergeFrom,
334 PartialEq,
335 Eq,
336 strum::VariantArray,
337 strum::VariantNames,
338)]
339#[serde(rename_all = "snake_case")]
340pub enum TerminalDockPosition {
341 Left,
342 Bottom,
343 Right,
344}
345
346#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
347#[serde(rename_all = "snake_case")]
348pub enum ActivateScript {
349 #[default]
350 Default,
351 Csh,
352 Fish,
353 Nushell,
354 PowerShell,
355 Pyenv,
356}
357
358#[cfg(test)]
359mod test {
360 use serde_json::json;
361
362 use crate::{ProjectSettingsContent, Shell, UserSettingsContent};
363
364 #[test]
365 fn test_project_settings() {
366 let project_content =
367 json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
368
369 let user_content =
370 json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
371
372 let user_settings = serde_json::from_value::<UserSettingsContent>(user_content).unwrap();
373 let project_settings =
374 serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
375
376 assert_eq!(
377 user_settings.content.terminal.unwrap().project.shell,
378 Some(Shell::Program("/bin/user".to_owned()))
379 );
380 assert_eq!(user_settings.content.project.terminal, None);
381 assert_eq!(
382 project_settings.terminal.unwrap().shell,
383 Some(Shell::Program("/bin/project".to_owned()))
384 );
385 }
386}