remote_output_toast.rs

  1use std::{ops::Range, time::Duration};
  2
  3use git::repository::{Remote, RemoteCommandOutput};
  4use gpui::{
  5    DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
  6    StyledText, Task, UnderlineStyle, WeakEntity,
  7};
  8use itertools::Itertools;
  9use linkify::{LinkFinder, LinkKind};
 10use ui::{
 11    div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
 12    IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
 13    Render, SharedString, Styled, StyledExt, Window,
 14};
 15use workspace::{
 16    notifications::{Notification, NotificationId},
 17    Workspace,
 18};
 19
 20pub enum RemoteAction {
 21    Fetch,
 22    Pull,
 23    Push(Remote),
 24}
 25
 26struct InfoFromRemote {
 27    name: SharedString,
 28    remote_text: SharedString,
 29    links: Vec<Range<usize>>,
 30}
 31
 32pub struct RemoteOutputToast {
 33    _workspace: WeakEntity<Workspace>,
 34    _id: NotificationId,
 35    message: SharedString,
 36    remote_info: Option<InfoFromRemote>,
 37    _dismiss_task: Task<()>,
 38    focus_handle: FocusHandle,
 39}
 40
 41impl Focusable for RemoteOutputToast {
 42    fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
 43        self.focus_handle.clone()
 44    }
 45}
 46
 47impl Notification for RemoteOutputToast {}
 48
 49const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
 50
 51impl RemoteOutputToast {
 52    pub fn new(
 53        action: RemoteAction,
 54        output: RemoteCommandOutput,
 55        id: NotificationId,
 56        workspace: WeakEntity<Workspace>,
 57        cx: &mut Context<Self>,
 58    ) -> Self {
 59        let task = cx.spawn({
 60            let workspace = workspace.clone();
 61            let id = id.clone();
 62            |_, mut cx| async move {
 63                cx.background_executor()
 64                    .timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
 65                    .await;
 66                workspace
 67                    .update(&mut cx, |workspace, cx| {
 68                        workspace.dismiss_notification(&id, cx);
 69                    })
 70                    .ok();
 71            }
 72        });
 73
 74        let message;
 75        let remote;
 76
 77        match action {
 78            RemoteAction::Fetch | RemoteAction::Pull => {
 79                if output.is_empty() {
 80                    message = "Up to date".into();
 81                } else {
 82                    message = output.stderr.into();
 83                }
 84                remote = None;
 85            }
 86
 87            RemoteAction::Push(remote_ref) => {
 88                message = output.stdout.trim().to_string().into();
 89                let remote_message = get_remote_lines(&output.stderr);
 90                let finder = LinkFinder::new();
 91                let links = finder
 92                    .links(&remote_message)
 93                    .filter(|link| *link.kind() == LinkKind::Url)
 94                    .map(|link| link.start()..link.end())
 95                    .collect_vec();
 96
 97                remote = Some(InfoFromRemote {
 98                    name: remote_ref.name,
 99                    remote_text: remote_message.into(),
100                    links,
101                });
102            }
103        }
104
105        Self {
106            _workspace: workspace,
107            _id: id,
108            message,
109            remote_info: remote,
110            _dismiss_task: task,
111            focus_handle: cx.focus_handle(),
112        }
113    }
114}
115
116impl Render for RemoteOutputToast {
117    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
118        div()
119            .occlude()
120            .w_full()
121            .max_h(vh(0.8, window))
122            .elevation_3(cx)
123            .child(
124                v_flex()
125                    .p_3()
126                    .overflow_hidden()
127                    .child(
128                        h_flex()
129                            .justify_between()
130                            .items_start()
131                            .child(
132                                h_flex()
133                                    .gap_2()
134                                    .child(Icon::new(IconName::GitBranch).color(Color::Default))
135                                    .child(Label::new("Git")),
136                            )
137                            .child(h_flex().child(
138                                IconButton::new("close", IconName::Close).on_click(
139                                    cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
140                                ),
141                            )),
142                    )
143                    .child(Label::new(self.message.clone()).size(LabelSize::Default))
144                    .when_some(self.remote_info.as_ref(), |this, remote_info| {
145                        this.child(
146                            div()
147                                .border_1()
148                                .border_color(Color::Muted.color(cx))
149                                .rounded_lg()
150                                .text_sm()
151                                .mt_1()
152                                .p_1()
153                                .child(
154                                    h_flex()
155                                        .gap_2()
156                                        .child(Icon::new(IconName::Cloud).color(Color::Default))
157                                        .child(
158                                            Label::new(remote_info.name.clone())
159                                                .size(LabelSize::Default),
160                                        ),
161                                )
162                                .map(|div| {
163                                    let styled_text =
164                                        StyledText::new(remote_info.remote_text.clone())
165                                            .with_highlights(remote_info.links.iter().map(
166                                                |link| {
167                                                    (
168                                                        link.clone(),
169                                                        HighlightStyle {
170                                                            underline: Some(UnderlineStyle {
171                                                                thickness: px(1.0),
172                                                                ..Default::default()
173                                                            }),
174                                                            ..Default::default()
175                                                        },
176                                                    )
177                                                },
178                                            ));
179                                    let this = cx.weak_entity();
180                                    let text = InteractiveText::new("remote-message", styled_text)
181                                        .on_click(
182                                            remote_info.links.clone(),
183                                            move |ix, _window, cx| {
184                                                this.update(cx, |this, cx| {
185                                                    if let Some(remote_info) = &this.remote_info {
186                                                        cx.open_url(
187                                                            &remote_info.remote_text
188                                                                [remote_info.links[ix].clone()],
189                                                        )
190                                                    }
191                                                })
192                                                .ok();
193                                            },
194                                        );
195
196                                    div.child(text)
197                                }),
198                        )
199                    }),
200            )
201    }
202}
203
204impl EventEmitter<DismissEvent> for RemoteOutputToast {}
205
206fn get_remote_lines(output: &str) -> String {
207    output
208        .lines()
209        .filter_map(|line| line.strip_prefix("remote:"))
210        .map(|line| line.trim())
211        .filter(|line| !line.is_empty())
212        .collect::<Vec<_>>()
213        .join("\n")
214}