1use anyhow::Result;
2use collections::HashMap;
3use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
4use itertools::Itertools;
5use language::LanguageName;
6use remote::RemoteClient;
7use settings::{Settings, SettingsLocation};
8use smol::channel::bounded;
9use std::{
10 borrow::Cow,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14use task::{Shell, ShellBuilder, SpawnInTerminal};
15use terminal::{
16 TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
17};
18use util::{get_default_system_shell, get_system_shell, maybe};
19
20use crate::{Project, ProjectPath};
21
22pub struct Terminals {
23 pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
24}
25
26impl Project {
27 pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
28 self.active_entry()
29 .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
30 .into_iter()
31 .chain(self.worktrees(cx))
32 .find_map(|tree| tree.read(cx).root_dir())
33 }
34
35 pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> {
36 let worktree = self.worktrees(cx).next()?;
37 let worktree = worktree.read(cx);
38 if worktree.root_entry()?.is_dir() {
39 Some(worktree.abs_path().to_path_buf())
40 } else {
41 None
42 }
43 }
44
45 pub fn create_terminal_task(
46 &mut self,
47 spawn_task: SpawnInTerminal,
48 cx: &mut Context<Self>,
49 ) -> Task<Result<Entity<Terminal>>> {
50 let is_via_remote = self.remote_client.is_some();
51 let project_path_context = self
52 .active_entry()
53 .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
54 .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
55 .map(|worktree_id| ProjectPath {
56 worktree_id,
57 path: Arc::from(Path::new("")),
58 });
59
60 let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
61 if is_via_remote {
62 Some(Arc::from(cwd.as_ref()))
63 } else {
64 let cwd = cwd.to_string_lossy();
65 let tilde_substituted = shellexpand::tilde(&cwd);
66 Some(Arc::from(Path::new(tilde_substituted.as_ref())))
67 }
68 } else {
69 self.active_project_directory(cx)
70 };
71
72 let mut settings_location = None;
73 if let Some(path) = path.as_ref()
74 && let Some((worktree, _)) = self.find_worktree(path, cx)
75 {
76 settings_location = Some(SettingsLocation {
77 worktree_id: worktree.read(cx).id(),
78 path,
79 });
80 }
81 let settings = TerminalSettings::get(settings_location, cx).clone();
82 let detect_venv = settings.detect_venv.as_option().is_some();
83
84 let (completion_tx, completion_rx) = bounded(1);
85
86 // Start with the environment that we might have inherited from the Zed CLI.
87 let mut env = self
88 .environment
89 .read(cx)
90 .get_cli_environment()
91 .unwrap_or_default();
92 // Then extend it with the explicit env variables from the settings, so they take
93 // precedence.
94 env.extend(settings.env);
95
96 let local_path = if is_via_remote { None } else { path.clone() };
97 let task_state = Some(TaskState {
98 id: spawn_task.id,
99 full_label: spawn_task.full_label,
100 label: spawn_task.label,
101 command_label: spawn_task.command_label,
102 hide: spawn_task.hide,
103 status: TaskStatus::Running,
104 show_summary: spawn_task.show_summary,
105 show_command: spawn_task.show_command,
106 show_rerun: spawn_task.show_rerun,
107 completion_rx,
108 });
109 let remote_client = self.remote_client.clone();
110 let shell = match &remote_client {
111 Some(remote_client) => remote_client
112 .read(cx)
113 .shell()
114 .unwrap_or_else(get_default_system_shell),
115 None => match &settings.shell {
116 Shell::Program(program) => program.clone(),
117 Shell::WithArguments {
118 program,
119 args: _,
120 title_override: _,
121 } => program.clone(),
122 Shell::System => get_system_shell(),
123 },
124 };
125
126 let toolchain = project_path_context
127 .filter(|_| detect_venv)
128 .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
129 let lang_registry = self.languages.clone();
130 let fs = self.fs.clone();
131 cx.spawn(async move |project, cx| {
132 let activation_script = maybe!(async {
133 let toolchain = toolchain?.await?;
134 lang_registry
135 .language_for_name(&toolchain.language_name.0)
136 .await
137 .ok()?
138 .toolchain_lister()?
139 .activation_script(&toolchain, fs.as_ref())
140 .await
141 })
142 .await;
143
144 project.update(cx, move |this, cx| {
145 let shell = {
146 env.extend(spawn_task.env);
147 match remote_client {
148 Some(remote_client) => create_remote_shell(
149 spawn_task
150 .command
151 .as_ref()
152 .map(|command| (command, &spawn_task.args)),
153 &mut env,
154 path,
155 remote_client,
156 activation_script.clone(),
157 cx,
158 )?,
159 None => match activation_script.clone() {
160 Some(activation_script) => {
161 let to_run = if let Some(command) = spawn_task.command {
162 let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
163 let args = spawn_task
164 .args
165 .iter()
166 .filter_map(|arg| shlex::try_quote(arg).ok());
167 command.into_iter().chain(args).join(" ")
168 } else {
169 format!("exec {shell} -l")
170 };
171 Shell::WithArguments {
172 program: get_default_system_shell(),
173 args: vec![
174 "-c".to_owned(),
175 format!("{activation_script}; {to_run}",),
176 ],
177 title_override: None,
178 }
179 }
180 None => {
181 if let Some(program) = spawn_task.command {
182 Shell::WithArguments {
183 program,
184 args: spawn_task.args,
185 title_override: None,
186 }
187 } else {
188 Shell::System
189 }
190 }
191 },
192 }
193 };
194 TerminalBuilder::new(
195 local_path.map(|path| path.to_path_buf()),
196 task_state,
197 shell,
198 env,
199 settings.cursor_shape.unwrap_or_default(),
200 settings.alternate_scroll,
201 settings.max_scroll_history_lines,
202 is_via_remote,
203 cx.entity_id().as_u64(),
204 Some(completion_tx),
205 cx,
206 activation_script,
207 )
208 .map(|builder| {
209 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
210
211 this.terminals
212 .local_handles
213 .push(terminal_handle.downgrade());
214
215 let id = terminal_handle.entity_id();
216 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
217 let handles = &mut project.terminals.local_handles;
218
219 if let Some(index) = handles
220 .iter()
221 .position(|terminal| terminal.entity_id() == id)
222 {
223 handles.remove(index);
224 cx.notify();
225 }
226 })
227 .detach();
228
229 terminal_handle
230 })
231 })?
232 })
233 }
234
235 pub fn create_terminal_shell(
236 &mut self,
237 cwd: Option<PathBuf>,
238 cx: &mut Context<Self>,
239 ) -> Task<Result<Entity<Terminal>>> {
240 let project_path_context = self
241 .active_entry()
242 .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
243 .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
244 .map(|worktree_id| ProjectPath {
245 worktree_id,
246 path: Arc::from(Path::new("")),
247 });
248 let path = cwd.map(|p| Arc::from(&*p));
249 let is_via_remote = self.remote_client.is_some();
250
251 let mut settings_location = None;
252 if let Some(path) = path.as_ref()
253 && let Some((worktree, _)) = self.find_worktree(path, cx)
254 {
255 settings_location = Some(SettingsLocation {
256 worktree_id: worktree.read(cx).id(),
257 path,
258 });
259 }
260 let settings = TerminalSettings::get(settings_location, cx).clone();
261 let detect_venv = settings.detect_venv.as_option().is_some();
262
263 // Start with the environment that we might have inherited from the Zed CLI.
264 let mut env = self
265 .environment
266 .read(cx)
267 .get_cli_environment()
268 .unwrap_or_default();
269 // Then extend it with the explicit env variables from the settings, so they take
270 // precedence.
271 env.extend(settings.env);
272
273 let local_path = if is_via_remote { None } else { path.clone() };
274
275 let toolchain = project_path_context
276 .filter(|_| detect_venv)
277 .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
278 let remote_client = self.remote_client.clone();
279 let shell = match &remote_client {
280 Some(remote_client) => remote_client
281 .read(cx)
282 .shell()
283 .unwrap_or_else(get_default_system_shell),
284 None => match &settings.shell {
285 Shell::Program(program) => program.clone(),
286 Shell::WithArguments {
287 program,
288 args: _,
289 title_override: _,
290 } => program.clone(),
291 Shell::System => get_system_shell(),
292 },
293 };
294
295 let lang_registry = self.languages.clone();
296 let fs = self.fs.clone();
297 cx.spawn(async move |project, cx| {
298 let activation_script = maybe!(async {
299 let toolchain = toolchain?.await?;
300 let language = lang_registry
301 .language_for_name(&toolchain.language_name.0)
302 .await
303 .ok();
304 let lister = language?.toolchain_lister();
305 lister?.activation_script(&toolchain, fs.as_ref()).await
306 })
307 .await;
308 project.update(cx, move |this, cx| {
309 let shell = {
310 match remote_client {
311 Some(remote_client) => create_remote_shell(
312 None,
313 &mut env,
314 path,
315 remote_client,
316 activation_script.clone(),
317 cx,
318 )?,
319 None => match activation_script.clone() {
320 Some(activation_script) => Shell::WithArguments {
321 program: get_default_system_shell(),
322 args: vec![
323 "-c".to_owned(),
324 format!("{activation_script}; exec {shell} -l",),
325 ],
326 title_override: Some(shell.into()),
327 },
328 None => settings.shell,
329 },
330 }
331 };
332 TerminalBuilder::new(
333 local_path.map(|path| path.to_path_buf()),
334 None,
335 shell,
336 env,
337 settings.cursor_shape.unwrap_or_default(),
338 settings.alternate_scroll,
339 settings.max_scroll_history_lines,
340 is_via_remote,
341 cx.entity_id().as_u64(),
342 None,
343 cx,
344 activation_script,
345 )
346 .map(|builder| {
347 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
348
349 this.terminals
350 .local_handles
351 .push(terminal_handle.downgrade());
352
353 let id = terminal_handle.entity_id();
354 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
355 let handles = &mut project.terminals.local_handles;
356
357 if let Some(index) = handles
358 .iter()
359 .position(|terminal| terminal.entity_id() == id)
360 {
361 handles.remove(index);
362 cx.notify();
363 }
364 })
365 .detach();
366
367 terminal_handle
368 })
369 })?
370 })
371 }
372
373 pub fn clone_terminal(
374 &mut self,
375 terminal: &Entity<Terminal>,
376 cx: &mut Context<'_, Project>,
377 cwd: impl FnOnce() -> Option<PathBuf>,
378 ) -> Result<Entity<Terminal>> {
379 terminal.read(cx).clone_builder(cx, cwd).map(|builder| {
380 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
381
382 self.terminals
383 .local_handles
384 .push(terminal_handle.downgrade());
385
386 let id = terminal_handle.entity_id();
387 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
388 let handles = &mut project.terminals.local_handles;
389
390 if let Some(index) = handles
391 .iter()
392 .position(|terminal| terminal.entity_id() == id)
393 {
394 handles.remove(index);
395 cx.notify();
396 }
397 })
398 .detach();
399
400 terminal_handle
401 })
402 }
403
404 pub fn terminal_settings<'a>(
405 &'a self,
406 path: &'a Option<PathBuf>,
407 cx: &'a App,
408 ) -> &'a TerminalSettings {
409 let mut settings_location = None;
410 if let Some(path) = path.as_ref()
411 && let Some((worktree, _)) = self.find_worktree(path, cx)
412 {
413 settings_location = Some(SettingsLocation {
414 worktree_id: worktree.read(cx).id(),
415 path,
416 });
417 }
418 TerminalSettings::get(settings_location, cx)
419 }
420
421 pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
422 let path = self.first_project_directory(cx);
423 let remote_client = self.remote_client.as_ref();
424 let settings = self.terminal_settings(&path, cx).clone();
425 let remote_shell = remote_client
426 .as_ref()
427 .and_then(|remote_client| remote_client.read(cx).shell());
428 let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
429 let (command, args) = builder.build(Some(command), &Vec::new());
430
431 let mut env = self
432 .environment
433 .read(cx)
434 .get_cli_environment()
435 .unwrap_or_default();
436 env.extend(settings.env);
437
438 match remote_client {
439 Some(remote_client) => {
440 let command_template = remote_client.read(cx).build_command(
441 Some(command),
442 &args,
443 &env,
444 None,
445 // todo
446 None,
447 None,
448 )?;
449 let mut command = std::process::Command::new(command_template.program);
450 command.args(command_template.args);
451 command.envs(command_template.env);
452 Ok(command)
453 }
454 None => {
455 let mut command = std::process::Command::new(command);
456 command.args(args);
457 command.envs(env);
458 if let Some(path) = path {
459 command.current_dir(path);
460 }
461 Ok(command)
462 }
463 }
464 }
465
466 pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
467 &self.terminals.local_handles
468 }
469}
470
471fn create_remote_shell(
472 spawn_command: Option<(&String, &Vec<String>)>,
473 env: &mut HashMap<String, String>,
474 working_directory: Option<Arc<Path>>,
475 remote_client: Entity<RemoteClient>,
476 activation_script: Option<String>,
477 cx: &mut App,
478) -> Result<Shell> {
479 // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
480 // to properly display colors.
481 // We do not have the luxury of assuming the host has it installed,
482 // so we set it to a default that does not break the highlighting via ssh.
483 env.entry("TERM".to_string())
484 .or_insert_with(|| "xterm-256color".to_string());
485
486 let (program, args) = match spawn_command {
487 Some((program, args)) => (Some(program.clone()), args),
488 None => (None, &Vec::new()),
489 };
490
491 let command = remote_client.read(cx).build_command(
492 program,
493 args.as_slice(),
494 env,
495 working_directory.map(|path| path.display().to_string()),
496 activation_script,
497 None,
498 )?;
499 *env = command.env;
500
501 log::debug!("Connecting to a remote server: {:?}", command.program);
502 let host = remote_client.read(cx).connection_options().host;
503
504 Ok(Shell::WithArguments {
505 program: command.program,
506 args: command.args,
507 title_override: Some(format!("{} — Terminal", host).into()),
508 })
509}