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