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;
16
17#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
18pub struct Toolbar {
19 pub breadcrumbs: bool,
20}
21
22#[derive(Clone, Debug, Deserialize)]
23pub struct TerminalSettings {
24 pub shell: Shell,
25 pub working_directory: WorkingDirectory,
26 pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
27 pub font_family: Option<FontFamilyName>,
28 pub font_fallbacks: Option<FontFallbacks>,
29 pub font_features: Option<FontFeatures>,
30 pub font_weight: Option<FontWeight>,
31 pub line_height: TerminalLineHeight,
32 pub env: HashMap<String, String>,
33 pub cursor_shape: Option<CursorShape>,
34 pub blinking: TerminalBlink,
35 pub alternate_scroll: AlternateScroll,
36 pub option_as_meta: bool,
37 pub copy_on_select: bool,
38 pub keep_selection_on_copy: bool,
39 pub button: bool,
40 pub dock: TerminalDockPosition,
41 pub default_width: Pixels,
42 pub default_height: Pixels,
43 pub detect_venv: VenvSettings,
44 pub max_scroll_history_lines: Option<usize>,
45 pub toolbar: Toolbar,
46 pub scrollbar: ScrollbarSettings,
47 pub minimum_contrast: f32,
48}
49
50#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
51pub struct ScrollbarSettings {
52 /// When to show the scrollbar in the terminal.
53 ///
54 /// Default: inherits editor scrollbar settings
55 pub show: Option<ShowScrollbar>,
56}
57
58fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
59 match shell {
60 settings::Shell::System => Shell::System,
61 settings::Shell::Program(program) => Shell::Program(program),
62 settings::Shell::WithArguments {
63 program,
64 args,
65 title_override,
66 } => Shell::WithArguments {
67 program,
68 args,
69 title_override,
70 },
71 }
72}
73
74impl settings::Settings for TerminalSettings {
75 fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
76 let content = content.terminal.clone().unwrap();
77 TerminalSettings {
78 shell: settings_shell_to_task_shell(content.shell.unwrap()),
79 working_directory: content.working_directory.unwrap(),
80 font_size: content.font_size.map(px),
81 font_family: content.font_family,
82 font_fallbacks: content.font_fallbacks.map(|fallbacks| {
83 FontFallbacks::from_fonts(
84 fallbacks
85 .into_iter()
86 .map(|family| family.0.to_string())
87 .collect(),
88 )
89 }),
90 font_features: content.font_features,
91 font_weight: content.font_weight.map(FontWeight),
92 line_height: content.line_height.unwrap(),
93 env: content.env.unwrap(),
94 cursor_shape: content.cursor_shape.map(Into::into),
95 blinking: content.blinking.unwrap(),
96 alternate_scroll: content.alternate_scroll.unwrap(),
97 option_as_meta: content.option_as_meta.unwrap(),
98 copy_on_select: content.copy_on_select.unwrap(),
99 keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
100 button: content.button.unwrap(),
101 dock: content.dock.unwrap(),
102 default_width: px(content.default_width.unwrap()),
103 default_height: px(content.default_height.unwrap()),
104 detect_venv: content.detect_venv.unwrap(),
105 max_scroll_history_lines: content.max_scroll_history_lines,
106 toolbar: Toolbar {
107 breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
108 },
109 scrollbar: ScrollbarSettings {
110 show: content.scrollbar.unwrap().show,
111 },
112 minimum_contrast: content.minimum_contrast.unwrap(),
113 }
114 }
115
116 fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
117 let mut default = TerminalSettingsContent::default();
118 let current = content.terminal.as_mut().unwrap_or(&mut default);
119 let name = |s| format!("terminal.integrated.{s}");
120
121 vscode.f32_setting(&name("fontSize"), &mut current.font_size);
122 if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
123 current.font_family = Some(FontFamilyName(font_family.into()));
124 }
125 vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
126 vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
127 vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
128 match vscode.read_bool(&name("cursorBlinking")) {
129 Some(true) => current.blinking = Some(TerminalBlink::On),
130 Some(false) => current.blinking = Some(TerminalBlink::Off),
131 None => {}
132 }
133 vscode.enum_setting(
134 &name("cursorStyle"),
135 &mut current.cursor_shape,
136 |s| match s {
137 "block" => Some(CursorShapeContent::Block),
138 "line" => Some(CursorShapeContent::Bar),
139 "underline" => Some(CursorShapeContent::Underline),
140 _ => None,
141 },
142 );
143 // they also have "none" and "outline" as options but just for the "Inactive" variant
144 if let Some(height) = vscode
145 .read_value(&name("lineHeight"))
146 .and_then(|v| v.as_f64())
147 {
148 current.line_height = Some(TerminalLineHeight::Custom(height as f32))
149 }
150
151 #[cfg(target_os = "windows")]
152 let platform = "windows";
153 #[cfg(target_os = "linux")]
154 let platform = "linux";
155 #[cfg(target_os = "macos")]
156 let platform = "osx";
157 #[cfg(target_os = "freebsd")]
158 let platform = "freebsd";
159
160 // TODO: handle arguments
161 let shell_name = format!("{platform}Exec");
162 if let Some(s) = vscode.read_string(&name(&shell_name)) {
163 current.shell = Some(settings::Shell::Program(s.to_owned()))
164 }
165
166 if let Some(env) = vscode
167 .read_value(&name(&format!("env.{platform}")))
168 .and_then(|v| v.as_object())
169 {
170 for (k, v) in env {
171 if v.is_null()
172 && let Some(zed_env) = current.env.as_mut()
173 {
174 zed_env.remove(k);
175 }
176 let Some(v) = v.as_str() else { continue };
177 if let Some(zed_env) = current.env.as_mut() {
178 zed_env.insert(k.clone(), v.to_owned());
179 } else {
180 current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
181 }
182 }
183 }
184 if content.terminal.is_none() && default != TerminalSettingsContent::default() {
185 content.terminal = Some(default)
186 }
187 }
188}
189
190#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
191#[serde(rename_all = "snake_case")]
192pub enum CursorShape {
193 /// Cursor is a block like `█`.
194 #[default]
195 Block,
196 /// Cursor is an underscore like `_`.
197 Underline,
198 /// Cursor is a vertical bar like `⎸`.
199 Bar,
200 /// Cursor is a hollow box like `▯`.
201 Hollow,
202}
203
204impl From<settings::CursorShapeContent> for CursorShape {
205 fn from(value: settings::CursorShapeContent) -> Self {
206 match value {
207 settings::CursorShapeContent::Block => CursorShape::Block,
208 settings::CursorShapeContent::Underline => CursorShape::Underline,
209 settings::CursorShapeContent::Bar => CursorShape::Bar,
210 settings::CursorShapeContent::Hollow => CursorShape::Hollow,
211 }
212 }
213}
214
215impl From<CursorShape> for AlacCursorShape {
216 fn from(value: CursorShape) -> Self {
217 match value {
218 CursorShape::Block => AlacCursorShape::Block,
219 CursorShape::Underline => AlacCursorShape::Underline,
220 CursorShape::Bar => AlacCursorShape::Beam,
221 CursorShape::Hollow => AlacCursorShape::HollowBlock,
222 }
223 }
224}
225
226impl From<CursorShape> for AlacCursorStyle {
227 fn from(value: CursorShape) -> Self {
228 AlacCursorStyle {
229 shape: value.into(),
230 blinking: false,
231 }
232 }
233}