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 is_windows = self.path_style(cx).is_windows();
95 let shell_kind = ShellKind::new(&shell, 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 return Some(
133 cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx)),
134 );
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 ))
245 })??
246 .await?;
247 project.update(cx, move |this, cx| {
248 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
249
250 this.terminals
251 .local_handles
252 .push(terminal_handle.downgrade());
253
254 let id = terminal_handle.entity_id();
255 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
256 let handles = &mut project.terminals.local_handles;
257
258 if let Some(index) = handles
259 .iter()
260 .position(|terminal| terminal.entity_id() == id)
261 {
262 handles.remove(index);
263 cx.notify();
264 }
265 })
266 .detach();
267
268 terminal_handle
269 })
270 })
271 }
272
273 pub fn create_terminal_shell(
274 &mut self,
275 cwd: Option<PathBuf>,
276 cx: &mut Context<Self>,
277 ) -> Task<Result<Entity<Terminal>>> {
278 self.create_terminal_shell_internal(cwd, false, cx)
279 }
280
281 /// Creates a local terminal even if the project is remote.
282 /// In remote projects: opens in Zed's launch directory (bypasses SSH).
283 /// In local projects: opens in the project directory (same as regular terminals).
284 pub fn create_local_terminal(
285 &mut self,
286 cx: &mut Context<Self>,
287 ) -> Task<Result<Entity<Terminal>>> {
288 let working_directory = if self.remote_client.is_some() {
289 // Remote project: don't use remote paths, let shell use Zed's cwd
290 None
291 } else {
292 // Local project: use project directory like normal terminals
293 self.active_project_directory(cx).map(|p| p.to_path_buf())
294 };
295 self.create_terminal_shell_internal(working_directory, true, cx)
296 }
297
298 /// Internal method for creating terminal shells.
299 /// If force_local is true, creates a local terminal even if the project has a remote client.
300 /// This allows "breaking out" to a local shell in remote projects.
301 fn create_terminal_shell_internal(
302 &mut self,
303 cwd: Option<PathBuf>,
304 force_local: bool,
305 cx: &mut Context<Self>,
306 ) -> Task<Result<Entity<Terminal>>> {
307 let path = cwd.map(|p| Arc::from(&*p));
308 let is_via_remote = !force_local && self.remote_client.is_some();
309
310 let mut settings_location = None;
311 if let Some(path) = path.as_ref()
312 && let Some((worktree, _)) = self.find_worktree(path, cx)
313 {
314 settings_location = Some(SettingsLocation {
315 worktree_id: worktree.read(cx).id(),
316 path: RelPath::empty(),
317 });
318 }
319 let settings = TerminalSettings::get(settings_location, cx).clone();
320 let detect_venv = settings.detect_venv.as_option().is_some();
321 let local_path = if is_via_remote { None } else { path.clone() };
322
323 let project_path_contexts = self
324 .active_entry()
325 .and_then(|entry_id| self.path_for_entry(entry_id, cx))
326 .into_iter()
327 .chain(
328 self.visible_worktrees(cx)
329 .map(|wt| wt.read(cx).id())
330 .map(|worktree_id| ProjectPath {
331 worktree_id,
332 path: RelPath::empty().into(),
333 }),
334 );
335 let toolchains = project_path_contexts
336 .filter(|_| detect_venv)
337 .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
338 .collect::<Vec<_>>();
339 let remote_client = if force_local {
340 None
341 } else {
342 self.remote_client.clone()
343 };
344 let shell = match &remote_client {
345 Some(remote_client) => remote_client
346 .read(cx)
347 .shell()
348 .unwrap_or_else(get_default_system_shell),
349 None => settings.shell.program(),
350 };
351
352 let is_windows = self.path_style(cx).is_windows();
353
354 // Prepare a task for resolving the environment
355 let env_task =
356 self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx);
357
358 let lang_registry = self.languages.clone();
359 cx.spawn(async move |project, cx| {
360 let shell_kind = ShellKind::new(&shell, is_windows);
361 let mut env = env_task.await.unwrap_or_default();
362 env.extend(settings.env);
363
364 let activation_script = maybe!(async {
365 for toolchain in toolchains {
366 let Some(toolchain) = toolchain.await else {
367 continue;
368 };
369 let language = lang_registry
370 .language_for_name(&toolchain.language_name.0)
371 .await
372 .ok();
373 let lister = language?.toolchain_lister()?;
374 return Some(
375 cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx)),
376 );
377 }
378 None
379 })
380 .await
381 .unwrap_or_default();
382
383 let builder = project
384 .update(cx, move |_, cx| {
385 let (shell, env) = {
386 match remote_client {
387 Some(remote_client) => {
388 create_remote_shell(None, env, path, remote_client, cx)?
389 }
390 None => (settings.shell, env),
391 }
392 };
393 anyhow::Ok(TerminalBuilder::new(
394 local_path.map(|path| path.to_path_buf()),
395 None,
396 shell,
397 env,
398 settings.cursor_shape,
399 settings.alternate_scroll,
400 settings.max_scroll_history_lines,
401 settings.path_hyperlink_regexes,
402 settings.path_hyperlink_timeout_ms,
403 is_via_remote,
404 cx.entity_id().as_u64(),
405 None,
406 cx,
407 activation_script,
408 ))
409 })??
410 .await?;
411 project.update(cx, move |this, cx| {
412 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
413
414 this.terminals
415 .local_handles
416 .push(terminal_handle.downgrade());
417
418 let id = terminal_handle.entity_id();
419 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
420 let handles = &mut project.terminals.local_handles;
421
422 if let Some(index) = handles
423 .iter()
424 .position(|terminal| terminal.entity_id() == id)
425 {
426 handles.remove(index);
427 cx.notify();
428 }
429 })
430 .detach();
431
432 terminal_handle
433 })
434 })
435 }
436
437 pub fn clone_terminal(
438 &mut self,
439 terminal: &Entity<Terminal>,
440 cx: &mut Context<'_, Project>,
441 cwd: Option<PathBuf>,
442 ) -> Task<Result<Entity<Terminal>>> {
443 // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
444 // For now, create a new shell instead.
445 if terminal.read(cx).task().is_some() {
446 return self.create_terminal_shell(cwd, cx);
447 }
448 let local_path = if self.is_via_remote_server() {
449 None
450 } else {
451 cwd
452 };
453
454 let builder = terminal.read(cx).clone_builder(cx, local_path);
455 cx.spawn(async |project, cx| {
456 let terminal = builder.await?;
457 project.update(cx, |project, cx| {
458 let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
459
460 project
461 .terminals
462 .local_handles
463 .push(terminal_handle.downgrade());
464
465 let id = terminal_handle.entity_id();
466 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
467 let handles = &mut project.terminals.local_handles;
468
469 if let Some(index) = handles
470 .iter()
471 .position(|terminal| terminal.entity_id() == id)
472 {
473 handles.remove(index);
474 cx.notify();
475 }
476 })
477 .detach();
478
479 terminal_handle
480 })
481 })
482 }
483
484 pub fn terminal_settings<'a>(
485 &'a self,
486 path: &'a Option<PathBuf>,
487 cx: &'a App,
488 ) -> &'a TerminalSettings {
489 let mut settings_location = None;
490 if let Some(path) = path.as_ref()
491 && let Some((worktree, _)) = self.find_worktree(path, cx)
492 {
493 settings_location = Some(SettingsLocation {
494 worktree_id: worktree.read(cx).id(),
495 path: RelPath::empty(),
496 });
497 }
498 TerminalSettings::get(settings_location, cx)
499 }
500
501 pub fn exec_in_shell(
502 &self,
503 command: String,
504 cx: &mut Context<Self>,
505 ) -> Task<Result<smol::process::Command>> {
506 let path = self.first_project_directory(cx);
507 let remote_client = self.remote_client.clone();
508 let settings = self.terminal_settings(&path, cx).clone();
509 let shell = remote_client
510 .as_ref()
511 .and_then(|remote_client| remote_client.read(cx).shell())
512 .map(Shell::Program)
513 .unwrap_or_else(|| settings.shell.clone());
514 let is_windows = self.path_style(cx).is_windows();
515 let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
516 let (command, args) = builder.build(Some(command), &Vec::new());
517
518 let env_task = self.resolve_directory_environment(
519 &shell.program(),
520 path.as_ref().map(|p| Arc::from(&**p)),
521 remote_client.clone(),
522 cx,
523 );
524
525 cx.spawn(async move |project, cx| {
526 let mut env = env_task.await.unwrap_or_default();
527 env.extend(settings.env);
528
529 project.update(cx, move |_, cx| {
530 match remote_client {
531 Some(remote_client) => {
532 let command_template = remote_client.read(cx).build_command(
533 Some(command),
534 &args,
535 &env,
536 None,
537 None,
538 )?;
539 let mut command = new_std_command(command_template.program);
540 command.args(command_template.args);
541 command.envs(command_template.env);
542 Ok(command)
543 }
544 None => {
545 let mut command = new_std_command(command);
546 command.args(args);
547 command.envs(env);
548 if let Some(path) = path {
549 command.current_dir(path);
550 }
551 Ok(command)
552 }
553 }
554 .map(|mut process| {
555 util::set_pre_exec_to_start_new_session(&mut process);
556 smol::process::Command::from(process)
557 })
558 })?
559 })
560 }
561
562 pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
563 &self.terminals.local_handles
564 }
565
566 fn resolve_directory_environment(
567 &self,
568 shell: &str,
569 path: Option<Arc<Path>>,
570 remote_client: Option<Entity<RemoteClient>>,
571 cx: &mut App,
572 ) -> Shared<Task<Option<HashMap<String, String>>>> {
573 if let Some(path) = &path {
574 let shell = Shell::Program(shell.to_string());
575 self.environment
576 .update(cx, |project_env, cx| match &remote_client {
577 Some(remote_client) => project_env.remote_directory_environment(
578 &shell,
579 path.clone(),
580 remote_client.clone(),
581 cx,
582 ),
583 None => project_env.local_directory_environment(&shell, path.clone(), cx),
584 })
585 } else {
586 Task::ready(None).shared()
587 }
588 }
589}
590
591fn create_remote_shell(
592 spawn_command: Option<(&String, &Vec<String>)>,
593 mut env: HashMap<String, String>,
594 working_directory: Option<Arc<Path>>,
595 remote_client: Entity<RemoteClient>,
596 cx: &mut App,
597) -> Result<(Shell, HashMap<String, String>)> {
598 insert_zed_terminal_env(&mut env, &release_channel::AppVersion::global(cx));
599
600 let (program, args) = match spawn_command {
601 Some((program, args)) => (Some(program.clone()), args),
602 None => (None, &Vec::new()),
603 };
604
605 let command = remote_client.read(cx).build_command(
606 program,
607 args.as_slice(),
608 &env,
609 working_directory.map(|path| path.display().to_string()),
610 None,
611 )?;
612
613 log::debug!("Connecting to a remote server: {:?}", command.program);
614 let host = remote_client.read(cx).connection_options().display_name();
615
616 Ok((
617 Shell::WithArguments {
618 program: command.program,
619 args: command.args,
620 title_override: Some(format!("{} — Terminal", host)),
621 },
622 command.env,
623 ))
624}