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 activation_script if !activation_script.is_empty() => {
190 let activation_script = activation_script.join("; ");
191 let to_run = if let Some(command) = spawn_task.command {
192 let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
193 let args = spawn_task
194 .args
195 .iter()
196 .filter_map(|arg| shlex::try_quote(arg).ok());
197 command.into_iter().chain(args).join(" ")
198 } else {
199 format!("exec {shell} -l")
200 };
201 Shell::WithArguments {
202 program: shell,
203 args: vec![
204 "-c".to_owned(),
205 format!("{activation_script}; {to_run}",),
206 ],
207 title_override: None,
208 }
209 }
210 _ => {
211 if let Some(program) = spawn_task.command {
212 Shell::WithArguments {
213 program,
214 args: spawn_task.args,
215 title_override: None,
216 }
217 } else {
218 Shell::System
219 }
220 }
221 },
222 }
223 };
224 TerminalBuilder::new(
225 local_path.map(|path| path.to_path_buf()),
226 task_state,
227 shell,
228 env,
229 settings.cursor_shape.unwrap_or_default(),
230 settings.alternate_scroll,
231 settings.max_scroll_history_lines,
232 is_via_remote,
233 cx.entity_id().as_u64(),
234 Some(completion_tx),
235 cx,
236 activation_script,
237 )
238 .map(|builder| {
239 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
240
241 this.terminals
242 .local_handles
243 .push(terminal_handle.downgrade());
244
245 let id = terminal_handle.entity_id();
246 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
247 let handles = &mut project.terminals.local_handles;
248
249 if let Some(index) = handles
250 .iter()
251 .position(|terminal| terminal.entity_id() == id)
252 {
253 handles.remove(index);
254 cx.notify();
255 }
256 })
257 .detach();
258
259 terminal_handle
260 })
261 })?
262 })
263 }
264
265 pub fn create_terminal_shell(
266 &mut self,
267 cwd: Option<PathBuf>,
268 cx: &mut Context<Self>,
269 ) -> Task<Result<Entity<Terminal>>> {
270 let project_path_context = self
271 .active_entry()
272 .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
273 .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
274 .map(|worktree_id| ProjectPath {
275 worktree_id,
276 path: Arc::from(Path::new("")),
277 });
278 let path = cwd.map(|p| Arc::from(&*p));
279 let is_via_remote = self.remote_client.is_some();
280
281 let mut settings_location = None;
282 if let Some(path) = path.as_ref()
283 && let Some((worktree, _)) = self.find_worktree(path, cx)
284 {
285 settings_location = Some(SettingsLocation {
286 worktree_id: worktree.read(cx).id(),
287 path,
288 });
289 }
290 let settings = TerminalSettings::get(settings_location, cx).clone();
291 let detect_venv = settings.detect_venv.as_option().is_some();
292
293 // Start with the environment that we might have inherited from the Zed CLI.
294 let mut env = self
295 .environment
296 .read(cx)
297 .get_cli_environment()
298 .unwrap_or_default();
299 // Then extend it with the explicit env variables from the settings, so they take
300 // precedence.
301 env.extend(settings.env);
302
303 let local_path = if is_via_remote { None } else { path.clone() };
304
305 let toolchain = project_path_context
306 .filter(|_| detect_venv)
307 .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
308 let remote_client = self.remote_client.clone();
309 let shell = match &remote_client {
310 Some(remote_client) => remote_client
311 .read(cx)
312 .shell()
313 .unwrap_or_else(get_default_system_shell),
314 None => match &settings.shell {
315 Shell::Program(program) => program.clone(),
316 Shell::WithArguments {
317 program,
318 args: _,
319 title_override: _,
320 } => program.clone(),
321 Shell::System => get_system_shell(),
322 },
323 };
324
325 let lang_registry = self.languages.clone();
326 let fs = self.fs.clone();
327 cx.spawn(async move |project, cx| {
328 let activation_script = maybe!(async {
329 let toolchain = toolchain?.await?;
330 let language = lang_registry
331 .language_for_name(&toolchain.language_name.0)
332 .await
333 .ok();
334 let lister = language?.toolchain_lister();
335 Some(
336 lister?
337 .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
338 .await,
339 )
340 })
341 .await
342 .unwrap_or_default();
343 project.update(cx, move |this, cx| {
344 let shell = {
345 match remote_client {
346 Some(remote_client) => {
347 create_remote_shell(None, &mut env, path, remote_client, cx)?
348 }
349 None => settings.shell,
350 }
351 };
352 TerminalBuilder::new(
353 local_path.map(|path| path.to_path_buf()),
354 None,
355 shell,
356 env,
357 settings.cursor_shape.unwrap_or_default(),
358 settings.alternate_scroll,
359 settings.max_scroll_history_lines,
360 is_via_remote,
361 cx.entity_id().as_u64(),
362 None,
363 cx,
364 activation_script,
365 )
366 .map(|builder| {
367 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
368
369 this.terminals
370 .local_handles
371 .push(terminal_handle.downgrade());
372
373 let id = terminal_handle.entity_id();
374 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
375 let handles = &mut project.terminals.local_handles;
376
377 if let Some(index) = handles
378 .iter()
379 .position(|terminal| terminal.entity_id() == id)
380 {
381 handles.remove(index);
382 cx.notify();
383 }
384 })
385 .detach();
386
387 terminal_handle
388 })
389 })?
390 })
391 }
392
393 pub fn clone_terminal(
394 &mut self,
395 terminal: &Entity<Terminal>,
396 cx: &mut Context<'_, Project>,
397 cwd: impl FnOnce() -> Option<PathBuf>,
398 ) -> Result<Entity<Terminal>> {
399 terminal.read(cx).clone_builder(cx, cwd).map(|builder| {
400 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
401
402 self.terminals
403 .local_handles
404 .push(terminal_handle.downgrade());
405
406 let id = terminal_handle.entity_id();
407 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
408 let handles = &mut project.terminals.local_handles;
409
410 if let Some(index) = handles
411 .iter()
412 .position(|terminal| terminal.entity_id() == id)
413 {
414 handles.remove(index);
415 cx.notify();
416 }
417 })
418 .detach();
419
420 terminal_handle
421 })
422 }
423
424 pub fn terminal_settings<'a>(
425 &'a self,
426 path: &'a Option<PathBuf>,
427 cx: &'a App,
428 ) -> &'a TerminalSettings {
429 let mut settings_location = None;
430 if let Some(path) = path.as_ref()
431 && let Some((worktree, _)) = self.find_worktree(path, cx)
432 {
433 settings_location = Some(SettingsLocation {
434 worktree_id: worktree.read(cx).id(),
435 path,
436 });
437 }
438 TerminalSettings::get(settings_location, cx)
439 }
440
441 pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
442 let path = self.first_project_directory(cx);
443 let remote_client = self.remote_client.as_ref();
444 let settings = self.terminal_settings(&path, cx).clone();
445 let remote_shell = remote_client
446 .as_ref()
447 .and_then(|remote_client| remote_client.read(cx).shell());
448 let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
449 let (command, args) = builder.build(Some(command), &Vec::new());
450
451 let mut env = self
452 .environment
453 .read(cx)
454 .get_cli_environment()
455 .unwrap_or_default();
456 env.extend(settings.env);
457
458 match remote_client {
459 Some(remote_client) => {
460 let command_template =
461 remote_client
462 .read(cx)
463 .build_command(Some(command), &args, &env, None, None)?;
464 let mut command = std::process::Command::new(command_template.program);
465 command.args(command_template.args);
466 command.envs(command_template.env);
467 Ok(command)
468 }
469 None => {
470 let mut command = std::process::Command::new(command);
471 command.args(args);
472 command.envs(env);
473 if let Some(path) = path {
474 command.current_dir(path);
475 }
476 Ok(command)
477 }
478 }
479 }
480
481 pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
482 &self.terminals.local_handles
483 }
484}
485
486fn create_remote_shell(
487 spawn_command: Option<(&String, &Vec<String>)>,
488 env: &mut HashMap<String, String>,
489 working_directory: Option<Arc<Path>>,
490 remote_client: Entity<RemoteClient>,
491 cx: &mut App,
492) -> Result<Shell> {
493 // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
494 // to properly display colors.
495 // We do not have the luxury of assuming the host has it installed,
496 // so we set it to a default that does not break the highlighting via ssh.
497 env.entry("TERM".to_string())
498 .or_insert_with(|| "xterm-256color".to_string());
499
500 let (program, args) = match spawn_command {
501 Some((program, args)) => (Some(program.clone()), args),
502 None => (None, &Vec::new()),
503 };
504
505 let command = remote_client.read(cx).build_command(
506 program,
507 args.as_slice(),
508 env,
509 working_directory.map(|path| path.display().to_string()),
510 None,
511 )?;
512 *env = command.env;
513
514 log::debug!("Connecting to a remote server: {:?}", command.program);
515 let host = remote_client.read(cx).connection_options().host;
516
517 Ok(Shell::WithArguments {
518 program: command.program,
519 args: command.args,
520 title_override: Some(format!("{} — Terminal", host).into()),
521 })
522}