1use anyhow::Result;
2use collections::HashMap;
3use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
4
5use futures::{FutureExt, future::Shared};
6use itertools::Itertools as _;
7use language::LanguageName;
8use remote::RemoteClient;
9use settings::{Settings, SettingsLocation};
10use smol::channel::bounded;
11use std::{
12 path::{Path, PathBuf},
13 sync::Arc,
14};
15use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
16use terminal::{
17 TaskState, TaskStatus, Terminal, TerminalBuilder, insert_zed_terminal_env,
18 terminal_settings::TerminalSettings,
19};
20use util::{
21 command::new_std_command, get_default_system_shell, get_system_shell, maybe, rel_path::RelPath,
22};
23
24use crate::{Project, ProjectPath};
25
26pub struct Terminals {
27 pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
28}
29
30impl Project {
31 pub fn active_entry_directory(&self, cx: &App) -> Option<PathBuf> {
32 let entry_id = self.active_entry()?;
33 let worktree = self.worktree_for_entry(entry_id, cx)?;
34 let worktree = worktree.read(cx);
35 let entry = worktree.entry_for_id(entry_id)?;
36
37 let absolute_path = worktree.absolutize(entry.path.as_ref());
38 if entry.is_dir() {
39 Some(absolute_path)
40 } else {
41 absolute_path.parent().map(|p| p.to_path_buf())
42 }
43 }
44
45 pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
46 self.active_entry()
47 .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
48 .into_iter()
49 .chain(self.worktrees(cx))
50 .find_map(|tree| tree.read(cx).root_dir())
51 }
52
53 pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> {
54 let worktree = self.worktrees(cx).next()?;
55 let worktree = worktree.read(cx);
56 if worktree.root_entry()?.is_dir() {
57 Some(worktree.abs_path().to_path_buf())
58 } else {
59 None
60 }
61 }
62
63 pub fn create_terminal_task(
64 &mut self,
65 spawn_task: SpawnInTerminal,
66 cx: &mut Context<Self>,
67 ) -> Task<Result<Entity<Terminal>>> {
68 let is_via_remote = self.remote_client.is_some();
69
70 let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
71 if is_via_remote {
72 Some(Arc::from(cwd.as_ref()))
73 } else {
74 let cwd = cwd.to_string_lossy();
75 let tilde_substituted = shellexpand::tilde(&cwd);
76 Some(Arc::from(Path::new(tilde_substituted.as_ref())))
77 }
78 } else {
79 self.active_project_directory(cx)
80 };
81
82 let mut settings_location = None;
83 if let Some(path) = path.as_ref()
84 && let Some((worktree, _)) = self.find_worktree(path, cx)
85 {
86 settings_location = Some(SettingsLocation {
87 worktree_id: worktree.read(cx).id(),
88 path: RelPath::empty(),
89 });
90 }
91 let settings = TerminalSettings::get(settings_location, cx).clone();
92 let detect_venv = settings.detect_venv.as_option().is_some();
93
94 let (completion_tx, completion_rx) = bounded(1);
95
96 let local_path = if is_via_remote { None } else { path.clone() };
97 let task_state = Some(TaskState {
98 spawned_task: spawn_task.clone(),
99 status: TaskStatus::Running,
100 completion_rx,
101 });
102 let remote_client = self.remote_client.clone();
103 let shell = match &remote_client {
104 Some(remote_client) => remote_client
105 .read(cx)
106 .shell()
107 .unwrap_or_else(get_default_system_shell),
108 None => get_system_shell(),
109 };
110 let path_style = self.path_style(cx);
111 let shell_kind = ShellKind::new(&shell, path_style.is_windows());
112
113 // Prepare a task for resolving the environment
114 let env_task =
115 self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx);
116
117 let project_path_contexts = self
118 .active_entry()
119 .and_then(|entry_id| self.path_for_entry(entry_id, cx))
120 .into_iter()
121 .chain(
122 self.visible_worktrees(cx)
123 .map(|wt| wt.read(cx).id())
124 .map(|worktree_id| ProjectPath {
125 worktree_id,
126 path: Arc::from(RelPath::empty()),
127 }),
128 );
129 let toolchains = project_path_contexts
130 .filter(|_| detect_venv)
131 .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
132 .collect::<Vec<_>>();
133 let lang_registry = self.languages.clone();
134 cx.spawn(async move |project, cx| {
135 let mut env = env_task.await.unwrap_or_default();
136 env.extend(settings.env);
137
138 let activation_script = maybe!(async {
139 for toolchain in toolchains {
140 let Some(toolchain) = toolchain.await else {
141 continue;
142 };
143 let language = lang_registry
144 .language_for_name(&toolchain.language_name.0)
145 .await
146 .ok();
147 let lister = language?.toolchain_lister()?;
148 let future =
149 cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx));
150 return Some(future.await);
151 }
152 None
153 })
154 .await
155 .unwrap_or_default();
156
157 let builder = project
158 .update(cx, move |_, cx| {
159 let format_to_run = || {
160 if let Some(command) = &spawn_task.command {
161 let command = shell_kind.prepend_command_prefix(command);
162 let command = shell_kind.try_quote_prefix_aware(&command);
163 let args = spawn_task
164 .args
165 .iter()
166 .filter_map(|arg| shell_kind.try_quote(&arg));
167
168 command.into_iter().chain(args).join(" ")
169 } else {
170 // todo: this breaks for remotes to windows
171 format!("exec {shell} -l")
172 }
173 };
174
175 let (shell, env) = {
176 env.extend(spawn_task.env);
177 match remote_client {
178 Some(remote_client) => match activation_script.clone() {
179 activation_script if !activation_script.is_empty() => {
180 let separator = shell_kind.sequential_commands_separator();
181 let activation_script =
182 activation_script.join(&format!("{separator} "));
183 let to_run = format_to_run();
184
185 let arg = format!("{activation_script}{separator} {to_run}");
186 let args = shell_kind.args_for_shell(true, arg);
187 let shell = remote_client
188 .read(cx)
189 .shell()
190 .unwrap_or_else(get_default_system_shell);
191
192 create_remote_shell(
193 Some((&shell, &args)),
194 env,
195 path,
196 remote_client,
197 cx,
198 )?
199 }
200 _ => create_remote_shell(
201 spawn_task
202 .command
203 .as_ref()
204 .map(|command| (command, &spawn_task.args)),
205 env,
206 path,
207 remote_client,
208 cx,
209 )?,
210 },
211 None => match activation_script.clone() {
212 activation_script if !activation_script.is_empty() => {
213 let separator = shell_kind.sequential_commands_separator();
214 let activation_script =
215 activation_script.join(&format!("{separator} "));
216 let to_run = format_to_run();
217
218 let arg = format!("{activation_script}{separator} {to_run}");
219 let args = shell_kind.args_for_shell(true, arg);
220
221 (
222 Shell::WithArguments {
223 program: shell,
224 args,
225 title_override: None,
226 },
227 env,
228 )
229 }
230 _ => (
231 if let Some(program) = spawn_task.command {
232 Shell::WithArguments {
233 program,
234 args: spawn_task.args,
235 title_override: None,
236 }
237 } else {
238 Shell::System
239 },
240 env,
241 ),
242 },
243 }
244 };
245 anyhow::Ok(TerminalBuilder::new(
246 local_path.map(|path| path.to_path_buf()),
247 task_state,
248 shell,
249 env,
250 settings.cursor_shape,
251 settings.alternate_scroll,
252 settings.max_scroll_history_lines,
253 settings.path_hyperlink_regexes,
254 settings.path_hyperlink_timeout_ms,
255 is_via_remote,
256 cx.entity_id().as_u64(),
257 Some(completion_tx),
258 cx,
259 activation_script,
260 path_style,
261 ))
262 })??
263 .await?;
264 project.update(cx, move |this, cx| {
265 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
266
267 this.terminals
268 .local_handles
269 .push(terminal_handle.downgrade());
270
271 let id = terminal_handle.entity_id();
272 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
273 let handles = &mut project.terminals.local_handles;
274
275 if let Some(index) = handles
276 .iter()
277 .position(|terminal| terminal.entity_id() == id)
278 {
279 handles.remove(index);
280 cx.notify();
281 }
282 })
283 .detach();
284
285 terminal_handle
286 })
287 })
288 }
289
290 pub fn create_terminal_shell(
291 &mut self,
292 cwd: Option<PathBuf>,
293 cx: &mut Context<Self>,
294 ) -> Task<Result<Entity<Terminal>>> {
295 self.create_terminal_shell_internal(cwd, false, cx)
296 }
297
298 /// Creates a local terminal even if the project is remote.
299 /// In remote projects: opens in Zed's launch directory (bypasses SSH).
300 /// In local projects: opens in the project directory (same as regular terminals).
301 pub fn create_local_terminal(
302 &mut self,
303 cx: &mut Context<Self>,
304 ) -> Task<Result<Entity<Terminal>>> {
305 let working_directory = if self.remote_client.is_some() {
306 // Remote project: don't use remote paths, let shell use Zed's cwd
307 None
308 } else {
309 // Local project: use project directory like normal terminals
310 self.active_project_directory(cx).map(|p| p.to_path_buf())
311 };
312 self.create_terminal_shell_internal(working_directory, true, cx)
313 }
314
315 /// Internal method for creating terminal shells.
316 /// If force_local is true, creates a local terminal even if the project has a remote client.
317 /// This allows "breaking out" to a local shell in remote projects.
318 fn create_terminal_shell_internal(
319 &mut self,
320 cwd: Option<PathBuf>,
321 force_local: bool,
322 cx: &mut Context<Self>,
323 ) -> Task<Result<Entity<Terminal>>> {
324 let path = cwd.map(|p| Arc::from(&*p));
325 let is_via_remote = !force_local && self.remote_client.is_some();
326
327 let mut settings_location = None;
328 if let Some(path) = path.as_ref()
329 && let Some((worktree, _)) = self.find_worktree(path, cx)
330 {
331 settings_location = Some(SettingsLocation {
332 worktree_id: worktree.read(cx).id(),
333 path: RelPath::empty(),
334 });
335 }
336 let settings = TerminalSettings::get(settings_location, cx).clone();
337 let detect_venv = settings.detect_venv.as_option().is_some();
338 let local_path = if is_via_remote { None } else { path.clone() };
339
340 let project_path_contexts = self
341 .active_entry()
342 .and_then(|entry_id| self.path_for_entry(entry_id, cx))
343 .into_iter()
344 .chain(
345 self.visible_worktrees(cx)
346 .map(|wt| wt.read(cx).id())
347 .map(|worktree_id| ProjectPath {
348 worktree_id,
349 path: RelPath::empty().into(),
350 }),
351 );
352 let toolchains = project_path_contexts
353 .filter(|_| detect_venv)
354 .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
355 .collect::<Vec<_>>();
356 let remote_client = if force_local {
357 None
358 } else {
359 self.remote_client.clone()
360 };
361 let shell = match &remote_client {
362 Some(remote_client) => remote_client
363 .read(cx)
364 .shell()
365 .unwrap_or_else(get_default_system_shell),
366 None => settings.shell.program(),
367 };
368 let env_shell = match &remote_client {
369 Some(_) => shell.clone(),
370 None => get_system_shell(),
371 };
372
373 let path_style = self.path_style(cx);
374
375 // Prepare a task for resolving the environment
376 let env_task =
377 self.resolve_directory_environment(&env_shell, path.clone(), remote_client.clone(), cx);
378
379 let lang_registry = self.languages.clone();
380 cx.spawn(async move |project, cx| {
381 let shell_kind = ShellKind::new(&shell, path_style.is_windows());
382 let mut env = env_task.await.unwrap_or_default();
383 env.extend(settings.env);
384
385 let activation_script = maybe!(async {
386 for toolchain in toolchains {
387 let Some(toolchain) = toolchain.await else {
388 continue;
389 };
390 let language = lang_registry
391 .language_for_name(&toolchain.language_name.0)
392 .await
393 .ok();
394 let lister = language?.toolchain_lister()?;
395 let future =
396 cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx));
397 return Some(future.await);
398 }
399 None
400 })
401 .await
402 .unwrap_or_default();
403
404 let builder = project
405 .update(cx, move |_, cx| {
406 let (shell, env) = {
407 match remote_client {
408 Some(remote_client) => {
409 create_remote_shell(None, env, path, remote_client, cx)?
410 }
411 None => (settings.shell, env),
412 }
413 };
414 anyhow::Ok(TerminalBuilder::new(
415 local_path.map(|path| path.to_path_buf()),
416 None,
417 shell,
418 env,
419 settings.cursor_shape,
420 settings.alternate_scroll,
421 settings.max_scroll_history_lines,
422 settings.path_hyperlink_regexes,
423 settings.path_hyperlink_timeout_ms,
424 is_via_remote,
425 cx.entity_id().as_u64(),
426 None,
427 cx,
428 activation_script,
429 path_style,
430 ))
431 })??
432 .await?;
433 project.update(cx, move |this, cx| {
434 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
435
436 this.terminals
437 .local_handles
438 .push(terminal_handle.downgrade());
439
440 let id = terminal_handle.entity_id();
441 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
442 let handles = &mut project.terminals.local_handles;
443
444 if let Some(index) = handles
445 .iter()
446 .position(|terminal| terminal.entity_id() == id)
447 {
448 handles.remove(index);
449 cx.notify();
450 }
451 })
452 .detach();
453
454 terminal_handle
455 })
456 })
457 }
458
459 pub fn clone_terminal(
460 &mut self,
461 terminal: &Entity<Terminal>,
462 cx: &mut Context<'_, Project>,
463 cwd: Option<PathBuf>,
464 ) -> Task<Result<Entity<Terminal>>> {
465 // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
466 // For now, create a new shell instead.
467 if terminal.read(cx).task().is_some() {
468 return self.create_terminal_shell(cwd, cx);
469 }
470 let local_path = if self.is_via_remote_server() {
471 None
472 } else {
473 cwd
474 };
475
476 let builder = terminal.read(cx).clone_builder(cx, local_path);
477 cx.spawn(async |project, cx| {
478 let terminal = builder.await?;
479 project.update(cx, |project, cx| {
480 let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
481
482 project
483 .terminals
484 .local_handles
485 .push(terminal_handle.downgrade());
486
487 let id = terminal_handle.entity_id();
488 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
489 let handles = &mut project.terminals.local_handles;
490
491 if let Some(index) = handles
492 .iter()
493 .position(|terminal| terminal.entity_id() == id)
494 {
495 handles.remove(index);
496 cx.notify();
497 }
498 })
499 .detach();
500
501 terminal_handle
502 })
503 })
504 }
505
506 pub fn terminal_settings<'a>(
507 &'a self,
508 path: &'a Option<PathBuf>,
509 cx: &'a App,
510 ) -> &'a TerminalSettings {
511 let mut settings_location = None;
512 if let Some(path) = path.as_ref()
513 && let Some((worktree, _)) = self.find_worktree(path, cx)
514 {
515 settings_location = Some(SettingsLocation {
516 worktree_id: worktree.read(cx).id(),
517 path: RelPath::empty(),
518 });
519 }
520 TerminalSettings::get(settings_location, cx)
521 }
522
523 pub fn exec_in_shell(
524 &self,
525 command: String,
526 cx: &mut Context<Self>,
527 ) -> Task<Result<smol::process::Command>> {
528 let path = self.first_project_directory(cx);
529 let remote_client = self.remote_client.clone();
530 let settings = self.terminal_settings(&path, cx).clone();
531 let shell = remote_client
532 .as_ref()
533 .and_then(|remote_client| remote_client.read(cx).shell())
534 .map(Shell::Program)
535 .unwrap_or(Shell::System);
536 let is_windows = self.path_style(cx).is_windows();
537 let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
538 let (command, args) = builder.build(Some(command), &Vec::new());
539
540 let env_task = self.resolve_directory_environment(
541 &shell.program(),
542 path.as_ref().map(|p| Arc::from(&**p)),
543 remote_client.clone(),
544 cx,
545 );
546
547 cx.spawn(async move |project, cx| {
548 let mut env = env_task.await.unwrap_or_default();
549 env.extend(settings.env);
550
551 project.update(cx, move |_, cx| {
552 match remote_client {
553 Some(remote_client) => {
554 let command_template = remote_client.read(cx).build_command(
555 Some(command),
556 &args,
557 &env,
558 None,
559 None,
560 )?;
561 let mut command = new_std_command(command_template.program);
562 command.args(command_template.args);
563 command.envs(command_template.env);
564 Ok(command)
565 }
566 None => {
567 let mut command = new_std_command(command);
568 command.args(args);
569 command.envs(env);
570 if let Some(path) = path {
571 command.current_dir(path);
572 }
573 Ok(command)
574 }
575 }
576 .map(|mut process| {
577 util::set_pre_exec_to_start_new_session(&mut process);
578 smol::process::Command::from(process)
579 })
580 })?
581 })
582 }
583
584 pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
585 &self.terminals.local_handles
586 }
587
588 fn resolve_directory_environment(
589 &self,
590 shell: &str,
591 path: Option<Arc<Path>>,
592 remote_client: Option<Entity<RemoteClient>>,
593 cx: &mut App,
594 ) -> Shared<Task<Option<HashMap<String, String>>>> {
595 if let Some(path) = &path {
596 let shell = Shell::Program(shell.to_string());
597 self.environment
598 .update(cx, |project_env, cx| match &remote_client {
599 Some(remote_client) => project_env.remote_directory_environment(
600 &shell,
601 path.clone(),
602 remote_client.clone(),
603 cx,
604 ),
605 None => project_env.local_directory_environment(&shell, path.clone(), cx),
606 })
607 } else {
608 Task::ready(None).shared()
609 }
610 }
611}
612
613fn create_remote_shell(
614 spawn_command: Option<(&String, &Vec<String>)>,
615 mut env: HashMap<String, String>,
616 working_directory: Option<Arc<Path>>,
617 remote_client: Entity<RemoteClient>,
618 cx: &mut App,
619) -> Result<(Shell, HashMap<String, String>)> {
620 insert_zed_terminal_env(&mut env, &release_channel::AppVersion::global(cx));
621
622 let (program, args) = match spawn_command {
623 Some((program, args)) => (Some(program.clone()), args),
624 None => (None, &Vec::new()),
625 };
626
627 let command = remote_client.read(cx).build_command(
628 program,
629 args.as_slice(),
630 &env,
631 working_directory.map(|path| path.display().to_string()),
632 None,
633 )?;
634
635 log::debug!("Connecting to a remote server: {:?}", command.program);
636 let host = remote_client.read(cx).connection_options().display_name();
637
638 Ok((
639 Shell::WithArguments {
640 program: command.program,
641 args: command.args,
642 title_override: Some(format!("{} — Terminal", host)),
643 },
644 command.env,
645 ))
646}