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 merge_from::MergeFrom,
14};
15use task::Shell;
16use theme::FontFamilyName;
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_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
77 let user_content = content.terminal.clone().unwrap();
78 // Note: we allow a subset of "terminal" settings in the project files.
79 let mut project_content = user_content.project.clone();
80 project_content.merge_from_option(content.project.terminal.as_ref());
81 TerminalSettings {
82 shell: settings_shell_to_task_shell(project_content.shell.unwrap()),
83 working_directory: project_content.working_directory.unwrap(),
84 font_size: user_content.font_size.map(px),
85 font_family: user_content.font_family,
86 font_fallbacks: user_content.font_fallbacks.map(|fallbacks| {
87 FontFallbacks::from_fonts(
88 fallbacks
89 .into_iter()
90 .map(|family| family.0.to_string())
91 .collect(),
92 )
93 }),
94 font_features: user_content.font_features,
95 font_weight: user_content.font_weight.map(FontWeight),
96 line_height: user_content.line_height.unwrap(),
97 env: project_content.env.unwrap(),
98 cursor_shape: user_content.cursor_shape.map(Into::into),
99 blinking: user_content.blinking.unwrap(),
100 alternate_scroll: user_content.alternate_scroll.unwrap(),
101 option_as_meta: user_content.option_as_meta.unwrap(),
102 copy_on_select: user_content.copy_on_select.unwrap(),
103 keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(),
104 button: user_content.button.unwrap(),
105 dock: user_content.dock.unwrap(),
106 default_width: px(user_content.default_width.unwrap()),
107 default_height: px(user_content.default_height.unwrap()),
108 detect_venv: project_content.detect_venv.unwrap(),
109 max_scroll_history_lines: user_content.max_scroll_history_lines,
110 toolbar: Toolbar {
111 breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(),
112 },
113 scrollbar: ScrollbarSettings {
114 show: user_content.scrollbar.unwrap().show,
115 },
116 minimum_contrast: user_content.minimum_contrast.unwrap(),
117 }
118 }
119
120 fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
121 let mut default = TerminalSettingsContent::default();
122 let current = content.terminal.as_mut().unwrap_or(&mut default);
123 let name = |s| format!("terminal.integrated.{s}");
124
125 vscode.f32_setting(&name("fontSize"), &mut current.font_size);
126 if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
127 current.font_family = Some(FontFamilyName(font_family.into()));
128 }
129 vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
130 vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
131 vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
132 match vscode.read_bool(&name("cursorBlinking")) {
133 Some(true) => current.blinking = Some(TerminalBlink::On),
134 Some(false) => current.blinking = Some(TerminalBlink::Off),
135 None => {}
136 }
137 vscode.enum_setting(
138 &name("cursorStyle"),
139 &mut current.cursor_shape,
140 |s| match s {
141 "block" => Some(CursorShapeContent::Block),
142 "line" => Some(CursorShapeContent::Bar),
143 "underline" => Some(CursorShapeContent::Underline),
144 _ => None,
145 },
146 );
147 // they also have "none" and "outline" as options but just for the "Inactive" variant
148 if let Some(height) = vscode
149 .read_value(&name("lineHeight"))
150 .and_then(|v| v.as_f64())
151 {
152 current.line_height = Some(TerminalLineHeight::Custom(height as f32))
153 }
154
155 #[cfg(target_os = "windows")]
156 let platform = "windows";
157 #[cfg(target_os = "linux")]
158 let platform = "linux";
159 #[cfg(target_os = "macos")]
160 let platform = "osx";
161 #[cfg(target_os = "freebsd")]
162 let platform = "freebsd";
163
164 // TODO: handle arguments
165 let shell_name = format!("{platform}Exec");
166 if let Some(s) = vscode.read_string(&name(&shell_name)) {
167 current.project.shell = Some(settings::Shell::Program(s.to_owned()))
168 }
169
170 if let Some(env) = vscode
171 .read_value(&name(&format!("env.{platform}")))
172 .and_then(|v| v.as_object())
173 {
174 for (k, v) in env {
175 if v.is_null()
176 && let Some(zed_env) = current.project.env.as_mut()
177 {
178 zed_env.remove(k);
179 }
180 let Some(v) = v.as_str() else { continue };
181 if let Some(zed_env) = current.project.env.as_mut() {
182 zed_env.insert(k.clone(), v.to_owned());
183 } else {
184 current.project.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
185 }
186 }
187 }
188 if content.terminal.is_none() && default != TerminalSettingsContent::default() {
189 content.terminal = Some(default)
190 }
191 }
192}
193
194#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
195#[serde(rename_all = "snake_case")]
196pub enum CursorShape {
197 /// Cursor is a block like `█`.
198 #[default]
199 Block,
200 /// Cursor is an underscore like `_`.
201 Underline,
202 /// Cursor is a vertical bar like `⎸`.
203 Bar,
204 /// Cursor is a hollow box like `▯`.
205 Hollow,
206}
207
208impl From<settings::CursorShapeContent> for CursorShape {
209 fn from(value: settings::CursorShapeContent) -> Self {
210 match value {
211 settings::CursorShapeContent::Block => CursorShape::Block,
212 settings::CursorShapeContent::Underline => CursorShape::Underline,
213 settings::CursorShapeContent::Bar => CursorShape::Bar,
214 settings::CursorShapeContent::Hollow => CursorShape::Hollow,
215 }
216 }
217}
218
219impl From<CursorShape> for AlacCursorShape {
220 fn from(value: CursorShape) -> Self {
221 match value {
222 CursorShape::Block => AlacCursorShape::Block,
223 CursorShape::Underline => AlacCursorShape::Underline,
224 CursorShape::Bar => AlacCursorShape::Beam,
225 CursorShape::Hollow => AlacCursorShape::HollowBlock,
226 }
227 }
228}
229
230impl From<CursorShape> for AlacCursorStyle {
231 fn from(value: CursorShape) -> Self {
232 AlacCursorStyle {
233 shape: value.into(),
234 blinking: false,
235 }
236 }
237}