1use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
2
3use crate::{default_working_directory, TerminalView};
4use collections::{HashMap, HashSet};
5use db::kvp::KEY_VALUE_STORE;
6use futures::future::join_all;
7use gpui::{
8 actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter,
9 ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
10 Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
11};
12use itertools::Itertools;
13use project::{terminals::TerminalKind, Fs, ProjectEntryId};
14use search::{buffer_search::DivRegistrar, BufferSearchBar};
15use serde::{Deserialize, Serialize};
16use settings::Settings;
17use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
18use terminal::{
19 terminal_settings::{TerminalDockPosition, TerminalSettings},
20 Terminal,
21};
22use ui::{
23 h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, PopoverMenu, Selectable,
24 Tooltip,
25};
26use util::{ResultExt, TryFutureExt};
27use workspace::{
28 dock::{DockPosition, Panel, PanelEvent},
29 item::SerializableItem,
30 pane,
31 ui::IconName,
32 DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace,
33};
34
35use anyhow::Result;
36use zed_actions::InlineAssist;
37
38const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
39
40actions!(terminal_panel, [ToggleFocus]);
41
42pub fn init(cx: &mut AppContext) {
43 cx.observe_new_views(
44 |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
45 workspace.register_action(TerminalPanel::new_terminal);
46 workspace.register_action(TerminalPanel::open_terminal);
47 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
48 if workspace
49 .panel::<TerminalPanel>(cx)
50 .as_ref()
51 .is_some_and(|panel| panel.read(cx).enabled)
52 {
53 workspace.toggle_panel_focus::<TerminalPanel>(cx);
54 }
55 });
56 },
57 )
58 .detach();
59}
60
61pub struct TerminalPanel {
62 pane: View<Pane>,
63 fs: Arc<dyn Fs>,
64 workspace: WeakView<Workspace>,
65 width: Option<Pixels>,
66 height: Option<Pixels>,
67 pending_serialization: Task<Option<()>>,
68 pending_terminals_to_add: usize,
69 _subscriptions: Vec<Subscription>,
70 deferred_tasks: HashMap<TaskId, Task<()>>,
71 enabled: bool,
72 assistant_enabled: bool,
73 assistant_tab_bar_button: Option<AnyView>,
74}
75
76impl TerminalPanel {
77 fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
78 let pane = cx.new_view(|cx| {
79 let mut pane = Pane::new(
80 workspace.weak_handle(),
81 workspace.project().clone(),
82 Default::default(),
83 None,
84 NewTerminal.boxed_clone(),
85 cx,
86 );
87 pane.set_can_split(false, cx);
88 pane.set_can_navigate(false, cx);
89 pane.display_nav_history_buttons(None);
90 pane.set_should_display_tab_bar(|_| true);
91
92 let workspace = workspace.weak_handle();
93 pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
94 if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
95 let item = if &tab.pane == cx.view() {
96 pane.item_for_index(tab.ix)
97 } else {
98 tab.pane.read(cx).item_for_index(tab.ix)
99 };
100 if let Some(item) = item {
101 if item.downcast::<TerminalView>().is_some() {
102 return ControlFlow::Continue(());
103 } else if let Some(project_path) = item.project_path(cx) {
104 if let Some(entry_path) = workspace
105 .update(cx, |workspace, cx| {
106 workspace
107 .project()
108 .read(cx)
109 .absolute_path(&project_path, cx)
110 })
111 .log_err()
112 .flatten()
113 {
114 add_paths_to_terminal(pane, &[entry_path], cx);
115 }
116 }
117 }
118 } else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
119 if let Some(entry_path) = workspace
120 .update(cx, |workspace, cx| {
121 let project = workspace.project().read(cx);
122 project
123 .path_for_entry(entry_id, cx)
124 .and_then(|project_path| project.absolute_path(&project_path, cx))
125 })
126 .log_err()
127 .flatten()
128 {
129 add_paths_to_terminal(pane, &[entry_path], cx);
130 }
131 } else if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
132 add_paths_to_terminal(pane, paths.paths(), cx);
133 }
134
135 ControlFlow::Break(())
136 });
137 let buffer_search_bar = cx.new_view(search::BufferSearchBar::new);
138 pane.toolbar()
139 .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
140 pane
141 });
142 let subscriptions = vec![
143 cx.observe(&pane, |_, _, cx| cx.notify()),
144 cx.subscribe(&pane, Self::handle_pane_event),
145 ];
146 let project = workspace.project().read(cx);
147 let enabled = project.supports_terminal(cx);
148 let this = Self {
149 pane,
150 fs: workspace.app_state().fs.clone(),
151 workspace: workspace.weak_handle(),
152 pending_serialization: Task::ready(None),
153 width: None,
154 height: None,
155 pending_terminals_to_add: 0,
156 deferred_tasks: HashMap::default(),
157 _subscriptions: subscriptions,
158 enabled,
159 assistant_enabled: false,
160 assistant_tab_bar_button: None,
161 };
162 this.apply_tab_bar_buttons(cx);
163 this
164 }
165
166 pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
167 self.assistant_enabled = enabled;
168 if enabled {
169 let focus_handle = self
170 .pane
171 .read(cx)
172 .active_item()
173 .map(|item| item.focus_handle(cx))
174 .unwrap_or(self.focus_handle(cx));
175 self.assistant_tab_bar_button = Some(
176 cx.new_view(move |_| InlineAssistTabBarButton { focus_handle })
177 .into(),
178 );
179 } else {
180 self.assistant_tab_bar_button = None;
181 }
182 self.apply_tab_bar_buttons(cx);
183 }
184
185 fn apply_tab_bar_buttons(&self, cx: &mut ViewContext<Self>) {
186 let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();
187 self.pane.update(cx, |pane, cx| {
188 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
189 if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
190 return (None, None);
191 }
192 let focus_handle = pane.focus_handle(cx);
193 let right_children = h_flex()
194 .gap_2()
195 .children(assistant_tab_bar_button.clone())
196 .child(
197 PopoverMenu::new("terminal-tab-bar-popover-menu")
198 .trigger(
199 IconButton::new("plus", IconName::Plus)
200 .icon_size(IconSize::Small)
201 .tooltip(|cx| Tooltip::text("New...", cx)),
202 )
203 .anchor(AnchorCorner::TopRight)
204 .with_handle(pane.new_item_context_menu_handle.clone())
205 .menu(move |cx| {
206 let focus_handle = focus_handle.clone();
207 let menu = ContextMenu::build(cx, |menu, _| {
208 menu.context(focus_handle.clone())
209 .action(
210 "New Terminal",
211 workspace::NewTerminal.boxed_clone(),
212 )
213 // We want the focus to go back to terminal panel once task modal is dismissed,
214 // hence we focus that first. Otherwise, we'd end up without a focused element, as
215 // context menu will be gone the moment we spawn the modal.
216 .action(
217 "Spawn task",
218 tasks_ui::Spawn::modal().boxed_clone(),
219 )
220 });
221
222 Some(menu)
223 }),
224 )
225 .child({
226 let zoomed = pane.is_zoomed();
227 IconButton::new("toggle_zoom", IconName::Maximize)
228 .icon_size(IconSize::Small)
229 .selected(zoomed)
230 .selected_icon(IconName::Minimize)
231 .on_click(cx.listener(|pane, _, cx| {
232 pane.toggle_zoom(&workspace::ToggleZoom, cx);
233 }))
234 .tooltip(move |cx| {
235 Tooltip::for_action(
236 if zoomed { "Zoom Out" } else { "Zoom In" },
237 &ToggleZoom,
238 cx,
239 )
240 })
241 })
242 .into_any_element()
243 .into();
244 (None, right_children)
245 });
246 });
247 }
248
249 pub async fn load(
250 workspace: WeakView<Workspace>,
251 mut cx: AsyncWindowContext,
252 ) -> Result<View<Self>> {
253 let serialized_panel = cx
254 .background_executor()
255 .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
256 .await
257 .log_err()
258 .flatten()
259 .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
260 .transpose()
261 .log_err()
262 .flatten();
263
264 let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
265 let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
266 let items = if let Some((serialized_panel, database_id)) =
267 serialized_panel.as_ref().zip(workspace.database_id())
268 {
269 panel.update(cx, |panel, cx| {
270 cx.notify();
271 panel.height = serialized_panel.height.map(|h| h.round());
272 panel.width = serialized_panel.width.map(|w| w.round());
273 panel.pane.update(cx, |_, cx| {
274 serialized_panel
275 .items
276 .iter()
277 .map(|item_id| {
278 TerminalView::deserialize(
279 workspace.project().clone(),
280 workspace.weak_handle(),
281 database_id,
282 *item_id,
283 cx,
284 )
285 })
286 .collect::<Vec<_>>()
287 })
288 })
289 } else {
290 Vec::new()
291 };
292 let pane = panel.read(cx).pane.clone();
293 (panel, pane, items)
294 })?;
295
296 if let Some(workspace) = workspace.upgrade() {
297 panel
298 .update(&mut cx, |panel, cx| {
299 panel._subscriptions.push(cx.subscribe(
300 &workspace,
301 |terminal_panel, _, e, cx| {
302 if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
303 terminal_panel.spawn_task(spawn_in_terminal, cx);
304 };
305 },
306 ))
307 })
308 .ok();
309 }
310
311 let pane = pane.downgrade();
312 let items = futures::future::join_all(items).await;
313 let mut alive_item_ids = Vec::new();
314 pane.update(&mut cx, |pane, cx| {
315 let active_item_id = serialized_panel
316 .as_ref()
317 .and_then(|panel| panel.active_item_id);
318 let mut active_ix = None;
319 for item in items {
320 if let Some(item) = item.log_err() {
321 let item_id = item.entity_id().as_u64();
322 pane.add_item(Box::new(item), false, false, None, cx);
323 alive_item_ids.push(item_id as ItemId);
324 if Some(item_id) == active_item_id {
325 active_ix = Some(pane.items_len() - 1);
326 }
327 }
328 }
329
330 if let Some(active_ix) = active_ix {
331 pane.activate_item(active_ix, false, false, cx)
332 }
333 })?;
334
335 // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
336 if let Some(workspace) = workspace.upgrade() {
337 let cleanup_task = workspace.update(&mut cx, |workspace, cx| {
338 workspace
339 .database_id()
340 .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx))
341 })?;
342 if let Some(task) = cleanup_task {
343 task.await.log_err();
344 }
345 }
346
347 Ok(panel)
348 }
349
350 fn handle_pane_event(
351 &mut self,
352 _pane: View<Pane>,
353 event: &pane::Event,
354 cx: &mut ViewContext<Self>,
355 ) {
356 match event {
357 pane::Event::ActivateItem { .. } => self.serialize(cx),
358 pane::Event::RemovedItem { .. } => self.serialize(cx),
359 pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
360 pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
361 pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
362
363 pane::Event::AddItem { item } => {
364 if let Some(workspace) = self.workspace.upgrade() {
365 let pane = self.pane.clone();
366 workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
367 }
368 }
369
370 _ => {}
371 }
372 }
373
374 pub fn open_terminal(
375 workspace: &mut Workspace,
376 action: &workspace::OpenTerminal,
377 cx: &mut ViewContext<Workspace>,
378 ) {
379 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
380 return;
381 };
382
383 terminal_panel
384 .update(cx, |panel, cx| {
385 panel.add_terminal(
386 TerminalKind::Shell(Some(action.working_directory.clone())),
387 RevealStrategy::Always,
388 cx,
389 )
390 })
391 .detach_and_log_err(cx);
392 }
393
394 fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
395 let mut spawn_task = spawn_in_terminal.clone();
396 // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
397 let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
398 Shell::System => {
399 match self
400 .workspace
401 .update(cx, |workspace, cx| workspace.project().read(cx).is_local())
402 {
403 Ok(local) => {
404 if local {
405 retrieve_system_shell().map(|shell| (shell, Vec::new()))
406 } else {
407 Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
408 }
409 }
410 Err(_no_window_e) => return,
411 }
412 }
413 Shell::Program(shell) => Some((shell, Vec::new())),
414 Shell::WithArguments { program, args } => Some((program, args)),
415 }) else {
416 return;
417 };
418 #[cfg(target_os = "windows")]
419 let windows_shell_type = to_windows_shell_type(&shell);
420
421 #[cfg(not(target_os = "windows"))]
422 {
423 spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
424 }
425 #[cfg(target_os = "windows")]
426 {
427 use crate::terminal_panel::WindowsShellType;
428
429 match windows_shell_type {
430 WindowsShellType::Powershell => {
431 spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
432 }
433 WindowsShellType::Cmd => {
434 spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
435 }
436 WindowsShellType::Other => {
437 spawn_task.command_label =
438 format!("{shell} -i -c '{}'", spawn_task.command_label)
439 }
440 }
441 }
442
443 let task_command = std::mem::replace(&mut spawn_task.command, shell);
444 let task_args = std::mem::take(&mut spawn_task.args);
445 let combined_command = task_args
446 .into_iter()
447 .fold(task_command, |mut command, arg| {
448 command.push(' ');
449 #[cfg(not(target_os = "windows"))]
450 command.push_str(&arg);
451 #[cfg(target_os = "windows")]
452 command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
453 command
454 });
455
456 #[cfg(not(target_os = "windows"))]
457 user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
458 #[cfg(target_os = "windows")]
459 {
460 use crate::terminal_panel::WindowsShellType;
461
462 match windows_shell_type {
463 WindowsShellType::Powershell => {
464 user_args.extend(["-C".to_owned(), combined_command])
465 }
466 WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
467 WindowsShellType::Other => {
468 user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
469 }
470 }
471 }
472 spawn_task.args = user_args;
473 let spawn_task = spawn_task;
474
475 let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
476 let use_new_terminal = spawn_in_terminal.use_new_terminal;
477
478 if allow_concurrent_runs && use_new_terminal {
479 self.spawn_in_new_terminal(spawn_task, cx)
480 .detach_and_log_err(cx);
481 return;
482 }
483
484 let terminals_for_task = self.terminals_for_task(&spawn_in_terminal.full_label, cx);
485 if terminals_for_task.is_empty() {
486 self.spawn_in_new_terminal(spawn_task, cx)
487 .detach_and_log_err(cx);
488 return;
489 }
490 let (existing_item_index, existing_terminal) = terminals_for_task
491 .last()
492 .expect("covered no terminals case above")
493 .clone();
494 if allow_concurrent_runs {
495 debug_assert!(
496 !use_new_terminal,
497 "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
498 );
499 self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx);
500 } else {
501 self.deferred_tasks.insert(
502 spawn_in_terminal.id.clone(),
503 cx.spawn(|terminal_panel, mut cx| async move {
504 wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
505 terminal_panel
506 .update(&mut cx, |terminal_panel, cx| {
507 if use_new_terminal {
508 terminal_panel
509 .spawn_in_new_terminal(spawn_task, cx)
510 .detach_and_log_err(cx);
511 } else {
512 terminal_panel.replace_terminal(
513 spawn_task,
514 existing_item_index,
515 existing_terminal,
516 cx,
517 );
518 }
519 })
520 .ok();
521 }),
522 );
523 }
524 }
525
526 pub fn spawn_in_new_terminal(
527 &mut self,
528 spawn_task: SpawnInTerminal,
529 cx: &mut ViewContext<Self>,
530 ) -> Task<Result<Model<Terminal>>> {
531 let reveal = spawn_task.reveal;
532 self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
533 }
534
535 /// Create a new Terminal in the current working directory or the user's home directory
536 fn new_terminal(
537 workspace: &mut Workspace,
538 _: &workspace::NewTerminal,
539 cx: &mut ViewContext<Workspace>,
540 ) {
541 let Some(terminal_panel) = workspace.panel::<Self>(cx) else {
542 return;
543 };
544
545 let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
546
547 terminal_panel
548 .update(cx, |this, cx| {
549 this.add_terminal(kind, RevealStrategy::Always, cx)
550 })
551 .detach_and_log_err(cx);
552 }
553
554 fn terminals_for_task(
555 &self,
556 label: &str,
557 cx: &mut AppContext,
558 ) -> Vec<(usize, View<TerminalView>)> {
559 self.pane
560 .read(cx)
561 .items()
562 .enumerate()
563 .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
564 .filter_map(|(index, terminal_view)| {
565 let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
566 if &task_state.full_label == label {
567 Some((index, terminal_view))
568 } else {
569 None
570 }
571 })
572 .collect()
573 }
574
575 fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
576 self.pane.update(cx, |pane, cx| {
577 pane.activate_item(item_index, true, true, cx)
578 })
579 }
580
581 fn add_terminal(
582 &mut self,
583 kind: TerminalKind,
584 reveal_strategy: RevealStrategy,
585 cx: &mut ViewContext<Self>,
586 ) -> Task<Result<Model<Terminal>>> {
587 if !self.enabled {
588 return Task::ready(Err(anyhow::anyhow!(
589 "terminal not yet supported for remote projects"
590 )));
591 }
592
593 let workspace = self.workspace.clone();
594 self.pending_terminals_to_add += 1;
595
596 cx.spawn(|terminal_panel, mut cx| async move {
597 let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?;
598 let result = workspace.update(&mut cx, |workspace, cx| {
599 let window = cx.window_handle();
600 let terminal = workspace
601 .project()
602 .update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
603 let terminal_view = Box::new(cx.new_view(|cx| {
604 TerminalView::new(
605 terminal.clone(),
606 workspace.weak_handle(),
607 workspace.database_id(),
608 cx,
609 )
610 }));
611 pane.update(cx, |pane, cx| {
612 let focus = pane.has_focus(cx);
613 pane.add_item(terminal_view, true, focus, None, cx);
614 });
615
616 if reveal_strategy == RevealStrategy::Always {
617 workspace.focus_panel::<Self>(cx);
618 }
619 Ok(terminal)
620 })?;
621 terminal_panel.update(&mut cx, |this, cx| {
622 this.pending_terminals_to_add = this.pending_terminals_to_add.saturating_sub(1);
623 this.serialize(cx)
624 })?;
625 result
626 })
627 }
628
629 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
630 let mut items_to_serialize = HashSet::default();
631 let items = self
632 .pane
633 .read(cx)
634 .items()
635 .filter_map(|item| {
636 let terminal_view = item.act_as::<TerminalView>(cx)?;
637 if terminal_view.read(cx).terminal().read(cx).task().is_some() {
638 None
639 } else {
640 let id = item.item_id().as_u64();
641 items_to_serialize.insert(id);
642 Some(id)
643 }
644 })
645 .collect::<Vec<_>>();
646 let active_item_id = self
647 .pane
648 .read(cx)
649 .active_item()
650 .map(|item| item.item_id().as_u64())
651 .filter(|active_id| items_to_serialize.contains(active_id));
652 let height = self.height;
653 let width = self.width;
654 self.pending_serialization = cx.background_executor().spawn(
655 async move {
656 KEY_VALUE_STORE
657 .write_kvp(
658 TERMINAL_PANEL_KEY.into(),
659 serde_json::to_string(&SerializedTerminalPanel {
660 items,
661 active_item_id,
662 height,
663 width,
664 })?,
665 )
666 .await?;
667 anyhow::Ok(())
668 }
669 .log_err(),
670 );
671 }
672
673 fn replace_terminal(
674 &self,
675 spawn_task: SpawnInTerminal,
676 terminal_item_index: usize,
677 terminal_to_replace: View<TerminalView>,
678 cx: &mut ViewContext<'_, Self>,
679 ) -> Option<()> {
680 let project = self
681 .workspace
682 .update(cx, |workspace, _| workspace.project().clone())
683 .ok()?;
684
685 let reveal = spawn_task.reveal;
686 let window = cx.window_handle();
687 let new_terminal = project.update(cx, |project, cx| {
688 project
689 .create_terminal(TerminalKind::Task(spawn_task), window, cx)
690 .log_err()
691 })?;
692 terminal_to_replace.update(cx, |terminal_to_replace, cx| {
693 terminal_to_replace.set_terminal(new_terminal, cx);
694 });
695
696 match reveal {
697 RevealStrategy::Always => {
698 self.activate_terminal_view(terminal_item_index, cx);
699 let task_workspace = self.workspace.clone();
700 cx.spawn(|_, mut cx| async move {
701 task_workspace
702 .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
703 .ok()
704 })
705 .detach();
706 }
707 RevealStrategy::Never => {}
708 }
709
710 Some(())
711 }
712
713 fn has_no_terminals(&self, cx: &WindowContext) -> bool {
714 self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0
715 }
716
717 pub fn assistant_enabled(&self) -> bool {
718 self.assistant_enabled
719 }
720}
721
722async fn wait_for_terminals_tasks(
723 terminals_for_task: Vec<(usize, View<TerminalView>)>,
724 cx: &mut AsyncWindowContext,
725) {
726 let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| {
727 terminal
728 .update(cx, |terminal_view, cx| {
729 terminal_view
730 .terminal()
731 .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
732 })
733 .ok()
734 });
735 let _: Vec<()> = join_all(pending_tasks).await;
736}
737
738fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) {
739 if let Some(terminal_view) = pane
740 .active_item()
741 .and_then(|item| item.downcast::<TerminalView>())
742 {
743 cx.focus_view(&terminal_view);
744 let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
745 new_text.push(' ');
746 terminal_view.update(cx, |terminal_view, cx| {
747 terminal_view.terminal().update(cx, |terminal, _| {
748 terminal.paste(&new_text);
749 });
750 });
751 }
752}
753
754impl EventEmitter<PanelEvent> for TerminalPanel {}
755
756impl Render for TerminalPanel {
757 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
758 let mut registrar = DivRegistrar::new(
759 |panel, cx| {
760 panel
761 .pane
762 .read(cx)
763 .toolbar()
764 .read(cx)
765 .item_of_type::<BufferSearchBar>()
766 },
767 cx,
768 );
769 BufferSearchBar::register(&mut registrar);
770 registrar.into_div().size_full().child(self.pane.clone())
771 }
772}
773
774impl FocusableView for TerminalPanel {
775 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
776 self.pane.focus_handle(cx)
777 }
778}
779
780impl Panel for TerminalPanel {
781 fn position(&self, cx: &WindowContext) -> DockPosition {
782 match TerminalSettings::get_global(cx).dock {
783 TerminalDockPosition::Left => DockPosition::Left,
784 TerminalDockPosition::Bottom => DockPosition::Bottom,
785 TerminalDockPosition::Right => DockPosition::Right,
786 }
787 }
788
789 fn position_is_valid(&self, _: DockPosition) -> bool {
790 true
791 }
792
793 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
794 settings::update_settings_file::<TerminalSettings>(
795 self.fs.clone(),
796 cx,
797 move |settings, _| {
798 let dock = match position {
799 DockPosition::Left => TerminalDockPosition::Left,
800 DockPosition::Bottom => TerminalDockPosition::Bottom,
801 DockPosition::Right => TerminalDockPosition::Right,
802 };
803 settings.dock = Some(dock);
804 },
805 );
806 }
807
808 fn size(&self, cx: &WindowContext) -> Pixels {
809 let settings = TerminalSettings::get_global(cx);
810 match self.position(cx) {
811 DockPosition::Left | DockPosition::Right => {
812 self.width.unwrap_or(settings.default_width)
813 }
814 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
815 }
816 }
817
818 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
819 match self.position(cx) {
820 DockPosition::Left | DockPosition::Right => self.width = size,
821 DockPosition::Bottom => self.height = size,
822 }
823 self.serialize(cx);
824 cx.notify();
825 }
826
827 fn is_zoomed(&self, cx: &WindowContext) -> bool {
828 self.pane.read(cx).is_zoomed()
829 }
830
831 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
832 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
833 }
834
835 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
836 if !active || !self.has_no_terminals(cx) {
837 return;
838 }
839 cx.defer(|this, cx| {
840 let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
841 TerminalKind::Shell(default_working_directory(workspace, cx))
842 }) else {
843 return;
844 };
845
846 this.add_terminal(kind, RevealStrategy::Never, cx)
847 .detach_and_log_err(cx)
848 })
849 }
850
851 fn icon_label(&self, cx: &WindowContext) -> Option<String> {
852 let count = self.pane.read(cx).items_len();
853 if count == 0 {
854 None
855 } else {
856 Some(count.to_string())
857 }
858 }
859
860 fn persistent_name() -> &'static str {
861 "TerminalPanel"
862 }
863
864 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
865 if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
866 Some(IconName::Terminal)
867 } else {
868 None
869 }
870 }
871
872 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
873 Some("Terminal Panel")
874 }
875
876 fn toggle_action(&self) -> Box<dyn gpui::Action> {
877 Box::new(ToggleFocus)
878 }
879
880 fn pane(&self) -> Option<View<Pane>> {
881 Some(self.pane.clone())
882 }
883}
884
885struct InlineAssistTabBarButton {
886 focus_handle: FocusHandle,
887}
888
889impl Render for InlineAssistTabBarButton {
890 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
891 let focus_handle = self.focus_handle.clone();
892 IconButton::new("terminal_inline_assistant", IconName::ZedAssistant)
893 .icon_size(IconSize::Small)
894 .on_click(cx.listener(|_, _, cx| {
895 cx.dispatch_action(InlineAssist::default().boxed_clone());
896 }))
897 .tooltip(move |cx| {
898 Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx)
899 })
900 }
901}
902
903#[derive(Serialize, Deserialize)]
904struct SerializedTerminalPanel {
905 items: Vec<u64>,
906 active_item_id: Option<u64>,
907 width: Option<Pixels>,
908 height: Option<Pixels>,
909}
910
911fn retrieve_system_shell() -> Option<String> {
912 #[cfg(not(target_os = "windows"))]
913 {
914 use anyhow::Context;
915 use util::ResultExt;
916
917 std::env::var("SHELL")
918 .context("Error finding SHELL in env.")
919 .log_err()
920 }
921 // `alacritty_terminal` uses this as default on Windows. See:
922 // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
923 #[cfg(target_os = "windows")]
924 return Some("powershell".to_owned());
925}
926
927#[cfg(target_os = "windows")]
928fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> String {
929 match shell_type {
930 WindowsShellType::Powershell => to_powershell_variable(input),
931 WindowsShellType::Cmd => to_cmd_variable(input),
932 WindowsShellType::Other => input,
933 }
934}
935
936#[cfg(target_os = "windows")]
937fn to_windows_shell_type(shell: &str) -> WindowsShellType {
938 if shell == "powershell"
939 || shell.ends_with("powershell.exe")
940 || shell == "pwsh"
941 || shell.ends_with("pwsh.exe")
942 {
943 WindowsShellType::Powershell
944 } else if shell == "cmd" || shell.ends_with("cmd.exe") {
945 WindowsShellType::Cmd
946 } else {
947 // Someother shell detected, the user might install and use a
948 // unix-like shell.
949 WindowsShellType::Other
950 }
951}
952
953/// Convert `${SOME_VAR}`, `$SOME_VAR` to `%SOME_VAR%`.
954#[inline]
955#[cfg(target_os = "windows")]
956fn to_cmd_variable(input: String) -> String {
957 if let Some(var_str) = input.strip_prefix("${") {
958 if var_str.find(':').is_none() {
959 // If the input starts with "${", remove the trailing "}"
960 format!("%{}%", &var_str[..var_str.len() - 1])
961 } else {
962 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
963 // which will result in the task failing to run in such cases.
964 input
965 }
966 } else if let Some(var_str) = input.strip_prefix('$') {
967 // If the input starts with "$", directly append to "$env:"
968 format!("%{}%", var_str)
969 } else {
970 // If no prefix is found, return the input as is
971 input
972 }
973}
974
975/// Convert `${SOME_VAR}`, `$SOME_VAR` to `$env:SOME_VAR`.
976#[inline]
977#[cfg(target_os = "windows")]
978fn to_powershell_variable(input: String) -> String {
979 if let Some(var_str) = input.strip_prefix("${") {
980 if var_str.find(':').is_none() {
981 // If the input starts with "${", remove the trailing "}"
982 format!("$env:{}", &var_str[..var_str.len() - 1])
983 } else {
984 // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
985 // which will result in the task failing to run in such cases.
986 input
987 }
988 } else if let Some(var_str) = input.strip_prefix('$') {
989 // If the input starts with "$", directly append to "$env:"
990 format!("$env:{}", var_str)
991 } else {
992 // If no prefix is found, return the input as is
993 input
994 }
995}
996
997#[cfg(target_os = "windows")]
998#[derive(Debug, Clone, Copy, PartialEq, Eq)]
999enum WindowsShellType {
1000 Powershell,
1001 Cmd,
1002 Other,
1003}