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