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 mut message: SharedString;
 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                if message.is_empty() {
 90                    message = output.stderr.trim().to_string().into();
 91                    if message.is_empty() {
 92                        message = "Push Successful".into();
 93                    }
 94                    remote = None;
 95                } else {
 96                    let remote_message = get_remote_lines(&output.stderr);
 97
 98                    remote = if remote_message.is_empty() {
 99                        None
100                    } else {
101                        let finder = LinkFinder::new();
102                        let links = finder
103                            .links(&remote_message)
104                            .filter(|link| *link.kind() == LinkKind::Url)
105                            .map(|link| link.start()..link.end())
106                            .collect_vec();
107
108                        Some(InfoFromRemote {
109                            name: remote_ref.name,
110                            remote_text: remote_message.into(),
111                            links,
112                        })
113                    }
114                }
115            }
116        }
117
118        Self {
119            _workspace: workspace,
120            _id: id,
121            message,
122            remote_info: remote,
123            _dismiss_task: task,
124            focus_handle: cx.focus_handle(),
125        }
126    }
127}
128
129impl Render for RemoteOutputToast {
130    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
131        div()
132            .occlude()
133            .w_full()
134            .max_h(vh(0.8, window))
135            .elevation_3(cx)
136            .child(
137                v_flex()
138                    .p_3()
139                    .overflow_hidden()
140                    .child(
141                        h_flex()
142                            .justify_between()
143                            .items_start()
144                            .child(
145                                h_flex()
146                                    .gap_2()
147                                    .child(Icon::new(IconName::GitBranch).color(Color::Default))
148                                    .child(Label::new("Git")),
149                            )
150                            .child(h_flex().child(
151                                IconButton::new("close", IconName::Close).on_click(
152                                    cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
153                                ),
154                            )),
155                    )
156                    .child(Label::new(self.message.clone()).size(LabelSize::Default))
157                    .when_some(self.remote_info.as_ref(), |this, remote_info| {
158                        this.child(
159                            div()
160                                .border_1()
161                                .border_color(Color::Muted.color(cx))
162                                .rounded_lg()
163                                .text_sm()
164                                .mt_1()
165                                .p_1()
166                                .child(
167                                    h_flex()
168                                        .gap_2()
169                                        .child(Icon::new(IconName::Cloud).color(Color::Default))
170                                        .child(
171                                            Label::new(remote_info.name.clone())
172                                                .size(LabelSize::Default),
173                                        ),
174                                )
175                                .map(|div| {
176                                    let styled_text =
177                                        StyledText::new(remote_info.remote_text.clone())
178                                            .with_highlights(remote_info.links.iter().map(
179                                                |link| {
180                                                    (
181                                                        link.clone(),
182                                                        HighlightStyle {
183                                                            underline: Some(UnderlineStyle {
184                                                                thickness: px(1.0),
185                                                                ..Default::default()
186                                                            }),
187                                                            ..Default::default()
188                                                        },
189                                                    )
190                                                },
191                                            ));
192                                    let this = cx.weak_entity();
193                                    let text = InteractiveText::new("remote-message", styled_text)
194                                        .on_click(
195                                            remote_info.links.clone(),
196                                            move |ix, _window, cx| {
197                                                this.update(cx, |this, cx| {
198                                                    if let Some(remote_info) = &this.remote_info {
199                                                        cx.open_url(
200                                                            &remote_info.remote_text
201                                                                [remote_info.links[ix].clone()],
202                                                        )
203                                                    }
204                                                })
205                                                .ok();
206                                            },
207                                        );
208
209                                    div.child(text)
210                                }),
211                        )
212                    }),
213            )
214    }
215}
216
217impl EventEmitter<DismissEvent> for RemoteOutputToast {}
218
219fn get_remote_lines(output: &str) -> String {
220    output
221        .lines()
222        .filter_map(|line| line.strip_prefix("remote:"))
223        .map(|line| line.trim())
224        .filter(|line| !line.is_empty())
225        .collect::<Vec<_>>()
226        .join("\n")
227}