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