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