terminals.rs

  1use crate::Project;
  2use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
  3use std::path::PathBuf;
  4use terminal::{Terminal, TerminalBuilder, TerminalSettings};
  5
  6#[cfg(target_os = "macos")]
  7use std::os::unix::ffi::OsStrExt;
  8
  9pub struct Terminals {
 10    pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
 11}
 12
 13impl Project {
 14    pub fn create_terminal(
 15        &mut self,
 16        working_directory: Option<PathBuf>,
 17        window: AnyWindowHandle,
 18        cx: &mut ModelContext<Self>,
 19    ) -> anyhow::Result<ModelHandle<Terminal>> {
 20        if self.is_remote() {
 21            return Err(anyhow::anyhow!(
 22                "creating terminals as a guest is not supported yet"
 23            ));
 24        } else {
 25            let settings = settings::get::<TerminalSettings>(cx);
 26
 27            let terminal = TerminalBuilder::new(
 28                working_directory.clone(),
 29                settings.shell.clone(),
 30                settings.env.clone(),
 31                Some(settings.blinking.clone()),
 32                settings.alternate_scroll,
 33                window,
 34            )
 35            .map(|builder| {
 36                let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));
 37
 38                self.terminals
 39                    .local_handles
 40                    .push(terminal_handle.downgrade());
 41
 42                let id = terminal_handle.id();
 43                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
 44                    let handles = &mut project.terminals.local_handles;
 45
 46                    if let Some(index) = handles.iter().position(|terminal| terminal.id() == id) {
 47                        handles.remove(index);
 48                        cx.notify();
 49                    }
 50                })
 51                .detach();
 52
 53                let setting = settings::get::<TerminalSettings>(cx);
 54
 55                if setting.automatically_activate_python_virtual_environment {
 56                    self.set_up_python_virtual_environment(&terminal_handle, cx);
 57                }
 58
 59                terminal_handle
 60            });
 61
 62            terminal
 63        }
 64    }
 65
 66    fn set_up_python_virtual_environment(
 67        &mut self,
 68        terminal_handle: &ModelHandle<Terminal>,
 69        cx: &mut ModelContext<Project>,
 70    ) {
 71        let virtual_environment = self.find_python_virtual_environment(cx);
 72        if let Some(virtual_environment) = virtual_environment {
 73            // Paths are not strings so we need to jump through some hoops to format the command without `format!`
 74            let mut command = Vec::from("source ".as_bytes());
 75            command.extend_from_slice(virtual_environment.as_os_str().as_bytes());
 76            command.push(b'\n');
 77
 78            terminal_handle.update(cx, |this, _| this.input_bytes(command));
 79        }
 80    }
 81
 82    pub fn find_python_virtual_environment(
 83        &mut self,
 84        cx: &mut ModelContext<Project>,
 85    ) -> Option<PathBuf> {
 86        const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"];
 87
 88        let worktree_paths = self
 89            .worktrees(cx)
 90            .map(|worktree| worktree.read(cx).abs_path());
 91
 92        for worktree_path in worktree_paths {
 93            for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES {
 94                let mut path = worktree_path.join(virtual_environment_name);
 95                path.push("bin/activate");
 96
 97                if path.exists() {
 98                    return Some(path);
 99                }
100            }
101        }
102
103        None
104    }
105
106    pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
107        &self.terminals.local_handles
108    }
109}
110
111// TODO: Add a few tests for adding and removing terminal tabs