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