Skip to main content

cleansys_tui/components/
password_prompt.rs

1use 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
10/// Password prompt component for sudo authentication
11pub struct PasswordPrompt {
12    /// The password input (stored temporarily during entry)
13    password_input: String,
14    /// Error message to display if authentication fails
15    error_message: Option<String>,
16    /// Whether the prompt is currently visible
17    visible: bool,
18    /// Whether authentication was successful
19    authenticated: bool,
20}
21
22impl Default for PasswordPrompt {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl PasswordPrompt {
29    /// Create a new password prompt
30    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    /// Show the password prompt
40    pub fn show(&mut self) {
41        self.visible = true;
42        self.password_input.clear();
43        self.error_message = None;
44    }
45
46    /// Hide the password prompt
47    pub fn hide(&mut self) {
48        self.visible = false;
49        self.password_input.clear();
50        self.error_message = None;
51    }
52
53    /// Check if the prompt is visible
54    pub fn is_visible(&self) -> bool {
55        self.visible
56    }
57
58    /// Check if authentication was successful
59    pub fn is_authenticated(&self) -> bool {
60        self.authenticated
61    }
62
63    /// Force the "already authenticated this session" flag for tests,
64    /// without shelling out to a real `sudo` process. Only compiled into
65    /// debug builds (which is what `cargo test` uses), never shipped in a
66    /// release binary.
67    #[cfg(debug_assertions)]
68    pub fn mark_authenticated_for_tests(&mut self) {
69        self.authenticated = true;
70    }
71
72    /// Add a character to the password input
73    pub fn add_char(&mut self, c: char) {
74        self.password_input.push(c);
75    }
76
77    /// Remove the last character from the password input
78    pub fn remove_char(&mut self) {
79        self.password_input.pop();
80    }
81
82    /// Verify the password using sudo (delegates to shared core logic).
83    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    /// Handle the submit action (Enter key)
100    pub fn submit(&mut self) -> Result<bool> {
101        self.verify_password()
102    }
103
104    /// Handle the cancel action (ESC key)
105    pub fn cancel(&mut self) {
106        self.hide();
107    }
108
109    /// Render the password prompt as an overlay
110    pub fn render(&self, f: &mut Frame, area: Rect) {
111        if !self.visible {
112            return;
113        }
114
115        // Create a centered popup
116        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        // Create the popup content
130        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        // Add password input line with masked characters
146        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        // Add error message if present
161        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        // Add instructions
170        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        // Clear the area behind the popup
187        let clear_block = Block::default().style(Style::default().bg(Color::Black));
188        f.render_widget(clear_block, popup_area);
189
190        // Render the popup
191        f.render_widget(popup, popup_area);
192    }
193}