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}