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