1use alacritty_terminal::vte::ansi::{
2 CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
3};
4use collections::HashMap;
5use gpui::{App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9pub use settings::AlternateScroll;
10use settings::{
11 CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
12 TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
13};
14use task::Shell;
15use theme::FontFamilyName;
16use util::MergeFrom;
17
18#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
19pub struct Toolbar {
20 pub breadcrumbs: bool,
21}
22
23#[derive(Clone, Debug, Deserialize)]
24pub struct TerminalSettings {
25 pub shell: Shell,
26 pub working_directory: WorkingDirectory,
27 pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
28 pub font_family: Option<FontFamilyName>,
29 pub font_fallbacks: Option<FontFallbacks>,
30 pub font_features: Option<FontFeatures>,
31 pub font_weight: Option<FontWeight>,
32 pub line_height: TerminalLineHeight,
33 pub env: HashMap<String, String>,
34 pub cursor_shape: Option<CursorShape>,
35 pub blinking: TerminalBlink,
36 pub alternate_scroll: AlternateScroll,
37 pub option_as_meta: bool,
38 pub copy_on_select: bool,
39 pub keep_selection_on_copy: 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 pub scrollbar: ScrollbarSettings,
48 pub minimum_contrast: f32,
49}
50
51#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
52pub struct ScrollbarSettings {
53 /// When to show the scrollbar in the terminal.
54 ///
55 /// Default: inherits editor scrollbar settings
56 pub show: Option<ShowScrollbar>,
57}
58
59fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
60 match shell {
61 settings::Shell::System => Shell::System,
62 settings::Shell::Program(program) => Shell::Program(program),
63 settings::Shell::WithArguments {
64 program,
65 args,
66 title_override,
67 } => Shell::WithArguments {
68 program,
69 args,
70 title_override,
71 },
72 }
73}
74
75impl settings::Settings for TerminalSettings {
76 fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
77 let content = content.terminal.clone().unwrap();
78 TerminalSettings {
79 shell: settings_shell_to_task_shell(content.shell.unwrap()),
80 working_directory: content.working_directory.unwrap(),
81 font_size: content.font_size.map(px),
82 font_family: content.font_family,
83 font_fallbacks: content.font_fallbacks.map(|fallbacks| {
84 FontFallbacks::from_fonts(
85 fallbacks
86 .into_iter()
87 .map(|family| family.0.to_string())
88 .collect(),
89 )
90 }),
91 font_features: content.font_features,
92 font_weight: content.font_weight.map(FontWeight),
93 line_height: content.line_height.unwrap(),
94 env: content.env.unwrap(),
95 cursor_shape: content.cursor_shape.map(Into::into),
96 blinking: content.blinking.unwrap(),
97 alternate_scroll: content.alternate_scroll.unwrap(),
98 option_as_meta: content.option_as_meta.unwrap(),
99 copy_on_select: content.copy_on_select.unwrap(),
100 keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
101 button: content.button.unwrap(),
102 dock: content.dock.unwrap(),
103 default_width: px(content.default_width.unwrap()),
104 default_height: px(content.default_height.unwrap()),
105 detect_venv: content.detect_venv.unwrap(),
106 max_scroll_history_lines: content.max_scroll_history_lines,
107 toolbar: Toolbar {
108 breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
109 },
110 scrollbar: ScrollbarSettings {
111 show: content.scrollbar.unwrap().show.flatten(),
112 },
113 minimum_contrast: content.minimum_contrast.unwrap(),
114 }
115 }
116
117 fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
118 let Some(content) = &content.terminal else {
119 return;
120 };
121 self.shell
122 .merge_from(&content.shell.clone().map(settings_shell_to_task_shell));
123 self.working_directory
124 .merge_from(&content.working_directory);
125 if let Some(font_size) = content.font_size.map(px) {
126 self.font_size = Some(font_size)
127 }
128 if let Some(font_family) = content.font_family.clone() {
129 self.font_family = Some(font_family);
130 }
131 if let Some(fallbacks) = content.font_fallbacks.clone() {
132 self.font_fallbacks = Some(FontFallbacks::from_fonts(
133 fallbacks
134 .into_iter()
135 .map(|family| family.0.to_string())
136 .collect(),
137 ))
138 }
139 if let Some(font_features) = content.font_features.clone() {
140 self.font_features = Some(font_features)
141 }
142 if let Some(font_weight) = content.font_weight {
143 self.font_weight = Some(FontWeight(font_weight));
144 }
145 self.line_height.merge_from(&content.line_height);
146 if let Some(env) = &content.env {
147 for (key, value) in env {
148 self.env.insert(key.clone(), value.clone());
149 }
150 }
151 if let Some(cursor_shape) = content.cursor_shape {
152 self.cursor_shape = Some(cursor_shape.into())
153 }
154 self.blinking.merge_from(&content.blinking);
155 self.alternate_scroll.merge_from(&content.alternate_scroll);
156 self.option_as_meta.merge_from(&content.option_as_meta);
157 self.copy_on_select.merge_from(&content.copy_on_select);
158 self.keep_selection_on_copy
159 .merge_from(&content.keep_selection_on_copy);
160 self.button.merge_from(&content.button);
161 self.dock.merge_from(&content.dock);
162 self.default_width
163 .merge_from(&content.default_width.map(px));
164 self.default_height
165 .merge_from(&content.default_height.map(px));
166 self.detect_venv.merge_from(&content.detect_venv);
167 if let Some(max_scroll_history_lines) = content.max_scroll_history_lines {
168 self.max_scroll_history_lines = Some(max_scroll_history_lines)
169 }
170 self.toolbar.breadcrumbs.merge_from(
171 &content
172 .toolbar
173 .as_ref()
174 .and_then(|toolbar| toolbar.breadcrumbs),
175 );
176 self.scrollbar.show.merge_from(
177 &content
178 .scrollbar
179 .as_ref()
180 .and_then(|scrollbar| scrollbar.show),
181 );
182 self.minimum_contrast.merge_from(&content.minimum_contrast);
183 }
184
185 fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
186 let mut default = TerminalSettingsContent::default();
187 let current = content.terminal.as_mut().unwrap_or(&mut default);
188 let name = |s| format!("terminal.integrated.{s}");
189
190 vscode.f32_setting(&name("fontSize"), &mut current.font_size);
191 if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
192 current.font_family = Some(FontFamilyName(font_family.into()));
193 }
194 vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
195 vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
196 vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
197 match vscode.read_bool(&name("cursorBlinking")) {
198 Some(true) => current.blinking = Some(TerminalBlink::On),
199 Some(false) => current.blinking = Some(TerminalBlink::Off),
200 None => {}
201 }
202 vscode.enum_setting(
203 &name("cursorStyle"),
204 &mut current.cursor_shape,
205 |s| match s {
206 "block" => Some(CursorShapeContent::Block),
207 "line" => Some(CursorShapeContent::Bar),
208 "underline" => Some(CursorShapeContent::Underline),
209 _ => None,
210 },
211 );
212 // they also have "none" and "outline" as options but just for the "Inactive" variant
213 if let Some(height) = vscode
214 .read_value(&name("lineHeight"))
215 .and_then(|v| v.as_f64())
216 {
217 current.line_height = Some(TerminalLineHeight::Custom(height as f32))
218 }
219
220 #[cfg(target_os = "windows")]
221 let platform = "windows";
222 #[cfg(target_os = "linux")]
223 let platform = "linux";
224 #[cfg(target_os = "macos")]
225 let platform = "osx";
226 #[cfg(target_os = "freebsd")]
227 let platform = "freebsd";
228
229 // TODO: handle arguments
230 let shell_name = format!("{platform}Exec");
231 if let Some(s) = vscode.read_string(&name(&shell_name)) {
232 current.shell = Some(settings::Shell::Program(s.to_owned()))
233 }
234
235 if let Some(env) = vscode
236 .read_value(&name(&format!("env.{platform}")))
237 .and_then(|v| v.as_object())
238 {
239 for (k, v) in env {
240 if v.is_null()
241 && let Some(zed_env) = current.env.as_mut()
242 {
243 zed_env.remove(k);
244 }
245 let Some(v) = v.as_str() else { continue };
246 if let Some(zed_env) = current.env.as_mut() {
247 zed_env.insert(k.clone(), v.to_owned());
248 } else {
249 current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
250 }
251 }
252 }
253 if content.terminal.is_none() && default != TerminalSettingsContent::default() {
254 content.terminal = Some(default)
255 }
256 }
257}
258
259#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
260#[serde(rename_all = "snake_case")]
261pub enum CursorShape {
262 /// Cursor is a block like `█`.
263 #[default]
264 Block,
265 /// Cursor is an underscore like `_`.
266 Underline,
267 /// Cursor is a vertical bar like `⎸`.
268 Bar,
269 /// Cursor is a hollow box like `▯`.
270 Hollow,
271}
272
273impl From<settings::CursorShapeContent> for CursorShape {
274 fn from(value: settings::CursorShapeContent) -> Self {
275 match value {
276 settings::CursorShapeContent::Block => CursorShape::Block,
277 settings::CursorShapeContent::Underline => CursorShape::Underline,
278 settings::CursorShapeContent::Bar => CursorShape::Bar,
279 settings::CursorShapeContent::Hollow => CursorShape::Hollow,
280 }
281 }
282}
283
284impl From<CursorShape> for AlacCursorShape {
285 fn from(value: CursorShape) -> Self {
286 match value {
287 CursorShape::Block => AlacCursorShape::Block,
288 CursorShape::Underline => AlacCursorShape::Underline,
289 CursorShape::Bar => AlacCursorShape::Beam,
290 CursorShape::Hollow => AlacCursorShape::HollowBlock,
291 }
292 }
293}
294
295impl From<CursorShape> for AlacCursorStyle {
296 fn from(value: CursorShape) -> Self {
297 AlacCursorStyle {
298 shape: value.into(),
299 blinking: false,
300 }
301 }
302}