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