1use std::{path::PathBuf, sync::Arc};
2
3use crate::TerminalView;
4use db::kvp::KEY_VALUE_STORE;
5use gpui::{
6 actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
7 FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext,
8 VisualContext, WeakView, WindowContext,
9};
10use project::Fs;
11use serde::{Deserialize, Serialize};
12use settings::{Settings, SettingsStore};
13use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
14use util::{ResultExt, TryFutureExt};
15use workspace::{
16 dock::{DockPosition, Panel, PanelEvent},
17 item::Item,
18 pane,
19 ui::Icon,
20 Pane, Workspace,
21};
22
23use anyhow::Result;
24
25const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
26
27actions!(ToggleFocus);
28
29pub fn init(cx: &mut AppContext) {
30 cx.observe_new_views(
31 |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
32 workspace.register_action(TerminalPanel::new_terminal);
33 workspace.register_action(TerminalPanel::open_terminal);
34 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
35 workspace.toggle_panel_focus::<TerminalPanel>(cx);
36 });
37 },
38 )
39 .detach();
40}
41
42pub struct TerminalPanel {
43 pane: View<Pane>,
44 fs: Arc<dyn Fs>,
45 workspace: WeakView<Workspace>,
46 width: Option<f32>,
47 height: Option<f32>,
48 pending_serialization: Task<Option<()>>,
49 _subscriptions: Vec<Subscription>,
50}
51
52impl TerminalPanel {
53 fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
54 let _weak_self = cx.view().downgrade();
55 let pane = cx.build_view(|cx| {
56 let _window = cx.window_handle();
57 let mut pane = Pane::new(
58 workspace.weak_handle(),
59 workspace.project().clone(),
60 Default::default(),
61 cx,
62 );
63 pane.set_can_split(false, cx);
64 pane.set_can_navigate(false, cx);
65 // todo!()
66 // pane.on_can_drop(move |drag_and_drop, cx| {
67 // drag_and_drop
68 // .currently_dragged::<DraggedItem>(window)
69 // .map_or(false, |(_, item)| {
70 // item.handle.act_as::<TerminalView>(cx).is_some()
71 // })
72 // });
73 // pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
74 // let this = weak_self.clone();
75 // Flex::row()
76 // .with_child(Pane::render_tab_bar_button(
77 // 0,
78 // "icons/plus.svg",
79 // false,
80 // Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
81 // cx,
82 // move |_, cx| {
83 // let this = this.clone();
84 // cx.window_context().defer(move |cx| {
85 // if let Some(this) = this.upgrade() {
86 // this.update(cx, |this, cx| {
87 // this.add_terminal(None, cx);
88 // });
89 // }
90 // })
91 // },
92 // |_, _| {},
93 // None,
94 // ))
95 // .with_child(Pane::render_tab_bar_button(
96 // 1,
97 // if pane.is_zoomed() {
98 // "icons/minimize.svg"
99 // } else {
100 // "icons/maximize.svg"
101 // },
102 // pane.is_zoomed(),
103 // Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
104 // cx,
105 // move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
106 // |_, _| {},
107 // None,
108 // ))
109 // .into_any()
110 // });
111 // let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
112 // pane.toolbar()
113 // .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
114 pane
115 });
116 let subscriptions = vec![
117 cx.observe(&pane, |_, _, cx| cx.notify()),
118 cx.subscribe(&pane, Self::handle_pane_event),
119 ];
120 let this = Self {
121 pane,
122 fs: workspace.app_state().fs.clone(),
123 workspace: workspace.weak_handle(),
124 pending_serialization: Task::ready(None),
125 width: None,
126 height: None,
127 _subscriptions: subscriptions,
128 };
129 let mut old_dock_position = this.position(cx);
130 cx.observe_global::<SettingsStore>(move |this, cx| {
131 let new_dock_position = this.position(cx);
132 if new_dock_position != old_dock_position {
133 old_dock_position = new_dock_position;
134 cx.emit(PanelEvent::ChangePosition);
135 }
136 })
137 .detach();
138 this
139 }
140
141 pub async fn load(
142 workspace: WeakView<Workspace>,
143 mut cx: AsyncWindowContext,
144 ) -> Result<View<Self>> {
145 let serialized_panel = cx
146 .background_executor()
147 .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
148 .await
149 .log_err()
150 .flatten()
151 .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
152 .transpose()
153 .log_err()
154 .flatten();
155
156 let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
157 let panel = cx.build_view(|cx| TerminalPanel::new(workspace, cx));
158 let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
159 panel.update(cx, |panel, cx| {
160 cx.notify();
161 panel.height = serialized_panel.height;
162 panel.width = serialized_panel.width;
163 panel.pane.update(cx, |_, cx| {
164 serialized_panel
165 .items
166 .iter()
167 .map(|item_id| {
168 TerminalView::deserialize(
169 workspace.project().clone(),
170 workspace.weak_handle(),
171 workspace.database_id(),
172 *item_id,
173 cx,
174 )
175 })
176 .collect::<Vec<_>>()
177 })
178 })
179 } else {
180 Default::default()
181 };
182 let pane = panel.read(cx).pane.clone();
183 (panel, pane, items)
184 })?;
185
186 let pane = pane.downgrade();
187 let items = futures::future::join_all(items).await;
188 pane.update(&mut cx, |pane, cx| {
189 let active_item_id = serialized_panel
190 .as_ref()
191 .and_then(|panel| panel.active_item_id);
192 let mut active_ix = None;
193 for item in items {
194 if let Some(item) = item.log_err() {
195 let item_id = item.entity_id().as_u64();
196 pane.add_item(Box::new(item), false, false, None, cx);
197 if Some(item_id) == active_item_id {
198 active_ix = Some(pane.items_len() - 1);
199 }
200 }
201 }
202
203 if let Some(active_ix) = active_ix {
204 pane.activate_item(active_ix, false, false, cx)
205 }
206 })?;
207
208 Ok(panel)
209 }
210
211 fn handle_pane_event(
212 &mut self,
213 _pane: View<Pane>,
214 event: &pane::Event,
215 cx: &mut ViewContext<Self>,
216 ) {
217 match event {
218 pane::Event::ActivateItem { .. } => self.serialize(cx),
219 pane::Event::RemoveItem { .. } => self.serialize(cx),
220 pane::Event::Remove => cx.emit(PanelEvent::Close),
221 pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
222 pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
223 pane::Event::Focus => cx.emit(PanelEvent::Focus),
224
225 pane::Event::AddItem { item } => {
226 if let Some(workspace) = self.workspace.upgrade() {
227 let pane = self.pane.clone();
228 workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
229 }
230 }
231
232 _ => {}
233 }
234 }
235
236 pub fn open_terminal(
237 workspace: &mut Workspace,
238 action: &workspace::OpenTerminal,
239 cx: &mut ViewContext<Workspace>,
240 ) {
241 let Some(this) = workspace.focus_panel::<Self>(cx) else {
242 return;
243 };
244
245 this.update(cx, |this, cx| {
246 this.add_terminal(Some(action.working_directory.clone()), cx)
247 })
248 }
249
250 ///Create a new Terminal in the current working directory or the user's home directory
251 fn new_terminal(
252 workspace: &mut Workspace,
253 _: &workspace::NewTerminal,
254 cx: &mut ViewContext<Workspace>,
255 ) {
256 let Some(this) = workspace.focus_panel::<Self>(cx) else {
257 return;
258 };
259
260 this.update(cx, |this, cx| this.add_terminal(None, cx))
261 }
262
263 fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
264 let workspace = self.workspace.clone();
265 cx.spawn(|this, mut cx| async move {
266 let pane = this.update(&mut cx, |this, _| this.pane.clone())?;
267 workspace.update(&mut cx, |workspace, cx| {
268 let working_directory = if let Some(working_directory) = working_directory {
269 Some(working_directory)
270 } else {
271 let working_directory_strategy =
272 TerminalSettings::get_global(cx).working_directory.clone();
273 crate::get_working_directory(workspace, cx, working_directory_strategy)
274 };
275
276 let window = cx.window_handle();
277 if let Some(terminal) = workspace.project().update(cx, |project, cx| {
278 project
279 .create_terminal(working_directory, window, cx)
280 .log_err()
281 }) {
282 let terminal = Box::new(cx.build_view(|cx| {
283 TerminalView::new(
284 terminal,
285 workspace.weak_handle(),
286 workspace.database_id(),
287 cx,
288 )
289 }));
290 pane.update(cx, |pane, cx| {
291 let focus = pane.has_focus(cx);
292 pane.add_item(terminal, true, focus, None, cx);
293 });
294 }
295 })?;
296 this.update(&mut cx, |this, cx| this.serialize(cx))?;
297 anyhow::Ok(())
298 })
299 .detach_and_log_err(cx);
300 }
301
302 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
303 let items = self
304 .pane
305 .read(cx)
306 .items()
307 .map(|item| item.item_id().as_u64())
308 .collect::<Vec<_>>();
309 let active_item_id = self
310 .pane
311 .read(cx)
312 .active_item()
313 .map(|item| item.item_id().as_u64());
314 let height = self.height;
315 let width = self.width;
316 self.pending_serialization = cx.background_executor().spawn(
317 async move {
318 KEY_VALUE_STORE
319 .write_kvp(
320 TERMINAL_PANEL_KEY.into(),
321 serde_json::to_string(&SerializedTerminalPanel {
322 items,
323 active_item_id,
324 height,
325 width,
326 })?,
327 )
328 .await?;
329 anyhow::Ok(())
330 }
331 .log_err(),
332 );
333 }
334}
335
336impl EventEmitter<PanelEvent> for TerminalPanel {}
337
338impl Render for TerminalPanel {
339 type Element = Div;
340
341 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
342 div().child(self.pane.clone())
343 }
344}
345
346impl FocusableView for TerminalPanel {
347 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
348 self.pane.focus_handle(cx)
349 }
350}
351
352impl Panel for TerminalPanel {
353 fn position(&self, cx: &WindowContext) -> DockPosition {
354 match TerminalSettings::get_global(cx).dock {
355 TerminalDockPosition::Left => DockPosition::Left,
356 TerminalDockPosition::Bottom => DockPosition::Bottom,
357 TerminalDockPosition::Right => DockPosition::Right,
358 }
359 }
360
361 fn position_is_valid(&self, _: DockPosition) -> bool {
362 true
363 }
364
365 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
366 settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
367 let dock = match position {
368 DockPosition::Left => TerminalDockPosition::Left,
369 DockPosition::Bottom => TerminalDockPosition::Bottom,
370 DockPosition::Right => TerminalDockPosition::Right,
371 };
372 settings.dock = Some(dock);
373 });
374 }
375
376 fn size(&self, cx: &WindowContext) -> f32 {
377 let settings = TerminalSettings::get_global(cx);
378 match self.position(cx) {
379 DockPosition::Left | DockPosition::Right => {
380 self.width.unwrap_or_else(|| settings.default_width)
381 }
382 DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
383 }
384 }
385
386 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
387 match self.position(cx) {
388 DockPosition::Left | DockPosition::Right => self.width = size,
389 DockPosition::Bottom => self.height = size,
390 }
391 self.serialize(cx);
392 cx.notify();
393 }
394
395 fn is_zoomed(&self, cx: &WindowContext) -> bool {
396 self.pane.read(cx).is_zoomed()
397 }
398
399 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
400 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
401 }
402
403 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
404 if active && self.pane.read(cx).items_len() == 0 {
405 self.add_terminal(None, cx)
406 }
407 }
408
409 fn icon_label(&self, cx: &WindowContext) -> Option<String> {
410 let count = self.pane.read(cx).items_len();
411 if count == 0 {
412 None
413 } else {
414 Some(count.to_string())
415 }
416 }
417
418 fn has_focus(&self, cx: &WindowContext) -> bool {
419 self.pane.read(cx).has_focus(cx)
420 }
421
422 fn persistent_name() -> &'static str {
423 "TerminalPanel"
424 }
425
426 // todo!()
427 // fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
428 // ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
429 // }
430
431 fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
432 Some(Icon::Terminal)
433 }
434
435 fn toggle_action(&self) -> Box<dyn gpui::Action> {
436 Box::new(ToggleFocus)
437 }
438}
439
440#[derive(Serialize, Deserialize)]
441struct SerializedTerminalPanel {
442 items: Vec<u64>,
443 active_item_id: Option<u64>,
444 width: Option<f32>,
445 height: Option<f32>,
446}