askpass_modal.rs

  1use askpass::EncryptedPassword;
  2use editor::Editor;
  3use futures::channel::oneshot;
  4use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Styled};
  5use ui::{
  6    ActiveTheme, AnyElement, App, Button, Clickable, Color, Context, DynamicSpacing, Headline,
  7    HeadlineSize, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
  8    LabelSize, ParentElement, Render, SharedString, StyledExt, StyledTypography, Window, div,
  9    h_flex, v_flex,
 10};
 11use util::maybe;
 12use workspace::ModalView;
 13use zeroize::Zeroize;
 14
 15pub(crate) struct AskPassModal {
 16    operation: SharedString,
 17    prompt: SharedString,
 18    editor: Entity<Editor>,
 19    tx: Option<oneshot::Sender<EncryptedPassword>>,
 20}
 21
 22impl EventEmitter<DismissEvent> for AskPassModal {}
 23impl ModalView for AskPassModal {}
 24impl Focusable for AskPassModal {
 25    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 26        self.editor.focus_handle(cx)
 27    }
 28}
 29
 30impl AskPassModal {
 31    pub fn new(
 32        operation: SharedString,
 33        prompt: SharedString,
 34        tx: oneshot::Sender<EncryptedPassword>,
 35        window: &mut Window,
 36        cx: &mut Context<Self>,
 37    ) -> Self {
 38        let editor = cx.new(|cx| {
 39            let mut editor = Editor::single_line(window, cx);
 40            if prompt.contains("yes/no") || prompt.contains("Username") {
 41                editor.set_masked(false, cx);
 42            } else {
 43                editor.set_masked(true, cx);
 44            }
 45            editor
 46        });
 47        Self {
 48            operation,
 49            prompt,
 50            editor,
 51            tx: Some(tx),
 52        }
 53    }
 54
 55    fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
 56        cx.emit(DismissEvent);
 57    }
 58
 59    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 60        maybe!({
 61            let tx = self.tx.take()?;
 62            let mut text = self.editor.update(cx, |this, cx| {
 63                let text = this.text(cx);
 64                this.clear(window, cx);
 65                text
 66            });
 67            let pw = askpass::EncryptedPassword::try_from(text.as_ref()).ok()?;
 68            text.zeroize();
 69            tx.send(pw).ok();
 70            Some(())
 71        });
 72
 73        cx.emit(DismissEvent);
 74    }
 75
 76    fn render_hint(&mut self, cx: &mut Context<Self>) -> Option<AnyElement> {
 77        let color = cx.theme().status().info_background;
 78        if (self.prompt.contains("Password") || self.prompt.contains("Username"))
 79            && self.prompt.contains("github.com")
 80        {
 81            return Some(
 82            div()
 83                .p_2()
 84                .bg(color)
 85                .border_t_1()
 86                .border_color(cx.theme().status().info_border)
 87                .child(
 88                    h_flex().gap_2()
 89                        .child(
 90                            Icon::new(IconName::Github).size(IconSize::Small)
 91                        )
 92                        .child(
 93                            Label::new("You may need to configure git for Github.")
 94                                .size(LabelSize::Small),
 95                        )
 96                        .child(Button::new("learn-more", "Learn more").color(Color::Accent).label_size(LabelSize::Small).on_click(|_, _, cx| {
 97                            cx.open_url("https://docs.github.com/en/get-started/git-basics/set-up-git#authenticating-with-github-from-git")
 98                        })),
 99                )
100                .into_any_element(),
101        );
102        }
103        None
104    }
105}
106
107impl Render for AskPassModal {
108    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
109        v_flex()
110            .key_context("PasswordPrompt")
111            .on_action(cx.listener(Self::cancel))
112            .on_action(cx.listener(Self::confirm))
113            .elevation_2(cx)
114            .size_full()
115            .child(
116                h_flex()
117                    .font_buffer(cx)
118                    .px(DynamicSpacing::Base12.rems(cx))
119                    .pt(DynamicSpacing::Base08.rems(cx))
120                    .pb(DynamicSpacing::Base04.rems(cx))
121                    .rounded_t_sm()
122                    .w_full()
123                    .gap_1p5()
124                    .child(Icon::new(IconName::GitBranch).size(IconSize::XSmall))
125                    .child(h_flex().gap_1().overflow_x_hidden().child(
126                        div().max_w_96().overflow_x_hidden().text_ellipsis().child(
127                            Headline::new(self.operation.clone()).size(HeadlineSize::XSmall),
128                        ),
129                    )),
130            )
131            .child(
132                div()
133                    .font_buffer(cx)
134                    .text_buffer(cx)
135                    .py_2()
136                    .px_3()
137                    .bg(cx.theme().colors().editor_background)
138                    .border_t_1()
139                    .border_color(cx.theme().colors().border_variant)
140                    .size_full()
141                    .overflow_hidden()
142                    .child(self.prompt.clone())
143                    .child(self.editor.clone()),
144            )
145            .children(self.render_hint(cx))
146    }
147}