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