cleansys_tui/components/
password_prompt.rs1use anyhow::Result;
2use ratatui::{
3 layout::Rect,
4 style::{Color, Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, Paragraph, Wrap},
7 Frame,
8};
9
10pub struct PasswordPrompt {
12 password_input: String,
14 error_message: Option<String>,
16 visible: bool,
18 authenticated: bool,
20}
21
22impl Default for PasswordPrompt {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl PasswordPrompt {
29 pub fn new() -> Self {
31 Self {
32 password_input: String::new(),
33 error_message: None,
34 visible: false,
35 authenticated: false,
36 }
37 }
38
39 pub fn show(&mut self) {
41 self.visible = true;
42 self.password_input.clear();
43 self.error_message = None;
44 }
45
46 pub fn hide(&mut self) {
48 self.visible = false;
49 self.password_input.clear();
50 self.error_message = None;
51 }
52
53 pub fn is_visible(&self) -> bool {
55 self.visible
56 }
57
58 pub fn is_authenticated(&self) -> bool {
60 self.authenticated
61 }
62
63 #[cfg(debug_assertions)]
68 pub fn mark_authenticated_for_tests(&mut self) {
69 self.authenticated = true;
70 }
71
72 pub fn add_char(&mut self, c: char) {
74 self.password_input.push(c);
75 }
76
77 pub fn remove_char(&mut self) {
79 self.password_input.pop();
80 }
81
82 pub fn verify_password(&mut self) -> Result<bool> {
84 let authenticated = cleansys_core::authenticate_sudo(&self.password_input)?;
85
86 if authenticated {
87 self.authenticated = true;
88 self.visible = false;
89 self.password_input.clear();
90 self.error_message = None;
91 Ok(true)
92 } else {
93 self.error_message = Some("Incorrect password. Please try again.".to_string());
94 self.password_input.clear();
95 Ok(false)
96 }
97 }
98
99 pub fn submit(&mut self) -> Result<bool> {
101 self.verify_password()
102 }
103
104 pub fn cancel(&mut self) {
106 self.hide();
107 }
108
109 pub fn render(&self, f: &mut Frame, area: Rect) {
111 if !self.visible {
112 return;
113 }
114
115 let popup_width = 60.min(area.width.saturating_sub(4));
117 let popup_height = 12.min(area.height.saturating_sub(4));
118
119 let popup_x = (area.width.saturating_sub(popup_width)) / 2;
120 let popup_y = (area.height.saturating_sub(popup_height)) / 2;
121
122 let popup_area = Rect {
123 x: popup_x,
124 y: popup_y,
125 width: popup_width,
126 height: popup_height,
127 };
128
129 let mut lines = vec![
131 Line::from(vec![Span::styled(
132 "🔒 System Cleaner Authentication",
133 Style::default()
134 .fg(Color::Yellow)
135 .add_modifier(Modifier::BOLD),
136 )]),
137 Line::from(vec![Span::raw("")]),
138 Line::from(vec![Span::raw(
139 "System cleaners require root privileges to clean system files.",
140 )]),
141 Line::from(vec![Span::raw("Please enter your password to continue:")]),
142 Line::from(vec![Span::raw("")]),
143 ];
144
145 let password_display = "•".repeat(self.password_input.len());
147 lines.push(Line::from(vec![
148 Span::styled("Password: ", Style::default().fg(Color::Cyan)),
149 Span::styled(
150 password_display,
151 Style::default()
152 .fg(Color::White)
153 .add_modifier(Modifier::BOLD),
154 ),
155 Span::styled("_", Style::default().fg(Color::Yellow)),
156 ]));
157
158 lines.push(Line::from(vec![Span::raw("")]));
159
160 if let Some(error) = &self.error_message {
162 lines.push(Line::from(vec![Span::styled(
163 format!("❌ {}", error),
164 Style::default().fg(Color::Red),
165 )]));
166 lines.push(Line::from(vec![Span::raw("")]));
167 }
168
169 lines.push(Line::from(vec![Span::styled(
171 "Press Enter to authenticate | ESC to cancel",
172 Style::default()
173 .fg(Color::DarkGray)
174 .add_modifier(Modifier::ITALIC),
175 )]));
176
177 let popup = Paragraph::new(lines)
178 .block(
179 Block::default()
180 .title("Authentication Required")
181 .borders(Borders::ALL)
182 .border_style(Style::default().fg(Color::Yellow)),
183 )
184 .wrap(Wrap { trim: true });
185
186 let clear_block = Block::default().style(Style::default().bg(Color::Black));
188 f.render_widget(clear_block, popup_area);
189
190 f.render_widget(popup, popup_area);
192 }
193}