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