1use crate::Project;
2use collections::HashMap;
3use gpui::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel};
4use settings::{Settings, SettingsLocation};
5use smol::channel::bounded;
6use std::path::{Path, PathBuf};
7use task::SpawnInTerminal;
8use terminal::{
9 terminal_settings::{self, Shell, TerminalSettings, VenvSettingsContent},
10 TaskState, TaskStatus, Terminal, TerminalBuilder,
11};
12use util::ResultExt;
13
14// #[cfg(target_os = "macos")]
15// use std::os::unix::ffi::OsStrExt;
16
17pub struct Terminals {
18 pub(crate) local_handles: Vec<WeakModel<terminal::Terminal>>,
19}
20
21impl Project {
22 pub fn create_terminal(
23 &mut self,
24 working_directory: Option<PathBuf>,
25 spawn_task: Option<SpawnInTerminal>,
26 window: AnyWindowHandle,
27 cx: &mut ModelContext<Self>,
28 ) -> anyhow::Result<Model<Terminal>> {
29 anyhow::ensure!(
30 !self.is_remote(),
31 "creating terminals as a guest is not supported yet"
32 );
33
34 // used only for TerminalSettings::get
35 let worktree = {
36 let terminal_cwd = working_directory.as_deref();
37 let task_cwd = spawn_task
38 .as_ref()
39 .and_then(|spawn_task| spawn_task.cwd.as_deref());
40
41 terminal_cwd
42 .and_then(|terminal_cwd| self.find_local_worktree(terminal_cwd, cx))
43 .or_else(|| task_cwd.and_then(|spawn_cwd| self.find_local_worktree(spawn_cwd, cx)))
44 };
45
46 let settings_location = worktree.as_ref().map(|(worktree, path)| SettingsLocation {
47 worktree_id: worktree.read(cx).id().to_usize(),
48 path,
49 });
50
51 let is_terminal = spawn_task.is_none();
52 let settings = TerminalSettings::get(settings_location, cx);
53 let python_settings = settings.detect_venv.clone();
54 let (completion_tx, completion_rx) = bounded(1);
55
56 let mut env = settings.env.clone();
57 // Alacritty uses parent project's working directory when no working directory is provided
58 // https://github.com/alacritty/alacritty/blob/fd1a3cc79192d1d03839f0fd8c72e1f8d0fce42e/extra/man/alacritty.5.scd?plain=1#L47-L52
59
60 let venv_base_directory = working_directory
61 .as_deref()
62 .unwrap_or_else(|| Path::new(""));
63
64 let (spawn_task, shell) = if let Some(spawn_task) = spawn_task {
65 log::debug!("Spawning task: {spawn_task:?}");
66 env.extend(spawn_task.env);
67 // Activate minimal Python virtual environment
68 if let Some(python_settings) = &python_settings.as_option() {
69 self.set_python_venv_path_for_tasks(python_settings, venv_base_directory, &mut env);
70 }
71 (
72 Some(TaskState {
73 id: spawn_task.id,
74 full_label: spawn_task.full_label,
75 label: spawn_task.label,
76 command_label: spawn_task.command_label,
77 status: TaskStatus::Running,
78 completion_rx,
79 }),
80 Shell::WithArguments {
81 program: spawn_task.command,
82 args: spawn_task.args,
83 },
84 )
85 } else {
86 (None, settings.shell.clone())
87 };
88
89 let terminal = TerminalBuilder::new(
90 working_directory.clone(),
91 spawn_task,
92 shell,
93 env,
94 Some(settings.blinking.clone()),
95 settings.alternate_scroll,
96 settings.max_scroll_history_lines,
97 window,
98 completion_tx,
99 )
100 .map(|builder| {
101 let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
102
103 self.terminals
104 .local_handles
105 .push(terminal_handle.downgrade());
106
107 let id = terminal_handle.entity_id();
108 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
109 let handles = &mut project.terminals.local_handles;
110
111 if let Some(index) = handles
112 .iter()
113 .position(|terminal| terminal.entity_id() == id)
114 {
115 handles.remove(index);
116 cx.notify();
117 }
118 })
119 .detach();
120
121 // if the terminal is not a task, activate full Python virtual environment
122 if is_terminal {
123 if let Some(python_settings) = &python_settings.as_option() {
124 if let Some(activate_script_path) =
125 self.find_activate_script_path(python_settings, venv_base_directory)
126 {
127 self.activate_python_virtual_environment(
128 Project::get_activate_command(python_settings),
129 activate_script_path,
130 &terminal_handle,
131 cx,
132 );
133 }
134 }
135 }
136 terminal_handle
137 });
138
139 terminal
140 }
141
142 pub fn find_activate_script_path(
143 &mut self,
144 settings: &VenvSettingsContent,
145 venv_base_directory: &Path,
146 ) -> Option<PathBuf> {
147 let activate_script_name = match settings.activate_script {
148 terminal_settings::ActivateScript::Default => "activate",
149 terminal_settings::ActivateScript::Csh => "activate.csh",
150 terminal_settings::ActivateScript::Fish => "activate.fish",
151 terminal_settings::ActivateScript::Nushell => "activate.nu",
152 };
153
154 settings
155 .directories
156 .into_iter()
157 .find_map(|virtual_environment_name| {
158 let path = venv_base_directory
159 .join(virtual_environment_name)
160 .join("bin")
161 .join(activate_script_name);
162 path.exists().then_some(path)
163 })
164 }
165
166 pub fn set_python_venv_path_for_tasks(
167 &mut self,
168 settings: &VenvSettingsContent,
169 venv_base_directory: &Path,
170 env: &mut HashMap<String, String>,
171 ) {
172 let activate_path = settings
173 .directories
174 .into_iter()
175 .find_map(|virtual_environment_name| {
176 let path = venv_base_directory.join(virtual_environment_name);
177 path.exists().then_some(path)
178 });
179
180 if let Some(path) = activate_path {
181 // Some tools use VIRTUAL_ENV to detect the virtual environment
182 env.insert(
183 "VIRTUAL_ENV".to_string(),
184 path.to_string_lossy().to_string(),
185 );
186
187 let path_bin = path.join("bin");
188 // We need to set the PATH to include the virtual environment's bin directory
189 if let Some(paths) = std::env::var_os("PATH") {
190 let paths = std::iter::once(path_bin).chain(std::env::split_paths(&paths));
191 if let Some(new_path) = std::env::join_paths(paths).log_err() {
192 env.insert("PATH".to_string(), new_path.to_string_lossy().to_string());
193 }
194 } else {
195 env.insert(
196 "PATH".to_string(),
197 path.join("bin").to_string_lossy().to_string(),
198 );
199 }
200 }
201 }
202
203 fn get_activate_command(settings: &VenvSettingsContent) -> &'static str {
204 match settings.activate_script {
205 terminal_settings::ActivateScript::Nushell => "overlay use",
206 _ => "source",
207 }
208 }
209
210 fn activate_python_virtual_environment(
211 &mut self,
212 activate_command: &'static str,
213 activate_script: PathBuf,
214 terminal_handle: &Model<Terminal>,
215 cx: &mut ModelContext<Project>,
216 ) {
217 // Paths are not strings so we need to jump through some hoops to format the command without `format!`
218 let mut command = Vec::from(activate_command.as_bytes());
219 command.push(b' ');
220 // Wrapping path in double quotes to catch spaces in folder name
221 command.extend_from_slice(b"\"");
222 command.extend_from_slice(activate_script.as_os_str().as_encoded_bytes());
223 command.extend_from_slice(b"\"");
224 command.push(b'\n');
225
226 terminal_handle.update(cx, |this, _| this.input_bytes(command));
227 }
228
229 pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal::Terminal>> {
230 &self.terminals.local_handles
231 }
232}
233
234// TODO: Add a few tests for adding and removing terminal tabs