1use collections::HashMap;
2use gpui::{px, AbsoluteLength, AppContext, FontFeatures, FontWeight, Pixels};
3use schemars::{
4 gen::SchemaGenerator,
5 schema::{InstanceType, RootSchema, Schema, SchemaObject},
6 JsonSchema,
7};
8use serde_derive::{Deserialize, Serialize};
9use serde_json::Value;
10use settings::{SettingsJsonSchemaParams, SettingsSources};
11use std::path::PathBuf;
12
13#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum TerminalDockPosition {
16 Left,
17 Bottom,
18 Right,
19}
20
21#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
22pub struct Toolbar {
23 pub title: bool,
24}
25
26#[derive(Deserialize)]
27pub struct TerminalSettings {
28 pub shell: Shell,
29 pub working_directory: WorkingDirectory,
30 pub font_size: Option<Pixels>,
31 pub font_family: Option<String>,
32 pub line_height: TerminalLineHeight,
33 pub font_features: Option<FontFeatures>,
34 pub font_weight: Option<FontWeight>,
35 pub env: HashMap<String, String>,
36 pub blinking: TerminalBlink,
37 pub alternate_scroll: AlternateScroll,
38 pub option_as_meta: bool,
39 pub copy_on_select: bool,
40 pub button: bool,
41 pub dock: TerminalDockPosition,
42 pub default_width: Pixels,
43 pub default_height: Pixels,
44 pub detect_venv: VenvSettings,
45 pub max_scroll_history_lines: Option<usize>,
46 pub toolbar: Toolbar,
47}
48
49#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "snake_case")]
51pub enum VenvSettings {
52 #[default]
53 Off,
54 On {
55 /// Default directories to search for virtual environments, relative
56 /// to the current working directory. We recommend overriding this
57 /// in your project's settings, rather than globally.
58 activate_script: Option<ActivateScript>,
59 directories: Option<Vec<PathBuf>>,
60 },
61}
62
63pub struct VenvSettingsContent<'a> {
64 pub activate_script: ActivateScript,
65 pub directories: &'a [PathBuf],
66}
67
68impl VenvSettings {
69 pub fn as_option(&self) -> Option<VenvSettingsContent> {
70 match self {
71 VenvSettings::Off => None,
72 VenvSettings::On {
73 activate_script,
74 directories,
75 } => Some(VenvSettingsContent {
76 activate_script: activate_script.unwrap_or(ActivateScript::Default),
77 directories: directories.as_deref().unwrap_or(&[]),
78 }),
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
84#[serde(rename_all = "snake_case")]
85pub enum ActivateScript {
86 #[default]
87 Default,
88 Csh,
89 Fish,
90 Nushell,
91}
92
93#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
94pub struct TerminalSettingsContent {
95 /// What shell to use when opening a terminal.
96 ///
97 /// Default: system
98 pub shell: Option<Shell>,
99 /// What working directory to use when launching the terminal
100 ///
101 /// Default: current_project_directory
102 pub working_directory: Option<WorkingDirectory>,
103 /// Sets the terminal's font size.
104 ///
105 /// If this option is not included,
106 /// the terminal will default to matching the buffer's font size.
107 pub font_size: Option<f32>,
108 /// Sets the terminal's font family.
109 ///
110 /// If this option is not included,
111 /// the terminal will default to matching the buffer's font family.
112 pub font_family: Option<String>,
113 /// Sets the terminal's line height.
114 ///
115 /// Default: comfortable
116 pub line_height: Option<TerminalLineHeight>,
117 pub font_features: Option<FontFeatures>,
118 /// Sets the terminal's font weight in CSS weight units 0-900.
119 pub font_weight: Option<f32>,
120 /// Any key-value pairs added to this list will be added to the terminal's
121 /// environment. Use `:` to separate multiple values.
122 ///
123 /// Default: {}
124 pub env: Option<HashMap<String, String>>,
125 /// Sets the cursor blinking behavior in the terminal.
126 ///
127 /// Default: terminal_controlled
128 pub blinking: Option<TerminalBlink>,
129 /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
130 /// Alternate Scroll mode converts mouse scroll events into up / down key
131 /// presses when in the alternate screen (e.g. when running applications
132 /// like vim or less). The terminal can still set and unset this mode.
133 ///
134 /// Default: off
135 pub alternate_scroll: Option<AlternateScroll>,
136 /// Sets whether the option key behaves as the meta key.
137 ///
138 /// Default: false
139 pub option_as_meta: Option<bool>,
140 /// Whether or not selecting text in the terminal will automatically
141 /// copy to the system clipboard.
142 ///
143 /// Default: false
144 pub copy_on_select: Option<bool>,
145 /// Whether to show the terminal button in the status bar.
146 ///
147 /// Default: true
148 pub button: Option<bool>,
149 pub dock: Option<TerminalDockPosition>,
150 /// Default width when the terminal is docked to the left or right.
151 ///
152 /// Default: 640
153 pub default_width: Option<f32>,
154 /// Default height when the terminal is docked to the bottom.
155 ///
156 /// Default: 320
157 pub default_height: Option<f32>,
158 /// Activates the python virtual environment, if one is found, in the
159 /// terminal's working directory (as resolved by the working_directory
160 /// setting). Set this to "off" to disable this behavior.
161 ///
162 /// Default: on
163 pub detect_venv: Option<VenvSettings>,
164 /// The maximum number of lines to keep in the scrollback history.
165 /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
166 /// 0 disables the scrolling.
167 /// Existing terminals will not pick up this change until they are recreated.
168 /// 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.
169 ///
170 /// Default: 10_000
171 pub max_scroll_history_lines: Option<usize>,
172 /// Toolbar related settings
173 pub toolbar: Option<ToolbarContent>,
174}
175
176impl settings::Settings for TerminalSettings {
177 const KEY: Option<&'static str> = Some("terminal");
178
179 type FileContent = TerminalSettingsContent;
180
181 fn load(
182 sources: SettingsSources<Self::FileContent>,
183 _: &mut AppContext,
184 ) -> anyhow::Result<Self> {
185 sources.json_merge()
186 }
187
188 fn json_schema(
189 generator: &mut SchemaGenerator,
190 params: &SettingsJsonSchemaParams,
191 _: &AppContext,
192 ) -> RootSchema {
193 let mut root_schema = generator.root_schema_for::<Self::FileContent>();
194 let available_fonts = params
195 .font_names
196 .iter()
197 .cloned()
198 .map(Value::String)
199 .collect();
200 let fonts_schema = SchemaObject {
201 instance_type: Some(InstanceType::String.into()),
202 enum_values: Some(available_fonts),
203 ..Default::default()
204 };
205 root_schema
206 .definitions
207 .extend([("FontFamilies".into(), fonts_schema.into())]);
208 root_schema
209 .schema
210 .object
211 .as_mut()
212 .unwrap()
213 .properties
214 .extend([(
215 "font_family".to_owned(),
216 Schema::new_ref("#/definitions/FontFamilies".into()),
217 )]);
218
219 root_schema
220 }
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
224#[serde(rename_all = "snake_case")]
225pub enum TerminalLineHeight {
226 /// Use a line height that's comfortable for reading, 1.618
227 #[default]
228 Comfortable,
229 /// Use a standard line height, 1.3. This option is useful for TUIs,
230 /// particularly if they use box characters
231 Standard,
232 /// Use a custom line height.
233 Custom(f32),
234}
235
236impl TerminalLineHeight {
237 pub fn value(&self) -> AbsoluteLength {
238 let value = match self {
239 TerminalLineHeight::Comfortable => 1.618,
240 TerminalLineHeight::Standard => 1.3,
241 TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
242 };
243 px(value).into()
244 }
245}
246
247#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
248#[serde(rename_all = "snake_case")]
249pub enum TerminalBlink {
250 /// Never blink the cursor, ignoring the terminal mode.
251 Off,
252 /// Default the cursor blink to off, but allow the terminal to
253 /// set blinking.
254 TerminalControlled,
255 /// Always blink the cursor, ignoring the terminal mode.
256 On,
257}
258
259#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
260#[serde(rename_all = "snake_case")]
261pub enum Shell {
262 /// Use the system's default terminal configuration in /etc/passwd
263 System,
264 Program(String),
265 WithArguments {
266 program: String,
267 args: Vec<String>,
268 },
269}
270
271impl Shell {
272 pub fn retrieve_system_shell() -> Option<String> {
273 #[cfg(not(target_os = "windows"))]
274 {
275 use anyhow::Context;
276 use util::ResultExt;
277
278 return std::env::var("SHELL")
279 .context("Error finding SHELL in env.")
280 .log_err();
281 }
282 // `alacritty_terminal` uses this as default on Windows. See:
283 // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
284 #[cfg(target_os = "windows")]
285 return Some("powershell".to_owned());
286 }
287
288 /// Convert unix-shell variable syntax to windows-shell syntax.
289 /// `powershell` and `cmd` are considered valid here.
290 #[cfg(target_os = "windows")]
291 pub fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
292 match shell_type {
293 WindowsShellType::Powershell => to_powershell_variable(input),
294 WindowsShellType::Cmd => to_cmd_variable(input),
295 WindowsShellType::Other => input,
296 }
297 }
298
299 #[cfg(target_os = "windows")]
300 pub fn to_windows_shell_type(shell: &str) -> WindowsShellType {
301 if shell == "powershell" || shell.ends_with("powershell.exe") {
302 WindowsShellType::Powershell
303 } else if shell == "cmd" || shell.ends_with("cmd.exe") {
304 WindowsShellType::Cmd
305 } else {
306 // Someother shell detected, the user might install and use a
307 // unix-like shell.
308 WindowsShellType::Other
309 }
310 }
311}
312
313#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
314#[serde(rename_all = "snake_case")]
315pub enum AlternateScroll {
316 On,
317 Off,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
321#[serde(rename_all = "snake_case")]
322pub enum WorkingDirectory {
323 /// Use the current file's project directory. Will Fallback to the
324 /// first project directory strategy if unsuccessful.
325 CurrentProjectDirectory,
326 /// Use the first project in this workspace's directory.
327 FirstProjectDirectory,
328 /// Always use this platform's home directory (if it can be found).
329 AlwaysHome,
330 /// Always use a specific directory. This value will be shell expanded.
331 /// If this path is not a valid directory the terminal will default to
332 /// this platform's home directory (if it can be found).
333 Always { directory: String },
334}
335
336// Toolbar related settings
337#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
338pub struct ToolbarContent {
339 /// Whether to display the terminal title in its toolbar.
340 ///
341 /// Default: true
342 pub title: Option<bool>,
343}
344
345#[cfg(target_os = "windows")]
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum WindowsShellType {
348 Powershell,
349 Cmd,
350 Other,
351}
352
353/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
354#[inline]
355#[cfg(target_os = "windows")]
356fn to_cmd_variable(input: String) -> String {
357 if let Some(var_str) = input.strip_prefix("${") {
358 if var_str.find(':').is_none() {
359 // If the input starts with "${", remove the trailing "}"
360 format!("%{}%", &var_str[..var_str.len() - 1])
361 } else {
362 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
363 // which will result in the task failing to run in such cases.
364 input
365 }
366 } else if let Some(var_str) = input.strip_prefix('$') {
367 // If the input starts with "$", directly append to "$env:"
368 format!("%{}%", var_str)
369 } else {
370 // If no prefix is found, return the input as is
371 input
372 }
373}
374
375/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
376#[inline]
377#[cfg(target_os = "windows")]
378fn to_powershell_variable(input: String) -> String {
379 if let Some(var_str) = input.strip_prefix("${") {
380 if var_str.find(':').is_none() {
381 // If the input starts with "${", remove the trailing "}"
382 format!("$env:{}", &var_str[..var_str.len() - 1])
383 } else {
384 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
385 // which will result in the task failing to run in such cases.
386 input
387 }
388 } else if let Some(var_str) = input.strip_prefix('$') {
389 // If the input starts with "$", directly append to "$env:"
390 format!("$env:{}", var_str)
391 } else {
392 // If no prefix is found, return the input as is
393 input
394 }
395}