chamber_ui/
ui.rs

1use crate::app::{
2    AddItemField, App, ChangeKeyField, ImportExportField, ImportExportMode, ItemCounts, PasswordGenField, Screen,
3    StatusType, UnlockField, ViewMode,
4};
5use chamber_vault::ItemKind;
6use color_eyre::Result;
7use ratatui::crossterm::event;
8use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
9use ratatui::layout::Alignment;
10use ratatui::{
11    Frame, Terminal,
12    backend::CrosstermBackend,
13    layout::{Constraint, Direction, Layout, Rect},
14    style::{Color, Modifier, Style},
15    text::{Line, Span, Text},
16    widgets::{Block, BorderType, Borders, Clear, List, ListItem, Paragraph, Wrap},
17};
18
19// --- Palette ---
20const fn c_bg() -> Color {
21    Color::Rgb(18, 18, 23)
22}
23const fn c_bg_panel() -> Color {
24    Color::Rgb(24, 26, 33)
25}
26const fn c_border() -> Color {
27    Color::Rgb(60, 66, 80)
28}
29const fn c_accent() -> Color {
30    Color::Rgb(80, 200, 255)
31} // cyan-ish
32const fn c_accent2() -> Color {
33    Color::Rgb(148, 92, 255)
34} // purple
35const fn c_ok() -> Color {
36    Color::Rgb(120, 220, 120)
37}
38const fn c_warn() -> Color {
39    Color::Rgb(255, 210, 90)
40}
41const fn c_err() -> Color {
42    Color::Rgb(255, 120, 120)
43}
44const fn c_text() -> Color {
45    Color::Rgb(220, 224, 232)
46}
47const fn c_text_dim() -> Color {
48    Color::Rgb(140, 145, 160)
49}
50const fn c_badge_pwd() -> Color {
51    Color::Rgb(255, 140, 140)
52}
53const fn c_badge_env() -> Color {
54    Color::Rgb(120, 220, 120)
55}
56const fn c_badge_note() -> Color {
57    Color::Rgb(120, 180, 255)
58}
59
60const fn c_badge_api() -> Color {
61    Color::Rgb(255, 165, 0) // Orange
62}
63
64const fn c_badge_ssh() -> Color {
65    Color::Rgb(0, 255, 255) // Cyan
66}
67
68const fn c_badge_cert() -> Color {
69    Color::Rgb(255, 20, 147) // Deep Pink
70}
71
72const fn c_badge_db() -> Color {
73    Color::Rgb(50, 205, 50) // Lime Green
74}
75
76const fn c_badge_creditcard() -> Color {
77    Color::Rgb(255, 215, 0) // Gold for credit cards
78}
79
80const fn c_badge_securenote() -> Color {
81    Color::Rgb(138, 43, 226) // Purple for secure notes
82}
83
84const fn c_badge_identity() -> Color {
85    Color::Rgb(0, 191, 255) // Deep sky blue for identity
86}
87
88const fn c_badge_server() -> Color {
89    Color::Rgb(220, 20, 60) // Crimson for servers
90}
91
92const fn c_badge_wifi() -> Color {
93    Color::Rgb(34, 139, 34) // Forest green for WiFi
94}
95
96const fn c_badge_license() -> Color {
97    Color::Rgb(255, 140, 0) // Dark orange for licenses
98}
99
100const fn c_badge_bankaccount() -> Color {
101    Color::Rgb(0, 100, 0) // Dark green for bank accounts
102}
103
104const fn c_badge_document() -> Color {
105    Color::Rgb(105, 105, 105) // Dim gray for documents
106}
107
108const fn c_badge_recovery() -> Color {
109    Color::Rgb(255, 20, 147) // Deep pink for recovery codes
110}
111
112const fn c_badge_oauth() -> Color {
113    Color::Rgb(30, 144, 255) // Dodger blue for OAuth
114}
115
116fn truncate_text(text: &str, max_width: usize) -> String {
117    if text.chars().count() <= max_width {
118        text.to_string()
119    } else {
120        let truncated: String = text.chars().take(max_width.saturating_sub(3)).collect();
121        format!("{truncated}...")
122    }
123}
124
125/// Runs the application in a terminal-based user interface with an event loop.
126///
127/// This function initializes a terminal using the Crossterm backend, clears its
128/// contents, and enters an event loop. In this loop, it draws the application's
129/// current state to the terminal and waits for user input events. When a key
130/// event occurs, it processes the event and checks if the application should
131/// exit. The loop exits when the `handle_key` function signals to terminate.
132///
133/// # Arguments
134///
135/// * `app` - A mutable reference to the application's state, which is used
136///   and updated throughout the event loop.
137///
138/// # Returns
139///
140/// * `Result<()>` - Returns `Ok(())` if the application runs and exits successfully.
141///   Returns an error if any issues are encountered during initialization, drawing,
142///   or event handling.
143///
144/// # Errors
145///
146/// This function can return an error in the following situations:
147/// * If the terminal backend cannot be initialized.
148/// * If the terminal fails to clear its contents or render the UI.
149/// * If there is an error reading input events or processing them.
150///
151/// # Notes
152///
153/// * The terminal clearing and rendering are achieved using the `Terminal` and
154///   `CrosstermBackend` from the `tui` (Terminal User Interface) library.
155/// * The event loop uses a polling mechanism to periodically check for input
156///   using `event::poll`, blocking for 250 milliseconds before timing out.
157/// * Pressing specific keys, as determined by the `handle_key` function, can
158///   terminate the event loop.
159pub async fn run_app(app: &mut App) -> Result<()> {
160    let backend = CrosstermBackend::new(std::io::stdout());
161    let mut terminal = Terminal::new(backend)?;
162
163    let mut last_countdown_update = std::time::Instant::now();
164    let countdown_update_interval = std::time::Duration::from_secs(1);
165
166    terminal.clear()?;
167    let _auto_lock_handle = if let Some(service) = &app.auto_lock_service {
168        Some(service.start().await)
169    } else {
170        None
171    };
172
173    loop {
174        if last_countdown_update.elapsed() >= countdown_update_interval {
175            app.update_countdown_info().await;
176            last_countdown_update = std::time::Instant::now();
177        }
178
179        terminal.draw(|f| draw(f, app))?;
180        if app.check_auto_lock().await {
181            continue;
182        }
183
184        if event::poll(std::time::Duration::from_millis(250))? {
185            if let Event::Key(key) = event::read()? {
186                app.update_activity().await;
187                if key.kind == KeyEventKind::Press && handle_key(app, key)? {
188                    break;
189                }
190            }
191        }
192    }
193    Ok(())
194}
195
196#[allow(clippy::too_many_lines)]
197#[allow(clippy::cognitive_complexity)]
198fn handle_key(app: &mut App, key: KeyEvent) -> Result<bool> {
199    if app.search_mode {
200        match key.code {
201            KeyCode::Esc => {
202                app.search_mode = false;
203                return Ok(false);
204            }
205            KeyCode::Enter => {
206                app.search_mode = false;
207                app.update_filtered_items();
208                return Ok(false);
209            }
210            KeyCode::Backspace => {
211                app.search_query.pop();
212                app.update_filtered_items();
213                return Ok(false);
214            }
215            KeyCode::Char(c) => {
216                app.search_query.push(c);
217                app.update_filtered_items();
218                return Ok(false);
219            }
220            _ => return Ok(false),
221        }
222    }
223
224    // Handle global Ctrl combinations FIRST, before screen-specific logic
225    if key.modifiers.contains(KeyModifiers::CONTROL) {
226        match key.code {
227            KeyCode::Char('v' | 'V') => {
228                // Handle paste based on current screen and focus
229                match app.screen {
230                    Screen::Unlock => {
231                        if let Ok(mut clipboard) = arboard::Clipboard::new() {
232                            if let Ok(text) = clipboard.get_text() {
233                                match app.unlock_focus {
234                                    UnlockField::Master => app.master_input.push_str(&text),
235                                    UnlockField::Confirm => {
236                                        if app.master_mode_is_setup {
237                                            app.master_confirm_input.push_str(&text);
238                                        }
239                                    }
240                                }
241                            }
242                        }
243                        return Ok(false); // Prevent further processing
244                    }
245                    Screen::AddItem => {
246                        match app.add_focus {
247                            AddItemField::Value => {
248                                if let Err(e) = app.paste_to_add_value() {
249                                    app.set_status(format!("Paste failed: {e}"), StatusType::Error);
250                                }
251                            }
252                            AddItemField::Name => {
253                                if let Ok(mut clipboard) = arboard::Clipboard::new() {
254                                    if let Ok(text) = clipboard.get_text() {
255                                        app.add_name.push_str(&text);
256                                        app.set_status(
257                                            format!("Pasted {} characters to name field", text.len()),
258                                            StatusType::Success,
259                                        );
260                                    }
261                                }
262                            }
263                            AddItemField::Kind => {}
264                        }
265                        return Ok(false); // Prevent further processing
266                    }
267                    Screen::EditItem => {
268                        if let Ok(mut clipboard) = arboard::Clipboard::new() {
269                            if let Ok(text) = clipboard.get_text() {
270                                app.edit_value.push_str(&text);
271                                app.set_status(
272                                    format!("Pasted {} characters to edit field", text.len()),
273                                    StatusType::Success,
274                                );
275                            }
276                        }
277                        return Ok(false); // Prevent further processing
278                    }
279                    Screen::ChangeMaster => {
280                        if let Ok(mut clipboard) = arboard::Clipboard::new() {
281                            if let Ok(text) = clipboard.get_text() {
282                                match app.ck_focus {
283                                    ChangeKeyField::Current => app.ck_current.push_str(&text),
284                                    ChangeKeyField::New => app.ck_new.push_str(&text),
285                                    ChangeKeyField::Confirm => app.ck_confirm.push_str(&text),
286                                }
287                            }
288                        }
289                        return Ok(false); // Prevent further processing
290                    }
291                    Screen::ImportExport => {
292                        if matches!(app.ie_focus, ImportExportField::Path) {
293                            if let Ok(mut clipboard) = arboard::Clipboard::new() {
294                                if let Ok(text) = clipboard.get_text() {
295                                    app.ie_path.push_str(&text);
296                                    app.set_status(
297                                        format!("Pasted {} characters to path field", text.len()),
298                                        StatusType::Success,
299                                    );
300                                }
301                            }
302                        }
303                        return Ok(false); // Prevent further processing
304                    }
305                    _ => {}
306                }
307                return Ok(false); // Always prevent further processing for Ctrl+V
308            }
309            KeyCode::Char('c' | 'C') => {
310                // Handle copy operations
311                if matches!(app.screen, Screen::Main) {
312                    app.copy_selected()?;
313                }
314                return Ok(false); // Prevent further processing
315            }
316            KeyCode::Enter => {
317                // Handle Ctrl+Enter for saving based on current screen and focus
318                match app.screen {
319                    Screen::AddItem if matches!(app.add_focus, AddItemField::Value) => {
320                        // Save the item when Ctrl+Enter is pressed in Value field
321                        return app.add_item().map(|()| false);
322                    }
323                    _ => {
324                        // For other screens/fields, let the normal handling occur
325                    }
326                }
327                return Ok(false);
328            }
329
330            _ => {
331                // For other Ctrl combinations, don't process them as regular characters
332                return Ok(false);
333            }
334        }
335    }
336
337    // Now handle screen-specific keys (after Ctrl combinations are processed)
338    match app.screen {
339        Screen::Unlock => match key.code {
340            KeyCode::Esc => return Ok(true),
341            KeyCode::Enter => {
342                app.unlock()?;
343            }
344            KeyCode::Tab => {
345                if app.master_mode_is_setup {
346                    app.unlock_focus = match app.unlock_focus {
347                        UnlockField::Master => UnlockField::Confirm,
348                        UnlockField::Confirm => UnlockField::Master,
349                    };
350                }
351            }
352            KeyCode::Backspace => match app.unlock_focus {
353                UnlockField::Master => {
354                    app.master_input.pop();
355                }
356                UnlockField::Confirm => {
357                    if app.master_mode_is_setup {
358                        app.master_confirm_input.pop();
359                    }
360                }
361            },
362            KeyCode::Char(c) => {
363                // Only process regular characters (Ctrl combinations handled above)
364                match app.unlock_focus {
365                    UnlockField::Master => app.master_input.push(c),
366                    UnlockField::Confirm => {
367                        if app.master_mode_is_setup {
368                            app.master_confirm_input.push(c);
369                        }
370                    }
371                }
372            }
373            _ => {}
374        },
375
376        Screen::Main => match key.code {
377            KeyCode::Char('q') => return Ok(true),
378            KeyCode::Char('a') => {
379                app.screen = Screen::AddItem;
380            }
381            KeyCode::Char('c') => {
382                // Only handle 'c' for copy if Ctrl is NOT pressed (Ctrl+C handled above)
383                if !key.modifiers.contains(KeyModifiers::CONTROL) {
384                    app.copy_selected()?;
385                }
386            }
387            KeyCode::Char('v') => {
388                // Only handle 'v' for view if Ctrl is NOT pressed (Ctrl+V handled above)
389                app.view_selected();
390            }
391            KeyCode::Char('e') => {
392                app.edit_selected();
393            }
394            KeyCode::Char('k') => {
395                app.ck_current.clear();
396                app.ck_new.clear();
397                app.ck_confirm.clear();
398                app.ck_focus = ChangeKeyField::Current;
399                app.error = None;
400                app.screen = Screen::ChangeMaster;
401            }
402            KeyCode::Char('g') => {
403                app.open_password_generator();
404            }
405            KeyCode::Char('x') => {
406                app.open_import_export(ImportExportMode::Export);
407            }
408            KeyCode::Char('i') => {
409                app.open_import_export(ImportExportMode::Import);
410            }
411            KeyCode::Char('d') => {
412                app.delete_selected()?;
413            }
414            KeyCode::Down => {
415                if app.filtered_items.is_empty() {
416                    return Ok(false);
417                }
418
419                if app.selected < app.filtered_items.len().saturating_sub(1) {
420                    app.selected += 1;
421                } else {
422                    app.selected = 0;
423                }
424
425                let viewport_height = 10;
426                if app.selected >= app.scroll_offset + viewport_height {
427                    app.scroll_offset = app.selected.saturating_sub(viewport_height - 1);
428                } else if app.selected == 0 {
429                    app.scroll_offset = 0;
430                }
431            }
432            KeyCode::Up => {
433                if app.filtered_items.is_empty() {
434                    return Ok(false);
435                }
436
437                if app.selected > 0 {
438                    app.selected -= 1;
439                } else {
440                    app.selected = app.filtered_items.len().saturating_sub(1);
441                }
442                if app.selected < app.scroll_offset {
443                    app.scroll_offset = app.selected;
444                }
445            }
446            KeyCode::Char('r') => {
447                app.refresh_items()?;
448            }
449            KeyCode::F(2) => {
450                app.open_vault_selector();
451            }
452            KeyCode::Char('/' | 's') => {
453                app.search_mode = true;
454            }
455            KeyCode::Esc => {
456                if !app.search_query.is_empty() {
457                    app.search_query.clear();
458                    app.update_filtered_items();
459                }
460            }
461            _ => {}
462        },
463
464        Screen::AddItem => match key.code {
465            KeyCode::Esc => {
466                app.screen = Screen::Main;
467            }
468            KeyCode::Tab => {
469                app.add_value = app.add_value_textarea.lines().join("\n");
470                app.add_focus = match app.add_focus {
471                    AddItemField::Name => AddItemField::Kind,
472                    AddItemField::Kind => AddItemField::Value,
473                    AddItemField::Value => AddItemField::Name,
474                };
475            }
476            KeyCode::Left | KeyCode::Right if matches!(app.add_focus, AddItemField::Kind) => {
477                let total_kinds = 17;
478                if key.code == KeyCode::Right {
479                    app.add_kind_idx = (app.add_kind_idx + 1) % total_kinds;
480                } else {
481                    app.add_kind_idx = if app.add_kind_idx == 0 {
482                        total_kinds - 1
483                    } else {
484                        app.add_kind_idx - 1
485                    };
486                }
487            }
488            KeyCode::Char(c) => {
489                // All regular character input (Ctrl combinations handled above)
490                match app.add_focus {
491                    AddItemField::Name => app.add_name.push(c),
492                    AddItemField::Value => {
493                        app.add_value_textarea.input(key);
494                    }
495                    AddItemField::Kind => {}
496                }
497            }
498            _ => {
499                match app.add_focus {
500                    AddItemField::Name => {
501                        // Handle name field input
502                        match key.code {
503                            KeyCode::Enter => {
504                                // Save item when Enter is pressed in name field
505                                app.add_value = app.add_value_textarea.lines().join("\n");
506                                return app.add_item().map(|()| false);
507                            }
508                            KeyCode::Backspace => {
509                                app.add_name.pop();
510                            }
511                            KeyCode::Char(c) => {
512                                app.add_name.push(c);
513                            }
514                            _ => {}
515                        }
516                    }
517                    AddItemField::Kind => {
518                        // Kind field doesn't need text input, just navigation
519                        if matches!(key.code, KeyCode::Enter) {
520                            app.add_value = app.add_value_textarea.lines().join("\n");
521                            return app.add_item().map(|()| false);
522                        }
523                    }
524                    AddItemField::Value => {
525                        // Let textarea handle ALL input for Value field
526                        match key.code {
527                            // Only intercept Ctrl+Enter for saving
528                            KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) => {
529                                app.add_value = app.add_value_textarea.lines().join("\n");
530                                return app.add_item().map(|()| false);
531                            }
532                            // Let textarea handle everything else, including regular Enter
533                            _ => {
534                                app.add_value_textarea.input(key);
535                            }
536                        }
537                    }
538                }
539            }
540        },
541
542        Screen::ViewItem => match key.code {
543            KeyCode::Esc => {
544                app.screen = Screen::Main;
545            }
546            KeyCode::Char('t') | KeyCode::Enter => {
547                app.toggle_value_visibility();
548            }
549            KeyCode::Char('c') => {
550                if let Some(item) = &app.view_item {
551                    if let Ok(mut clipboard) = arboard::Clipboard::new() {
552                        let _ = clipboard.set_text(&item.value);
553                        app.set_status(format!("Copied '{}' to clipboard", item.name), StatusType::Success);
554                    }
555                }
556            }
557            _ => {}
558        },
559
560        Screen::EditItem => match key.code {
561            KeyCode::Esc => {
562                app.screen = Screen::Main;
563            }
564            KeyCode::Enter => {
565                app.save_edit()?;
566            }
567            KeyCode::Backspace => {
568                app.edit_value.pop();
569            }
570            KeyCode::Char(c) => {
571                // All regular character input (Ctrl combinations handled above)
572                app.edit_value.push(c);
573            }
574            _ => {}
575        },
576
577        Screen::ChangeMaster => match key.code {
578            KeyCode::Esc => {
579                app.screen = Screen::Main;
580            }
581            KeyCode::Enter => {
582                app.change_master()?;
583            }
584            KeyCode::Tab => {
585                app.ck_focus = match app.ck_focus {
586                    ChangeKeyField::Current => ChangeKeyField::New,
587                    ChangeKeyField::New => ChangeKeyField::Confirm,
588                    ChangeKeyField::Confirm => ChangeKeyField::Current,
589                };
590            }
591            KeyCode::Backspace => match app.ck_focus {
592                ChangeKeyField::Current => {
593                    app.ck_current.pop();
594                }
595                ChangeKeyField::New => {
596                    app.ck_new.pop();
597                }
598                ChangeKeyField::Confirm => {
599                    app.ck_confirm.pop();
600                }
601            },
602            KeyCode::Char(c) => {
603                // All regular character input (Ctrl combinations handled above)
604                match app.ck_focus {
605                    ChangeKeyField::Current => app.ck_current.push(c),
606                    ChangeKeyField::New => app.ck_new.push(c),
607                    ChangeKeyField::Confirm => app.ck_confirm.push(c),
608                }
609            }
610            _ => {}
611        },
612
613        Screen::GeneratePassword => match key.code {
614            KeyCode::Esc => {
615                app.screen = Screen::Main;
616            }
617            KeyCode::Tab => {
618                app.gen_focus = match app.gen_focus {
619                    PasswordGenField::Length => PasswordGenField::Options,
620                    PasswordGenField::Options => PasswordGenField::Generate,
621                    PasswordGenField::Generate => PasswordGenField::Length,
622                };
623            }
624            KeyCode::Char('g') | KeyCode::Enter => {
625                app.generate_password();
626            }
627            KeyCode::Char('c') => {
628                if let Err(e) = app.copy_generated_password() {
629                    app.set_status(format!("Copy failed: {e}"), StatusType::Error);
630                }
631            }
632            KeyCode::Char('u') => {
633                app.use_generated_password();
634            }
635            KeyCode::Char(c) if matches!(app.gen_focus, PasswordGenField::Length) => {
636                if c.is_ascii_digit() {
637                    app.gen_length_str.push(c);
638                }
639            }
640            KeyCode::Backspace if matches!(app.gen_focus, PasswordGenField::Length) => {
641                app.gen_length_str.pop();
642            }
643            KeyCode::Char('1') if matches!(app.gen_focus, PasswordGenField::Options) => {
644                app.gen_config.include_uppercase = !app.gen_config.include_uppercase;
645            }
646            KeyCode::Char('2') if matches!(app.gen_focus, PasswordGenField::Options) => {
647                app.gen_config.include_lowercase = !app.gen_config.include_lowercase;
648            }
649            KeyCode::Char('3') if matches!(app.gen_focus, PasswordGenField::Options) => {
650                app.gen_config.include_digits = !app.gen_config.include_digits;
651            }
652            KeyCode::Char('4') if matches!(app.gen_focus, PasswordGenField::Options) => {
653                app.gen_config.include_symbols = !app.gen_config.include_symbols;
654            }
655            KeyCode::Char('5') if matches!(app.gen_focus, PasswordGenField::Options) => {
656                app.gen_config.exclude_ambiguous = !app.gen_config.exclude_ambiguous;
657            }
658            _ => {}
659        },
660
661        Screen::ImportExport => match key.code {
662            KeyCode::Esc => {
663                app.screen = Screen::Main;
664            }
665            KeyCode::Tab => {
666                app.ie_focus = match app.ie_focus {
667                    ImportExportField::Path => ImportExportField::Format,
668                    ImportExportField::Format => ImportExportField::Action,
669                    ImportExportField::Action => ImportExportField::Path,
670                };
671            }
672            KeyCode::Enter => {
673                if matches!(app.ie_focus, ImportExportField::Action) {
674                    app.execute_import_export()?;
675                }
676            }
677            KeyCode::Left | KeyCode::Right if matches!(app.ie_focus, ImportExportField::Format) => {
678                if key.code == KeyCode::Right {
679                    app.ie_format_idx = (app.ie_format_idx + 1) % app.ie_formats.len();
680                } else {
681                    app.ie_format_idx = if app.ie_format_idx == 0 {
682                        app.ie_formats.len() - 1
683                    } else {
684                        app.ie_format_idx - 1
685                    };
686                }
687            }
688            KeyCode::Backspace if matches!(app.ie_focus, ImportExportField::Path) => {
689                app.ie_path.pop();
690            }
691            KeyCode::Char(c) if matches!(app.ie_focus, ImportExportField::Path) => {
692                // All regular character input (Ctrl combinations handled above)
693                app.ie_path.push(c);
694            }
695            _ => {}
696        },
697        Screen::VaultSelector => {
698            if let Some(action) = app.vault_selector.handle_input(key) {
699                app.handle_vault_action(action)?;
700            }
701            return Ok(false);
702        }
703    }
704
705    Ok(false)
706}
707
708fn draw(f: &mut Frame, app: &mut App) {
709    let size = f.area();
710    let bg_block = Block::default().style(Style::default().bg(c_bg()));
711    f.render_widget(bg_block, size);
712
713    let root = Layout::default()
714        .direction(Direction::Vertical)
715        .constraints([
716            Constraint::Length(1), // Header
717            Constraint::Min(0),    // Body
718            Constraint::Length(1), // Status bar
719        ])
720        .split(size);
721
722    draw_header(f, root[0]);
723    draw_status_bar(f, app, root[2]);
724
725    match app.screen {
726        Screen::Unlock => draw_unlock(f, app, root[1]),
727        Screen::Main => draw_main(f, app, root[1]),
728        Screen::AddItem => {
729            draw_main(f, app, root[1]);
730            draw_add_item(f, app);
731        }
732        Screen::ViewItem => {
733            draw_main(f, app, root[1]);
734            draw_view_item(f, app);
735        }
736        Screen::EditItem => {
737            draw_main(f, app, root[1]);
738            draw_edit_item(f, app);
739        }
740        Screen::ChangeMaster => {
741            draw_main(f, app, root[1]);
742            draw_change_master(f, app);
743        }
744        Screen::GeneratePassword => {
745            draw_main(f, app, root[1]);
746            draw_generate_password(f, app);
747        }
748        Screen::ImportExport => {
749            draw_main(f, app, root[1]);
750            draw_import_export(f, app);
751        }
752        Screen::VaultSelector => draw_vault_selector(f, app, root[1]),
753    }
754}
755
756fn draw_vault_selector(f: &mut Frame, app: &mut App, area: Rect) {
757    app.vault_selector.render(f, area);
758}
759
760fn draw_header(f: &mut Frame, area: Rect) {
761    let title = Line::from(vec![
762        Span::styled(
763            "  ◈ chamber ",
764            Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
765        ),
766        Span::raw(" "),
767        Span::styled("secure vault", Style::default().fg(c_text_dim())),
768    ]);
769
770    let bar = Block::default()
771        .borders(Borders::BOTTOM)
772        .border_type(BorderType::Plain)
773        .border_style(Style::default().fg(c_border()))
774        .style(Style::default().bg(c_bg_panel()));
775    f.render_widget(bar, area);
776
777    let inner = Layout::default()
778        .direction(Direction::Horizontal)
779        .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
780        .split(area);
781
782    let title_para = Paragraph::new(title).style(Style::default().fg(c_text()));
783    f.render_widget(title_para, inner[0]);
784
785    let version_info = Paragraph::new(Line::from(vec![
786        Span::styled("v", Style::default().fg(c_text_dim())),
787        Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(c_accent())),
788        Span::styled(" © 2025", Style::default().fg(c_text_dim())),
789    ]))
790    .style(Style::default().fg(c_text()))
791    .alignment(Alignment::Right);
792    f.render_widget(version_info, inner[1]);
793}
794
795fn draw_unlock(f: &mut Frame, app: &App, body: Rect) {
796    let area = centered_rect(60, 40, body);
797    let title = if app.master_mode_is_setup {
798        " Create Master Key "
799    } else {
800        " Unlock "
801    };
802    let block = Block::default()
803        .borders(Borders::ALL)
804        .border_type(BorderType::Rounded)
805        .title(Span::styled(
806            title,
807            Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
808        ))
809        .style(Style::default().bg(c_bg_panel()).fg(c_text()))
810        .border_style(Style::default().fg(c_border()));
811
812    let highlight = Style::default().fg(c_accent()).add_modifier(Modifier::BOLD);
813    let dim = Style::default().fg(c_text_dim());
814
815    let mk_label = if app.master_mode_is_setup {
816        "New master key"
817    } else {
818        "Master key"
819    };
820    let mk_value = field_box(
821        &mask(&app.master_input),
822        matches!(app.unlock_focus, UnlockField::Master),
823    );
824
825    let mut lines: Vec<Line> = vec![
826        Line::from(Span::styled(mk_label, Style::default().fg(c_text_dim()))),
827        Line::from(mk_value),
828    ];
829
830    if app.master_mode_is_setup {
831        let cf_value = field_box(
832            &mask(&app.master_confirm_input),
833            matches!(app.unlock_focus, UnlockField::Confirm),
834        );
835        lines.push(Line::default());
836        lines.push(Line::from(Span::styled(
837            "Confirm master key",
838            Style::default().fg(c_text_dim()),
839        )));
840        lines.push(Line::from(cf_value));
841        lines.push(Line::default());
842        lines.push(Line::from(vec![
843            Span::styled("[Tab]", highlight),
844            Span::styled(" switch  ", dim),
845            Span::styled("[Enter]", highlight),
846            Span::styled(" initialize & unlock  ", dim),
847            Span::styled("[Esc]", highlight),
848            Span::styled(" quit", dim),
849        ]));
850    } else {
851        lines.push(Line::default());
852        lines.push(Line::from(vec![
853            Span::styled("[Enter]", highlight),
854            Span::styled(" unlock  ", dim),
855            Span::styled("[Esc]", highlight),
856            Span::styled(" quit", dim),
857        ]));
858    }
859
860    if let Some(err) = &app.error {
861        lines.push(Line::default());
862        lines.push(Line::from(Span::styled(
863            err,
864            Style::default().fg(c_err()).add_modifier(Modifier::BOLD),
865        )));
866    }
867
868    let p = Paragraph::new(Text::from(lines)).block(block).wrap(Wrap { trim: true });
869    f.render_widget(Clear, area);
870    f.render_widget(p, area);
871}
872
873#[allow(clippy::too_many_lines)]
874fn draw_main(f: &mut Frame, app: &App, body: Rect) {
875    // Create three-column layout: Items | Categories | Help
876    let main_layout = Layout::default()
877        .direction(Direction::Horizontal)
878        .constraints([
879            Constraint::Percentage(60), // Items section
880            Constraint::Percentage(20), // Categories section
881            Constraint::Percentage(20), // Help section
882        ])
883        .split(centered_rect(96, 90, body));
884
885    let items_area = main_layout[0];
886    let categories_area = main_layout[1];
887    let help_area = main_layout[2];
888
889    // === ITEMS SECTION ===
890    draw_items_section(f, app, items_area);
891
892    // === CATEGORIES SECTION ===
893    draw_categories_section(f, app, categories_area);
894
895    // === HELP SECTION ===
896    draw_help_section(f, help_area);
897}
898
899#[allow(clippy::too_many_lines)]
900fn draw_items_section(f: &mut Frame, app: &App, area: Rect) {
901    // Create layout for search bar and items list
902    let (search_area, items_area) = if app.search_mode || !app.search_query.is_empty() {
903        let chunks = Layout::default()
904            .direction(Direction::Vertical)
905            .constraints([Constraint::Length(3), Constraint::Min(1)])
906            .split(area);
907        (Some(chunks[0]), chunks[1])
908    } else {
909        (None, area) // Use full area if no search
910    };
911
912    // Draw search bar if needed
913    if let Some(search_rect) = search_area {
914        let search_text = if app.search_mode {
915            format!("Search: {}_", app.search_query) // Show cursor when in search mode
916        } else {
917            format!("Search: {} (Press '/' or 's' to edit, Esc to clear)", app.search_query)
918        };
919
920        let search_style = if app.search_mode {
921            Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)
922        } else {
923            Style::default().fg(c_text_dim())
924        };
925
926        let search_block = Block::default()
927            .borders(Borders::ALL)
928            .border_type(BorderType::Rounded)
929            .border_style(Style::default().fg(if app.search_mode { c_accent() } else { c_border() }))
930            .style(Style::default().bg(c_bg_panel()));
931
932        let search_paragraph = Paragraph::new(search_text).style(search_style).block(search_block);
933
934        f.render_widget(search_paragraph, search_rect);
935    }
936
937    // Calculate viewport dimensions for items area
938    let viewport_height = items_area.height.saturating_sub(2) as usize; // account for borders
939    let content_length = app.filtered_items.len();
940
941    // Calculate what items to show based on scroll offset
942    let visible_start = app.scroll_offset.min(content_length.saturating_sub(1));
943    let visible_end = (visible_start + viewport_height).min(content_length);
944
945    // Create list items only for visible range
946    let mut list_items: Vec<ListItem> = Vec::new();
947    let mut current_category = None;
948
949    for (index, item) in app.filtered_items.iter().enumerate() {
950        // Skip items outside visible range
951        if index < visible_start || index >= visible_end {
952            continue;
953        }
954
955        // Add category header if needed
956        let item_category = item.kind.as_str();
957        if current_category != Some(item_category) {
958            current_category = Some(item_category);
959
960            // Add spacing before new category (except for first visible item)
961            if !list_items.is_empty() {
962                list_items.push(ListItem::new(Line::from("")));
963            }
964
965            // Category header
966            let (category_name, category_icon, category_color) = match item.kind {
967                ItemKind::Password => (" PASSWORDS", "🔐", c_badge_pwd()),
968                ItemKind::EnvVar => (" ENVIRONMENT VARIABLES", "🌍", c_badge_env()),
969                ItemKind::Note => (" NOTES", "📝", c_badge_note()),
970                ItemKind::ApiKey => (" API KEYS", "🔑", c_badge_api()),
971                ItemKind::SshKey => (" SSH KEYS", "🔒", c_badge_ssh()),
972                ItemKind::Certificate => (" CERTIFICATES", "📜", c_badge_cert()),
973                ItemKind::Database => (" DATABASES", "🗄️", c_badge_db()),
974                ItemKind::CreditCard => (" CREDIT CARDS", "💳", c_badge_creditcard()),
975                ItemKind::SecureNote => (" SECURE NOTES", "🔒", c_badge_securenote()),
976                ItemKind::Identity => (" IDENTITIES", "🆔", c_badge_identity()),
977                ItemKind::Server => (" SERVERS", "🖥️", c_badge_server()),
978                ItemKind::WifiPassword => (" WIFI", "📶", c_badge_wifi()),
979                ItemKind::License => (" LICENSES", "📄", c_badge_license()),
980                ItemKind::BankAccount => (" BANK ACCOUNTS", "🏦", c_badge_bankaccount()),
981                ItemKind::Document => (" DOCUMENTS", "📋", c_badge_document()),
982                ItemKind::Recovery => (" RECOVERY", "🔄", c_badge_recovery()),
983                ItemKind::OAuth => (" OAUTH TOKENS", "🎫", c_badge_oauth()),
984            };
985
986            let header_line = Line::from(vec![
987                Span::styled(
988                    format!("  {category_icon} "),
989                    Style::default().fg(category_color).add_modifier(Modifier::BOLD),
990                ),
991                Span::styled(
992                    category_name,
993                    Style::default().fg(category_color).add_modifier(Modifier::BOLD),
994                ),
995                Span::styled(" ".repeat(50), Style::default().fg(c_border())),
996            ]);
997
998            list_items.push(ListItem::new(header_line).style(Style::default().bg(Color::Rgb(30, 32, 38))));
999        }
1000
1001        // Regular item
1002        let (badge, badge_color) = match item.kind {
1003            ItemKind::Password => ("🔐", c_badge_pwd()),
1004            ItemKind::EnvVar => ("🌍", c_badge_env()),
1005            ItemKind::Note => ("📝", c_badge_note()),
1006            ItemKind::ApiKey => ("🔑", c_badge_api()),
1007            ItemKind::SshKey => ("🔒", c_badge_ssh()),
1008            ItemKind::Certificate => ("📜", c_badge_cert()),
1009            ItemKind::Database => ("🗄️", c_badge_db()),
1010            ItemKind::CreditCard => ("💳", c_badge_creditcard()),
1011            ItemKind::SecureNote => ("🔒", c_badge_securenote()),
1012            ItemKind::Identity => ("🆔", c_badge_identity()),
1013            ItemKind::Server => ("🖥️", c_badge_server()),
1014            ItemKind::WifiPassword => ("📶", c_badge_wifi()),
1015            ItemKind::License => ("📄", c_badge_license()),
1016            ItemKind::BankAccount => ("🏦", c_badge_bankaccount()),
1017            ItemKind::Document => ("📋", c_badge_document()),
1018            ItemKind::Recovery => ("🔄", c_badge_recovery()),
1019            ItemKind::OAuth => ("🎫", c_badge_oauth()),
1020        };
1021
1022        let created_date = match time::format_description::parse("[year]-[month]-[day]") {
1023            Ok(format) => item
1024                .created_at
1025                .format(&format)
1026                .unwrap_or_else(|_| "unknown".to_string()),
1027            Err(_) => "unknown".to_string(),
1028        };
1029
1030        let content_width = items_area.width.saturating_sub(8) as usize;
1031        let max_name_width = content_width.saturating_sub(20); // Conservative estimate for all fixed elements
1032
1033        let truncated_name = if max_name_width < 1 {
1034            "…".to_string() // Show ellipsis if no room
1035        } else {
1036            truncate_text(&item.name, max_name_width.max(1))
1037        };
1038
1039        // Highlight search matches in the item name
1040        let item_name_spans = if !app.search_query.is_empty() && !app.search_mode {
1041            highlight_search_matches(&truncated_name, &app.search_query, c_text(), c_accent())
1042        } else {
1043            vec![Span::styled(
1044                truncated_name,
1045                Style::default().fg(c_text()).add_modifier(Modifier::BOLD),
1046            )]
1047        };
1048
1049        let mut item_line_spans = vec![
1050            Span::raw("    "), // Indentation for items under category
1051            Span::styled(format!("{badge} "), Style::default().fg(badge_color)),
1052        ];
1053        item_line_spans.extend(item_name_spans);
1054        item_line_spans.push(Span::styled(
1055            format!(" ({created_date})"),
1056            Style::default().fg(c_text_dim()),
1057        ));
1058
1059        let item_line = Line::from(item_line_spans);
1060
1061        // Highlight selected item
1062        let item_style = if app.selected == index {
1063            Style::default()
1064                .bg(Color::Rgb(40, 46, 60))
1065                .fg(c_accent())
1066                .add_modifier(Modifier::BOLD)
1067        } else {
1068            Style::default()
1069        };
1070
1071        list_items.push(ListItem::new(item_line).style(item_style));
1072    }
1073
1074    // Show empty state or search results info
1075    if list_items.is_empty() && app.filtered_items.is_empty() {
1076        let empty_message = if app.search_query.is_empty() {
1077            match app.view_mode {
1078                ViewMode::All => "No items in vault".to_string(),
1079                ViewMode::Passwords => "No passwords stored".to_string(),
1080                ViewMode::Environment => "No environment variables stored".to_string(),
1081                ViewMode::Notes => "No notes stored".to_string(),
1082            }
1083        } else {
1084            format!("No items match search: '{}'", app.search_query)
1085        };
1086
1087        list_items.push(ListItem::new(Line::from(vec![Span::styled(
1088            format!("     {empty_message}"),
1089            Style::default().fg(c_text_dim()),
1090        )])));
1091    }
1092
1093    let items_title = if app.search_query.is_empty() {
1094        format!(
1095            " {} ({}/{}) ",
1096            app.view_mode.as_str(),
1097            app.filtered_items.len(),
1098            app.items.len()
1099        )
1100    } else {
1101        format!(
1102            " {} ({}/{}) - Search: '{}' ",
1103            app.view_mode.as_str(),
1104            app.filtered_items.len(),
1105            app.items.len(),
1106            app.search_query
1107        )
1108    };
1109
1110    let list_block = Block::default()
1111        .borders(Borders::ALL)
1112        .border_type(BorderType::Rounded)
1113        .border_style(Style::default().fg(c_border()))
1114        .style(Style::default().bg(c_bg_panel()))
1115        .title(Span::styled(
1116            &items_title,
1117            Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
1118        ));
1119
1120    let list = List::new(list_items).block(list_block.clone());
1121    f.render_widget(list, items_area);
1122
1123    // Create scrollbar
1124    let mut scrollbar_state =
1125        ratatui::widgets::ScrollbarState::new(content_length.max(1).saturating_sub(1)).position(app.scroll_offset);
1126
1127    let scrollbar = ratatui::widgets::Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalRight)
1128        .begin_symbol(Some("↑"))
1129        .end_symbol(Some("↓"))
1130        .thumb_style(Style::default().fg(c_accent()).add_modifier(Modifier::BOLD))
1131        .track_style(Style::default().fg(c_text_dim()));
1132
1133    let inner_area = list_block.inner(items_area);
1134    let scrollbar_area = Rect {
1135        x: inner_area.x + inner_area.width.saturating_sub(1),
1136        y: inner_area.y,
1137        width: 1,
1138        height: inner_area.height,
1139    };
1140    f.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
1141}
1142
1143// Helper function to highlight search matches in text
1144fn highlight_search_matches(
1145    text: &str,
1146    query: &str,
1147    normal_color: Color,
1148    highlight_color: Color,
1149) -> Vec<Span<'static>> {
1150    if query.is_empty() {
1151        return vec![Span::styled(
1152            text.to_string(),
1153            Style::default().fg(normal_color).add_modifier(Modifier::BOLD),
1154        )];
1155    }
1156
1157    let mut spans = Vec::new();
1158    let text_lower = text.to_lowercase();
1159    let query_lower = query.to_lowercase();
1160    let mut last_end = 0;
1161
1162    // Find all matches
1163    for match_start in text_lower.match_indices(&query_lower).map(|(i, _)| i) {
1164        // Add text before match
1165        if match_start > last_end {
1166            spans.push(Span::styled(
1167                text[last_end..match_start].to_string(),
1168                Style::default().fg(normal_color).add_modifier(Modifier::BOLD),
1169            ));
1170        }
1171
1172        // Add highlighted match
1173        let match_end = match_start + query.len();
1174        spans.push(Span::styled(
1175            text[match_start..match_end].to_string(),
1176            Style::default()
1177                .fg(highlight_color)
1178                .bg(Color::Rgb(60, 60, 0))
1179                .add_modifier(Modifier::BOLD),
1180        ));
1181
1182        last_end = match_end;
1183    }
1184
1185    // Add remaining text
1186    if last_end < text.len() {
1187        spans.push(Span::styled(
1188            text[last_end..].to_string(),
1189            Style::default().fg(normal_color).add_modifier(Modifier::BOLD),
1190        ));
1191    }
1192
1193    spans
1194}
1195
1196#[allow(clippy::too_many_lines)]
1197fn draw_categories_section(f: &mut Frame, app: &App, area: Rect) {
1198    let ItemCounts {
1199        total: _,
1200        passwords,
1201        env_vars,
1202        notes,
1203        api_keys,
1204        ssh_keys,
1205        certificates,
1206        databases,
1207        credit_cards,
1208        secure_notes,
1209        identities,
1210        servers,
1211        wifi_passwords,
1212        licenses,
1213        bank_accounts,
1214        documents,
1215        recovery_codes,
1216        oauth_tokens,
1217    } = app.get_item_counts();
1218
1219    let categories_content = vec![
1220        Line::from(vec![
1221            Span::styled("🔐 ", Style::default().fg(c_badge_pwd())),
1222            Span::styled(format!("Passwords ({passwords})"), Style::default().fg(c_text())),
1223        ]),
1224        Line::from(vec![
1225            Span::styled("🌍 ", Style::default().fg(c_badge_env())),
1226            Span::styled(format!("Environment ({env_vars})"), Style::default().fg(c_text())),
1227        ]),
1228        Line::from(vec![
1229            Span::styled("📝 ", Style::default().fg(c_badge_note())),
1230            Span::styled(format!("Notes ({notes})"), Style::default().fg(c_text())),
1231        ]),
1232        Line::from(vec![
1233            Span::styled("🔑 ", Style::default().fg(c_badge_api())),
1234            Span::styled(format!("API Keys ({api_keys})"), Style::default().fg(c_text())),
1235        ]),
1236        Line::from(vec![
1237            Span::styled("🔒 ", Style::default().fg(c_badge_ssh())),
1238            Span::styled(format!("SSH Keys ({ssh_keys})"), Style::default().fg(c_text())),
1239        ]),
1240        Line::from(vec![
1241            Span::styled("📜 ", Style::default().fg(c_badge_cert())),
1242            Span::styled(format!("Certificates ({certificates})"), Style::default().fg(c_text())),
1243        ]),
1244        Line::from(vec![
1245            Span::styled("🗄️ ", Style::default().fg(c_badge_db())),
1246            Span::styled(format!("Databases ({databases})"), Style::default().fg(c_text())),
1247        ]),
1248        Line::from(vec![
1249            Span::styled("💳 ", Style::default().fg(c_badge_creditcard())),
1250            Span::styled(format!("Credit Cards ({credit_cards})"), Style::default().fg(c_text())),
1251        ]),
1252        Line::from(vec![
1253            Span::styled("🔒 ", Style::default().fg(c_badge_securenote())),
1254            Span::styled(format!("Secure Notes ({secure_notes})"), Style::default().fg(c_text())),
1255        ]),
1256        Line::from(vec![
1257            Span::styled("🆔 ", Style::default().fg(c_badge_identity())),
1258            Span::styled(format!("Identities ({identities})"), Style::default().fg(c_text())),
1259        ]),
1260        Line::from(vec![
1261            Span::styled("🖥️ ", Style::default().fg(c_badge_server())),
1262            Span::styled(format!("Servers ({servers})"), Style::default().fg(c_text())),
1263        ]),
1264        Line::from(vec![
1265            Span::styled("📶 ", Style::default().fg(c_badge_wifi())),
1266            Span::styled(format!("WiFi ({wifi_passwords})"), Style::default().fg(c_text())),
1267        ]),
1268        Line::from(vec![
1269            Span::styled("📄 ", Style::default().fg(c_badge_license())),
1270            Span::styled(format!("Licenses ({licenses})"), Style::default().fg(c_text())),
1271        ]),
1272        Line::from(vec![
1273            Span::styled("🏦 ", Style::default().fg(c_badge_bankaccount())),
1274            Span::styled(
1275                format!("Bank Accounts ({bank_accounts})"),
1276                Style::default().fg(c_text()),
1277            ),
1278        ]),
1279        Line::from(vec![
1280            Span::styled("📋 ", Style::default().fg(c_badge_document())),
1281            Span::styled(format!("Documents ({documents})"), Style::default().fg(c_text())),
1282        ]),
1283        Line::from(vec![
1284            Span::styled("🔄 ", Style::default().fg(c_badge_recovery())),
1285            Span::styled(format!("Recovery ({recovery_codes})"), Style::default().fg(c_text())),
1286        ]),
1287        Line::from(vec![
1288            Span::styled("🎫 ", Style::default().fg(c_badge_oauth())),
1289            Span::styled(format!("OAuth ({oauth_tokens})"), Style::default().fg(c_text())),
1290        ]),
1291    ];
1292
1293    let categories_block = Block::default()
1294        .borders(Borders::ALL)
1295        .border_type(BorderType::Rounded)
1296        .border_style(Style::default().fg(c_border()))
1297        .style(Style::default().bg(c_bg_panel()))
1298        .title(Span::styled(
1299            " Categories ",
1300            Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
1301        ));
1302
1303    let categories_paragraph = Paragraph::new(categories_content)
1304        .block(categories_block)
1305        .wrap(Wrap { trim: true })
1306        .style(Style::default().fg(c_text()));
1307
1308    f.render_widget(categories_paragraph, area);
1309}
1310
1311fn draw_help_section(f: &mut Frame, area: Rect) {
1312    let help_content = vec![
1313        Line::from(vec![
1314            Span::styled("a ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1315            Span::raw("Add item"),
1316        ]),
1317        Line::from(vec![
1318            Span::styled("e ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1319            Span::raw("Edit item"),
1320        ]),
1321        Line::from(vec![
1322            Span::styled("c ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1323            Span::raw("Copy value"),
1324        ]),
1325        Line::from(vec![
1326            Span::styled("s or / ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1327            Span::raw("Start search"),
1328        ]),
1329        Line::from(vec![
1330            Span::styled("g ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1331            Span::raw("Generate password"),
1332        ]),
1333        Line::from(vec![
1334            Span::styled("x ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1335            Span::raw("Export items"),
1336        ]),
1337        Line::from(vec![
1338            Span::styled("i ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1339            Span::raw("Import items"),
1340        ]),
1341        Line::from(vec![
1342            Span::styled("d ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1343            Span::raw("Delete selected"),
1344        ]),
1345        Line::from(vec![
1346            Span::styled("v ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1347            Span::raw("View item"),
1348        ]),
1349        Line::from(vec![
1350            Span::styled("k ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1351            Span::raw("Change master key"),
1352        ]),
1353        Line::from(vec![
1354            Span::styled("Ctrl+v ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1355            Span::raw("Paste clipboard"),
1356        ]),
1357        Line::from(vec![
1358            Span::styled("F2 ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1359            Span::raw("Vault registry"),
1360        ]),
1361        Line::from(vec![
1362            Span::styled("q ", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1363            Span::raw("Quit"),
1364        ]),
1365    ];
1366
1367    let help_block = Block::default()
1368        .borders(Borders::ALL)
1369        .border_type(BorderType::Rounded)
1370        .border_style(Style::default().fg(c_border()))
1371        .style(Style::default().bg(c_bg_panel()))
1372        .title(Span::styled(
1373            " Help ",
1374            Style::default().fg(c_accent2()).add_modifier(Modifier::BOLD),
1375        ));
1376
1377    let help_paragraph = Paragraph::new(help_content)
1378        .block(help_block)
1379        .wrap(Wrap { trim: true })
1380        .style(Style::default().fg(c_text()));
1381
1382    f.render_widget(help_paragraph, area);
1383}
1384fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) {
1385    // Create the status bar background
1386    let status_block = Block::default()
1387        .borders(Borders::TOP)
1388        .border_style(Style::default().fg(c_border()))
1389        .style(Style::default().bg(c_bg_panel()));
1390
1391    f.render_widget(status_block, area);
1392
1393    // Check if we need to show auto-lock countdown
1394    let show_countdown = app
1395        .get_countdown_info()
1396        .is_some_and(|info| info.minutes_left <= 3 && info.enabled);
1397
1398    if show_countdown {
1399        // Split into three parts: status message, countdown, key hints
1400        let chunks = Layout::default()
1401            .direction(Direction::Horizontal)
1402            .constraints([
1403                Constraint::Percentage(50), // Status message
1404                Constraint::Length(20),     // Countdown
1405                Constraint::Min(20),        // Key hints
1406            ])
1407            .split(area);
1408
1409        // Left side: Status message
1410        let (message, message_style) = get_status_message_and_style(app);
1411        let status_paragraph = Paragraph::new(Line::from(vec![
1412            Span::raw(" "), // Small padding
1413            Span::styled(message, message_style),
1414        ]))
1415        .style(Style::default().bg(c_bg_panel()))
1416        .wrap(Wrap { trim: true });
1417
1418        f.render_widget(status_paragraph, chunks[0]);
1419
1420        // Middle: Auto-lock countdown
1421        if let Some(countdown_info) = app.get_countdown_info() {
1422            let countdown_text = if countdown_info.seconds_left > 0 {
1423                format!(
1424                    "🔒 {}m{}s",
1425                    countdown_info.minutes_left,
1426                    countdown_info.seconds_left % 60
1427                )
1428            } else {
1429                "🔒 Locking...".to_string()
1430            };
1431
1432            let countdown_color = if countdown_info.minutes_left <= 1 {
1433                Color::Red
1434            } else if countdown_info.minutes_left <= 2 {
1435                Color::Yellow
1436            } else {
1437                Color::Cyan
1438            };
1439
1440            let countdown_paragraph = Paragraph::new(Line::from(vec![Span::styled(
1441                countdown_text,
1442                Style::default().fg(countdown_color).add_modifier(Modifier::BOLD),
1443            )]))
1444            .style(Style::default().bg(c_bg_panel()))
1445            .alignment(Alignment::Center);
1446
1447            f.render_widget(countdown_paragraph, chunks[1]);
1448        }
1449
1450        // Right side: Key hints
1451        let key_hints = get_key_hints_for_screen(app);
1452        let hints_paragraph = Paragraph::new(Line::from(key_hints))
1453            .style(Style::default().bg(c_bg_panel()).fg(c_text_dim()))
1454            .alignment(Alignment::Right)
1455            .wrap(Wrap { trim: true });
1456
1457        f.render_widget(hints_paragraph, chunks[2]);
1458    } else {
1459        // Normal layout without countdown
1460        let chunks = Layout::default()
1461            .direction(Direction::Horizontal)
1462            .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
1463            .split(area);
1464
1465        // Left side: Status message
1466        let (message, message_style) = get_status_message_and_style(app);
1467        let status_paragraph = Paragraph::new(Line::from(vec![
1468            Span::raw(" "), // Small padding
1469            Span::styled(message, message_style),
1470        ]))
1471        .style(Style::default().bg(c_bg_panel()))
1472        .wrap(Wrap { trim: true });
1473
1474        f.render_widget(status_paragraph, chunks[0]);
1475
1476        // Right side: Key hints
1477        let key_hints = get_key_hints_for_screen(app);
1478        let hints_paragraph = Paragraph::new(Line::from(key_hints))
1479            .style(Style::default().bg(c_bg_panel()).fg(c_text_dim()))
1480            .alignment(Alignment::Right)
1481            .wrap(Wrap { trim: true });
1482
1483        f.render_widget(hints_paragraph, chunks[1]);
1484    }
1485}
1486
1487fn get_status_message_and_style(app: &App) -> (String, Style) {
1488    if let Some(message) = &app.status_message {
1489        let style = match app.status_type {
1490            StatusType::Success => Style::default().fg(c_ok()).add_modifier(Modifier::BOLD),
1491            StatusType::Warning => Style::default().fg(c_warn()).add_modifier(Modifier::BOLD),
1492            StatusType::Error => Style::default().fg(c_err()).add_modifier(Modifier::BOLD),
1493            StatusType::Info => Style::default().fg(c_accent()),
1494        };
1495        (message.clone(), style)
1496    } else {
1497        // Default context message based on the current screen
1498        let context_message = match app.screen {
1499            Screen::Unlock => "Enter your master key to unlock the vault".to_string(),
1500            Screen::Main => {
1501                format!(" {} items in vault", app.items.len())
1502            }
1503            Screen::AddItem => "Fill in the item details and press Enter to save".to_string(),
1504            Screen::ViewItem => "Viewing item details".to_string(),
1505            Screen::EditItem => "Edit the item value and press Enter to save".to_string(),
1506            Screen::ChangeMaster => "Change your master key".to_string(),
1507            Screen::GeneratePassword => "Configure and generate a new password".to_string(),
1508            Screen::ImportExport => match app.ie_mode {
1509                crate::app::ImportExportMode::Export => "Export items to file".to_string(),
1510                crate::app::ImportExportMode::Import => "Import items from file".to_string(),
1511            },
1512            Screen::VaultSelector => "Select a vault to open".to_string(),
1513        };
1514        (context_message, Style::default().fg(c_text_dim()))
1515    }
1516}
1517
1518fn get_key_hints_for_screen(app: &App) -> Vec<Span<'static>> {
1519    let mut spans = Vec::new();
1520
1521    let add_hint = |spans: &mut Vec<Span<'static>>, key: &'static str, action: &'static str, emphasized: bool| {
1522        if !spans.is_empty() {
1523            spans.push(Span::raw(" "));
1524        }
1525        let key_style = if emphasized {
1526            Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)
1527        } else {
1528            Style::default().fg(c_text()).add_modifier(Modifier::BOLD)
1529        };
1530        spans.push(Span::styled(format!("[{key}]"), key_style));
1531        spans.push(Span::styled(format!(" {action}"), Style::default().fg(c_text_dim())));
1532    };
1533
1534    match app.screen {
1535        Screen::Unlock => {
1536            add_hint(&mut spans, "Tab", "Switch", false);
1537            add_hint(&mut spans, "Enter", "Unlock", true);
1538        }
1539        Screen::Main => {
1540            if app.search_mode {
1541                // When in search mode, show search-specific hints
1542                add_hint(&mut spans, "Type", "Search", true);
1543                add_hint(&mut spans, "Enter", "Confirm", false);
1544                add_hint(&mut spans, "Esc", "Exit Search", false);
1545            } else {
1546                // Normal main screen navigation
1547                add_hint(&mut spans, "↑↓", "Navigate", false);
1548
1549                if !app.filtered_items.is_empty() {
1550                    add_hint(&mut spans, "Enter", "View", true);
1551                    add_hint(&mut spans, "e", "Edit", false);
1552                    add_hint(&mut spans, "c", "Copy", false);
1553                    add_hint(&mut spans, "Del", "Delete", false);
1554                }
1555
1556                add_hint(&mut spans, "a", "Add", false);
1557                add_hint(&mut spans, "/", "Search", false);
1558
1559                // Show different Esc behavior based on search state
1560                if app.search_query.is_empty() {
1561                    add_hint(&mut spans, "q", "Quit", false);
1562                } else {
1563                    add_hint(&mut spans, "Esc", "Clear Search", false);
1564                }
1565
1566                add_hint(&mut spans, "g", "Generate", false);
1567                add_hint(&mut spans, "i", "Import", false);
1568                add_hint(&mut spans, "o", "Export", false);
1569                add_hint(&mut spans, "v", "Vaults", false);
1570            }
1571        }
1572        Screen::AddItem => {
1573            add_hint(&mut spans, "Tab", "Next Field", false);
1574            add_hint(&mut spans, "Ctrl+V", "Paste", false);
1575            add_hint(&mut spans, "Enter", "Save", true);
1576            add_hint(&mut spans, "Esc", "Cancel", false);
1577        }
1578        Screen::ViewItem => {
1579            add_hint(&mut spans, "v", "Toggle Value", true);
1580            add_hint(&mut spans, "e", "Edit", false);
1581            add_hint(&mut spans, "c", "Copy", false);
1582            add_hint(&mut spans, "Esc", "Back", false);
1583        }
1584        Screen::EditItem => {
1585            add_hint(&mut spans, "Enter", "Save", true);
1586            add_hint(&mut spans, "Esc", "Cancel", false);
1587        }
1588        Screen::ChangeMaster => {
1589            add_hint(&mut spans, "Tab", "Next Field", false);
1590            add_hint(&mut spans, "Enter", "Change", true);
1591            add_hint(&mut spans, "Esc", "Cancel", false);
1592        }
1593        Screen::GeneratePassword => {
1594            add_hint(&mut spans, "Space", "Generate", true);
1595            add_hint(&mut spans, "c", "Copy", false);
1596            add_hint(&mut spans, "u", "Use", false);
1597            add_hint(&mut spans, "Esc", "Back", false);
1598        }
1599        Screen::ImportExport => {
1600            add_hint(&mut spans, "Tab", "Next Field", false);
1601            add_hint(&mut spans, "Enter", "Execute", true);
1602            add_hint(&mut spans, "Esc", "Cancel", false);
1603        }
1604        Screen::VaultSelector => {
1605            add_hint(&mut spans, "↑↓", "Navigate", false);
1606            add_hint(&mut spans, "Tab", "Next Field", false);
1607            add_hint(&mut spans, "Enter", "Select", true);
1608            add_hint(&mut spans, "Esc", "Close", false);
1609        }
1610    }
1611
1612    // Add a trailing space for padding
1613    spans.push(Span::raw(" "));
1614    spans
1615}
1616
1617#[allow(clippy::too_many_lines)]
1618fn draw_add_item(f: &mut Frame, app: &App) {
1619    // Create a centered modal area
1620    let modal_area = centered_rect(70, 80, f.area());
1621
1622    // Clear the area to avoid visual artifacts
1623    f.render_widget(Clear, modal_area);
1624
1625    let block = Block::default()
1626        .title("Add Item")
1627        .borders(Borders::ALL)
1628        .border_style(Style::default().fg(c_border()))
1629        .style(Style::default().bg(c_bg_panel()));
1630
1631    let inner = block.inner(modal_area);
1632    f.render_widget(block, modal_area);
1633
1634    let chunks = Layout::default()
1635        .direction(Direction::Vertical)
1636        .margin(1)
1637        .constraints([
1638            Constraint::Length(3), // Name input
1639            Constraint::Length(3), // Kind selection
1640            Constraint::Min(5),    // Value input (expanded for multi-line)
1641            Constraint::Length(5), // Instructions
1642        ])
1643        .split(inner);
1644
1645    // Name input
1646    let name_block = Block::default().title("Name").borders(Borders::ALL).border_style(
1647        if matches!(app.add_focus, AddItemField::Name) {
1648            Style::default().fg(c_accent())
1649        } else {
1650            Style::default().fg(c_border())
1651        },
1652    );
1653    let name_input = Paragraph::new(app.add_name.as_str())
1654        .block(name_block)
1655        .style(Style::default().fg(c_text()));
1656    f.render_widget(name_input, chunks[0]);
1657
1658    // Kind selection
1659    let selected_kind = ItemKind::all()[app.add_kind_idx.min(ItemKind::all().len() - 1)];
1660    let kind_text = format!("◄ {} ►", selected_kind.display_name());
1661    let kind_block = Block::default().title("Type").borders(Borders::ALL).border_style(
1662        if matches!(app.add_focus, AddItemField::Kind) {
1663            Style::default().fg(c_accent())
1664        } else {
1665            Style::default().fg(c_border())
1666        },
1667    );
1668    let kind_widget = Paragraph::new(kind_text)
1669        .block(kind_block)
1670        .style(Style::default().fg(if matches!(app.add_focus, AddItemField::Kind) {
1671            c_accent()
1672        } else {
1673            c_text()
1674        }))
1675        .alignment(Alignment::Center);
1676    f.render_widget(kind_widget, chunks[1]);
1677
1678    let value_title = get_value_title_for_kind(selected_kind);
1679
1680    // Create a mutable textarea for rendering
1681    let mut textarea = app.add_value_textarea.clone();
1682
1683    // Set the block with proper styling
1684    textarea.set_block(
1685        Block::default()
1686            .title(format!("{value_title} (Enter for new line, Ctrl+Enter to save)"))
1687            .borders(Borders::ALL)
1688            .border_style(if matches!(app.add_focus, AddItemField::Value) {
1689                Style::default().fg(c_accent())
1690            } else {
1691                Style::default().fg(c_border())
1692            }),
1693    );
1694
1695    // Set text and cursor styles
1696    textarea.set_style(Style::default().fg(c_text()));
1697
1698    // Set cursor style when focused
1699    if matches!(app.add_focus, AddItemField::Value) {
1700        textarea.set_cursor_line_style(Style::default().bg(c_bg()));
1701        textarea.set_cursor_style(Style::default().bg(c_accent()));
1702    }
1703
1704    // Enable line numbers with styling
1705    textarea.set_line_number_style(Style::default().fg(c_text_dim()));
1706
1707    f.render_widget(&textarea, chunks[2]);
1708
1709    // Instructions
1710    let instructions = match app.add_focus {
1711        AddItemField::Name => {
1712            vec![Line::from(vec![
1713                Span::styled("Enter ", Style::default().fg(c_text_dim())),
1714                Span::styled("Tab", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1715                Span::styled(" to continue", Style::default().fg(c_text_dim())),
1716            ])]
1717        }
1718        AddItemField::Kind => {
1719            vec![Line::from(vec![
1720                Span::styled("Use ", Style::default().fg(c_text_dim())),
1721                Span::styled("←/→", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1722                Span::styled(" to select, ", Style::default().fg(c_text_dim())),
1723                Span::styled("Tab", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1724                Span::styled(" to continue", Style::default().fg(c_text_dim())),
1725            ])]
1726        }
1727        AddItemField::Value => {
1728            vec![
1729                Line::from(vec![
1730                    Span::styled("Enter the ", Style::default().fg(c_text_dim())),
1731                    Span::styled(
1732                        get_value_title_for_kind(selected_kind).to_lowercase(),
1733                        Style::default().fg(c_accent()),
1734                    ),
1735                    Span::styled(" value", Style::default().fg(c_text_dim())),
1736                ]),
1737                Line::from(vec![
1738                    Span::styled("Press ", Style::default().fg(c_text_dim())),
1739                    Span::styled("Enter", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1740                    Span::styled(" for new line, ", Style::default().fg(c_text_dim())),
1741                    Span::styled(
1742                        "Ctrl+Enter",
1743                        Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
1744                    ),
1745                    Span::styled(" to save", Style::default().fg(c_text_dim())),
1746                ]),
1747                Line::from(vec![
1748                    Span::styled("Press ", Style::default().fg(c_text_dim())),
1749                    Span::styled("Ctrl+v", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1750                    Span::styled(" to paste content from clipboard, ", Style::default().fg(c_text_dim())),
1751                ]),
1752            ]
1753        }
1754    };
1755
1756    let instr_block = Block::default()
1757        .borders(Borders::ALL)
1758        .border_style(Style::default().fg(c_border()));
1759    let instr_text = Paragraph::new(instructions)
1760        .block(instr_block)
1761        .style(Style::default().fg(c_text_dim()))
1762        .wrap(Wrap { trim: false });
1763    f.render_widget(instr_text, chunks[3]);
1764}
1765
1766const fn get_value_title_for_kind(kind: ItemKind) -> &'static str {
1767    match kind {
1768        ItemKind::Password => "Password",
1769        ItemKind::EnvVar => "Environment Variable Value",
1770        ItemKind::Note => "Note Content",
1771        ItemKind::ApiKey => "API Key / Token",
1772        ItemKind::SshKey => "SSH Private Key",
1773        ItemKind::Certificate => "Certificate (PEM format)",
1774        ItemKind::Database => "Connection String",
1775        ItemKind::CreditCard => "Card Details",
1776        ItemKind::SecureNote => "Secure Note Content",
1777        ItemKind::Identity => "Identity Information",
1778        ItemKind::Server => "Server Credentials",
1779        ItemKind::WifiPassword => "WiFi Password",
1780        ItemKind::License => "License Key",
1781        ItemKind::BankAccount => "Account Details",
1782        ItemKind::Document => "Document Content",
1783        ItemKind::Recovery => "Recovery Codes",
1784        ItemKind::OAuth => "OAuth Token",
1785    }
1786}
1787
1788#[allow(clippy::too_many_lines)]
1789fn draw_import_export(f: &mut Frame, app: &App) {
1790    let area = centered_rect(80, 70, f.area());
1791    f.render_widget(Clear, area);
1792
1793    let title = match app.ie_mode {
1794        ImportExportMode::Export => " Export Items ",
1795        ImportExportMode::Import => " Import Items ",
1796    };
1797
1798    let block = Block::default()
1799        .borders(Borders::ALL)
1800        .border_type(BorderType::Rounded)
1801        .border_style(Style::default().fg(c_border()))
1802        .style(Style::default().bg(c_bg_panel()).fg(c_text()))
1803        .title(Span::styled(
1804            title,
1805            Style::default().fg(c_accent2()).add_modifier(Modifier::BOLD),
1806        ));
1807    f.render_widget(block, area);
1808
1809    let inner = Layout::default()
1810        .direction(Direction::Vertical)
1811        .constraints([
1812            Constraint::Length(2), // Description
1813            Constraint::Length(5), // Path field with hints
1814            Constraint::Length(3), // Format field
1815            Constraint::Length(3), // Action button
1816            Constraint::Length(3), // Actions
1817            Constraint::Length(2), // Error
1818        ])
1819        .split(pad(area, 2, 2));
1820
1821    // Description
1822    let desc_text = match app.ie_mode {
1823        ImportExportMode::Export => format!("Export {} items to file", app.items.len()),
1824        ImportExportMode::Import => "Import items from file".to_string(),
1825    };
1826    let desc = Paragraph::new(vec![
1827        Line::from(vec![Span::styled(&desc_text, Style::default().fg(c_text_dim()))]),
1828        Line::from(vec![Span::styled(
1829            "Tip: Use / for paths on all systems, ~ for home directory",
1830            Style::default().fg(c_text_dim()),
1831        )]),
1832    ]);
1833    f.render_widget(desc, inner[0]);
1834
1835    let focused = |field: ImportExportField| app.ie_focus == field;
1836
1837    // Path field with better hints
1838    let path_hint = match app.ie_mode {
1839        ImportExportMode::Export => "e.g., ~/Documents/backup.json or C:/backup.json",
1840        ImportExportMode::Import => "e.g., ~/Documents/data.csv or /path/to/import.json",
1841    };
1842
1843    let path_block = Block::default()
1844        .borders(Borders::ALL)
1845        .border_type(BorderType::Rounded)
1846        .border_style(if focused(ImportExportField::Path) {
1847            Style::default().fg(c_accent())
1848        } else {
1849            Style::default().fg(c_border())
1850        })
1851        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
1852        .title(Span::styled(" File Path ", Style::default().fg(c_text_dim())));
1853
1854    let path_content = if app.ie_path.is_empty() && !focused(ImportExportField::Path) {
1855        Paragraph::new(vec![Line::from(Span::styled(
1856            path_hint,
1857            Style::default().fg(c_text_dim()),
1858        ))])
1859    } else {
1860        Paragraph::new(vec![
1861            Line::from(&*app.ie_path),
1862            Line::from(Span::styled(path_hint, Style::default().fg(c_text_dim()))),
1863        ])
1864    };
1865
1866    let path_display = path_content.block(path_block).style(Style::default().fg(c_text()));
1867    f.render_widget(path_display, inner[1]);
1868
1869    // Format field
1870    let format_display = format!("< {} >", app.ie_formats[app.ie_format_idx]);
1871    let format_block = Block::default()
1872        .borders(Borders::ALL)
1873        .border_type(BorderType::Rounded)
1874        .border_style(if focused(ImportExportField::Format) {
1875            Style::default().fg(c_accent())
1876        } else {
1877            Style::default().fg(c_border())
1878        })
1879        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
1880        .title(Span::styled(" Format ", Style::default().fg(c_text_dim())));
1881
1882    let format_content = Paragraph::new(Line::from(vec![
1883        Span::styled(
1884            &format_display,
1885            if focused(ImportExportField::Format) {
1886                Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)
1887            } else {
1888                Style::default().fg(c_text())
1889            },
1890        ),
1891        if focused(ImportExportField::Format) {
1892            Span::styled("  [←/→ to change]", Style::default().fg(c_text_dim()))
1893        } else {
1894            Span::raw("")
1895        },
1896    ]))
1897    .block(format_block);
1898    f.render_widget(format_content, inner[2]);
1899
1900    // Action button
1901    let action_text = match app.ie_mode {
1902        ImportExportMode::Export => "Export",
1903        ImportExportMode::Import => "Import",
1904    };
1905    let action_block = Block::default()
1906        .borders(Borders::ALL)
1907        .border_type(BorderType::Rounded)
1908        .border_style(if focused(ImportExportField::Action) {
1909            Style::default().fg(c_ok())
1910        } else {
1911            Style::default().fg(c_border())
1912        })
1913        .style(Style::default().bg(if focused(ImportExportField::Action) {
1914            Color::Rgb(20, 40, 20)
1915        } else {
1916            Color::Rgb(30, 32, 40)
1917        }));
1918
1919    let action_content = Paragraph::new(Line::from(vec![Span::styled(
1920        format!("  {action_text}  "),
1921        Style::default().fg(c_ok()).add_modifier(Modifier::BOLD),
1922    )]))
1923    .block(action_block);
1924    f.render_widget(action_content, inner[3]);
1925
1926    // Actions with file path help
1927    let actions = Paragraph::new(vec![
1928        Line::from(vec![
1929            Span::styled("[Tab]", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
1930            Span::styled(" switch   ", Style::default().fg(c_text_dim())),
1931            Span::styled("[Enter]", Style::default().fg(c_ok()).add_modifier(Modifier::BOLD)),
1932            Span::styled(" execute   ", Style::default().fg(c_text_dim())),
1933            Span::styled("[Esc]", Style::default().fg(c_err()).add_modifier(Modifier::BOLD)),
1934            Span::styled(" cancel", Style::default().fg(c_text_dim())),
1935        ]),
1936        Line::from(vec![
1937            Span::styled("Examples: ", Style::default().fg(c_text_dim())),
1938            Span::styled("./backup.json", Style::default().fg(c_accent())),
1939            Span::styled(", ", Style::default().fg(c_text_dim())),
1940            Span::styled("~/Documents/vault.csv", Style::default().fg(c_accent())),
1941        ]),
1942    ]);
1943    f.render_widget(actions, inner[4]);
1944
1945    // Error/status display
1946    if let Some(err) = &app.error {
1947        let color = if err.contains("Exported") || err.contains("Imported") {
1948            c_ok()
1949        } else {
1950            c_err()
1951        };
1952        let err_p = Paragraph::new(Span::styled(
1953            err.clone(),
1954            Style::default().fg(color).add_modifier(Modifier::BOLD),
1955        ));
1956        f.render_widget(err_p, inner[5]);
1957    }
1958}
1959
1960#[allow(clippy::too_many_lines)]
1961fn draw_generate_password(f: &mut Frame, app: &App) {
1962    let area = centered_rect(75, 80, f.area());
1963    f.render_widget(Clear, area);
1964
1965    let block = Block::default()
1966        .borders(Borders::ALL)
1967        .border_type(BorderType::Rounded)
1968        .border_style(Style::default().fg(c_border()))
1969        .style(Style::default().bg(c_bg_panel()).fg(c_text()))
1970        .title(Span::styled(
1971            " Password Generator ",
1972            Style::default().fg(c_accent2()).add_modifier(Modifier::BOLD),
1973        ));
1974    f.render_widget(block, area);
1975
1976    let inner = Layout::default()
1977        .direction(Direction::Vertical)
1978        .constraints([
1979            Constraint::Length(2), // Title
1980            Constraint::Length(3), // Length field
1981            Constraint::Length(8), // Options
1982            Constraint::Length(4), // Generated password
1983            Constraint::Length(2), // Actions
1984            Constraint::Length(2), // Error
1985        ])
1986        .split(pad(area, 2, 2));
1987
1988    // Title
1989    let title = Paragraph::new(Line::from(vec![Span::styled(
1990        "Configure and generate secure passwords",
1991        Style::default().fg(c_text_dim()),
1992    )]));
1993    f.render_widget(title, inner[0]);
1994
1995    // Length field
1996    let length_block = Block::default()
1997        .borders(Borders::ALL)
1998        .border_type(BorderType::Rounded)
1999        .border_style(if matches!(app.gen_focus, PasswordGenField::Length) {
2000            Style::default().fg(c_accent())
2001        } else {
2002            Style::default().fg(c_border())
2003        })
2004        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2005        .title(Span::styled(" Length ", Style::default().fg(c_text_dim())));
2006
2007    let length_content = Paragraph::new(&*app.gen_length_str)
2008        .block(length_block)
2009        .style(Style::default().fg(c_text()));
2010    f.render_widget(length_content, inner[1]);
2011
2012    // Options
2013    let options_block = Block::default()
2014        .borders(Borders::ALL)
2015        .border_type(BorderType::Rounded)
2016        .border_style(if matches!(app.gen_focus, PasswordGenField::Options) {
2017            Style::default().fg(c_accent())
2018        } else {
2019            Style::default().fg(c_border())
2020        })
2021        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2022        .title(Span::styled(" Options ", Style::default().fg(c_text_dim())));
2023
2024    let check_mark = |enabled: bool| if enabled { "☑" } else { "☐" };
2025    let options_lines = vec![
2026        Line::from(vec![
2027            Span::styled("[1] ", Style::default().fg(c_accent())),
2028            Span::styled(
2029                check_mark(app.gen_config.include_uppercase),
2030                Style::default().fg(c_text()),
2031            ),
2032            Span::styled(" Uppercase letters (A-Z)", Style::default().fg(c_text())),
2033        ]),
2034        Line::from(vec![
2035            Span::styled("[2] ", Style::default().fg(c_accent())),
2036            Span::styled(
2037                check_mark(app.gen_config.include_lowercase),
2038                Style::default().fg(c_text()),
2039            ),
2040            Span::styled(" Lowercase letters (a-z)", Style::default().fg(c_text())),
2041        ]),
2042        Line::from(vec![
2043            Span::styled("[3] ", Style::default().fg(c_accent())),
2044            Span::styled(check_mark(app.gen_config.include_digits), Style::default().fg(c_text())),
2045            Span::styled(" Digits (0-9)", Style::default().fg(c_text())),
2046        ]),
2047        Line::from(vec![
2048            Span::styled("[4] ", Style::default().fg(c_accent())),
2049            Span::styled(
2050                check_mark(app.gen_config.include_symbols),
2051                Style::default().fg(c_text()),
2052            ),
2053            Span::styled(" Symbols (!@#$%^&*...)", Style::default().fg(c_text())),
2054        ]),
2055        Line::from(vec![
2056            Span::styled("[5] ", Style::default().fg(c_accent())),
2057            Span::styled(
2058                check_mark(app.gen_config.exclude_ambiguous),
2059                Style::default().fg(c_text()),
2060            ),
2061            Span::styled(" Exclude ambiguous (0,O,1,l,I)", Style::default().fg(c_text())),
2062        ]),
2063    ];
2064
2065    let options_content = Paragraph::new(options_lines).block(options_block);
2066    f.render_widget(options_content, inner[2]);
2067
2068    // Generated password display
2069    let pwd_block = Block::default()
2070        .borders(Borders::ALL)
2071        .border_type(BorderType::Rounded)
2072        .border_style(Style::default().fg(c_border()))
2073        .style(Style::default().bg(Color::Rgb(20, 25, 35)))
2074        .title(Span::styled(" Generated Password ", Style::default().fg(c_warn())));
2075
2076    let pwd_content = if let Some(password) = &app.generated_password {
2077        Paragraph::new(password.clone())
2078            .style(Style::default().fg(c_accent()).add_modifier(Modifier::BOLD))
2079            .wrap(Wrap { trim: true })
2080    } else {
2081        Paragraph::new("Press 'g' or Enter to generate").style(Style::default().fg(c_text_dim()))
2082    };
2083    let pwd_display = pwd_content.block(pwd_block);
2084    f.render_widget(pwd_display, inner[3]);
2085
2086    // Actions
2087    let actions = Paragraph::new(Line::from(vec![
2088        Span::styled("[Tab]", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
2089        Span::styled(" switch   ", Style::default().fg(c_text_dim())),
2090        Span::styled("[g/Enter]", Style::default().fg(c_ok()).add_modifier(Modifier::BOLD)),
2091        Span::styled(" generate   ", Style::default().fg(c_text_dim())),
2092        Span::styled("[c]", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
2093        Span::styled(" copy   ", Style::default().fg(c_text_dim())),
2094        Span::styled("[u]", Style::default().fg(c_warn()).add_modifier(Modifier::BOLD)),
2095        Span::styled(" use   ", Style::default().fg(c_text_dim())),
2096        Span::styled("[Esc]", Style::default().fg(c_err()).add_modifier(Modifier::BOLD)),
2097        Span::styled(" back", Style::default().fg(c_text_dim())),
2098    ]));
2099    f.render_widget(actions, inner[4]);
2100
2101    // Error display
2102    if let Some(err) = &app.error {
2103        let err_p = Paragraph::new(Span::styled(
2104            err.clone(),
2105            Style::default()
2106                .fg(if err.contains("copied") { c_ok() } else { c_err() })
2107                .add_modifier(Modifier::BOLD),
2108        ));
2109        f.render_widget(err_p, inner[5]);
2110    }
2111}
2112
2113fn draw_change_master(f: &mut Frame, app: &App) {
2114    let area = centered_rect(65, 70, f.area());
2115    f.render_widget(Clear, area);
2116    let block = Block::default()
2117        .borders(Borders::ALL)
2118        .border_type(BorderType::Rounded)
2119        .border_style(Style::default().fg(c_border()))
2120        .style(Style::default().bg(c_bg_panel()).fg(c_text()))
2121        .title(Span::styled(
2122            " Change Master Key ",
2123            Style::default().fg(c_accent2()).add_modifier(Modifier::BOLD),
2124        ));
2125    f.render_widget(block, area);
2126
2127    let inner = Layout::default()
2128        .direction(Direction::Vertical)
2129        .constraints([
2130            Constraint::Length(2),
2131            Constraint::Length(3),
2132            Constraint::Length(3),
2133            Constraint::Length(3),
2134            Constraint::Length(2),
2135            Constraint::Length(2),
2136        ])
2137        .split(pad(area, 2, 2));
2138
2139    let subtitle = Paragraph::new(Line::from(vec![
2140        Span::styled("Policy: ", Style::default().fg(c_text_dim())),
2141        Span::styled("≥8 chars, upper, lower, digit", Style::default().fg(c_accent())),
2142    ]));
2143    f.render_widget(subtitle, inner[0]);
2144
2145    let focused = |foc: ChangeKeyField| app.ck_focus == foc;
2146
2147    let current_block = Block::default()
2148        .borders(Borders::ALL)
2149        .border_type(BorderType::Rounded)
2150        .border_style(if focused(ChangeKeyField::Current) {
2151            Style::default().fg(c_accent())
2152        } else {
2153            Style::default().fg(c_border())
2154        })
2155        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2156        .title(Span::styled(" Current master key ", Style::default().fg(c_text_dim())));
2157
2158    let current_content = Paragraph::new(mask(&app.ck_current))
2159        .block(current_block)
2160        .style(Style::default().fg(c_text()));
2161    f.render_widget(current_content, inner[1]);
2162
2163    let new_block = Block::default()
2164        .borders(Borders::ALL)
2165        .border_type(BorderType::Rounded)
2166        .border_style(if focused(ChangeKeyField::New) {
2167            Style::default().fg(c_accent())
2168        } else {
2169            Style::default().fg(c_border())
2170        })
2171        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2172        .title(Span::styled(" New master key ", Style::default().fg(c_text_dim())));
2173
2174    let new_content = Paragraph::new(mask(&app.ck_new))
2175        .block(new_block)
2176        .style(Style::default().fg(c_text()));
2177    f.render_widget(new_content, inner[2]);
2178
2179    let confirm_block = Block::default()
2180        .borders(Borders::ALL)
2181        .border_type(BorderType::Rounded)
2182        .border_style(if focused(ChangeKeyField::Confirm) {
2183            Style::default().fg(c_accent())
2184        } else {
2185            Style::default().fg(c_border())
2186        })
2187        .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2188        .title(Span::styled(
2189            " Confirm new master key ",
2190            Style::default().fg(c_text_dim()),
2191        ));
2192
2193    let confirm_content = Paragraph::new(mask(&app.ck_confirm))
2194        .block(confirm_block)
2195        .style(Style::default().fg(c_text()));
2196    f.render_widget(confirm_content, inner[3]);
2197
2198    let hints = Paragraph::new(Line::from(vec![
2199        Span::styled("[Tab]", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
2200        Span::styled(" switch  ", Style::default().fg(c_text_dim())),
2201        Span::styled("[Enter]", Style::default().fg(c_ok()).add_modifier(Modifier::BOLD)),
2202        Span::styled(" apply  ", Style::default().fg(c_text_dim())),
2203        Span::styled("[Esc]", Style::default().fg(c_err()).add_modifier(Modifier::BOLD)),
2204        Span::styled(" cancel", Style::default().fg(c_text_dim())),
2205    ]));
2206    f.render_widget(hints, inner[4]);
2207
2208    if let Some(err) = &app.error {
2209        let err_p = Paragraph::new(Span::styled(
2210            err.clone(),
2211            Style::default().fg(c_err()).add_modifier(Modifier::BOLD),
2212        ));
2213        f.render_widget(err_p, inner[5]);
2214    }
2215}
2216
2217#[allow(clippy::too_many_lines)]
2218fn draw_view_item(f: &mut Frame, app: &App) {
2219    if let Some(item) = &app.view_item {
2220        let area = centered_rect(70, 60, f.area());
2221        f.render_widget(Clear, area);
2222
2223        let block = Block::default()
2224            .borders(Borders::ALL)
2225            .border_type(BorderType::Rounded)
2226            .border_style(Style::default().fg(c_border()))
2227            .style(Style::default().bg(c_bg_panel()).fg(c_text()))
2228            .title(Span::styled(
2229                " View Item ",
2230                Style::default().fg(c_accent2()).add_modifier(Modifier::BOLD),
2231            ));
2232        f.render_widget(block, area);
2233
2234        let inner = Layout::default()
2235            .direction(Direction::Vertical)
2236            .constraints([
2237                Constraint::Length(3),
2238                Constraint::Length(3),
2239                Constraint::Length(6),
2240                Constraint::Length(3),
2241                Constraint::Length(3),
2242                Constraint::Length(2),
2243            ])
2244            .split(pad(area, 2, 2));
2245
2246        let name_block = Block::default()
2247            .borders(Borders::ALL)
2248            .border_type(BorderType::Rounded)
2249            .border_style(Style::default().fg(c_border()))
2250            .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2251            .title(Span::styled(" Name ", Style::default().fg(c_text_dim())));
2252        let name_content = Paragraph::new(&*item.name)
2253            .block(name_block)
2254            .style(Style::default().fg(c_text()));
2255        f.render_widget(name_content, inner[0]);
2256
2257        let (badge, color) = match item.kind.as_str() {
2258            "password" => ("Password", c_badge_pwd()),
2259            "env" => ("Environment Variable", c_badge_env()),
2260            _ => ("Note", c_badge_note()),
2261        };
2262        let kind_block = Block::default()
2263            .borders(Borders::ALL)
2264            .border_type(BorderType::Rounded)
2265            .border_style(Style::default().fg(c_border()))
2266            .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2267            .title(Span::styled(" Kind ", Style::default().fg(c_text_dim())));
2268        let kind_content = Paragraph::new(Line::from(vec![Span::styled(
2269            format!(" {badge} "),
2270            Style::default().bg(color).fg(Color::Black).add_modifier(Modifier::BOLD),
2271        )]))
2272        .block(kind_block);
2273        f.render_widget(kind_content, inner[1]);
2274
2275        let value_display = if app.view_show_value {
2276            &item.value
2277        } else {
2278            "••••••••••••••••••••••••••••••••••••"
2279        };
2280
2281        let value_block = Block::default()
2282            .borders(Borders::ALL)
2283            .border_type(BorderType::Rounded)
2284            .border_style(Style::default().fg(c_border()))
2285            .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2286            .title(Span::styled(
2287                if app.view_show_value {
2288                    " Value (visible) "
2289                } else {
2290                    " Value (hidden) "
2291                },
2292                Style::default().fg(if app.view_show_value { c_warn() } else { c_text_dim() }),
2293            ));
2294        let value_content = Paragraph::new(value_display)
2295            .block(value_block)
2296            .style(Style::default().fg(if app.view_show_value { c_text() } else { c_text_dim() }))
2297            .wrap(Wrap { trim: true });
2298        f.render_widget(value_content, inner[2]);
2299
2300        let created_str = item
2301            .created_at
2302            .format(&time::format_description::well_known::Rfc3339)
2303            .unwrap_or_else(|_| "Unknown".to_string());
2304        let created_block = Block::default()
2305            .borders(Borders::ALL)
2306            .border_type(BorderType::Rounded)
2307            .border_style(Style::default().fg(c_border()))
2308            .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2309            .title(Span::styled(" Created ", Style::default().fg(c_text_dim())));
2310        let created_content = Paragraph::new(created_str)
2311            .block(created_block)
2312            .style(Style::default().fg(c_text()));
2313        f.render_widget(created_content, inner[3]);
2314
2315        let updated_str = item
2316            .updated_at
2317            .format(&time::format_description::well_known::Rfc3339)
2318            .unwrap_or_else(|_| "Unknown".to_string());
2319        let updated_block = Block::default()
2320            .borders(Borders::ALL)
2321            .border_type(BorderType::Rounded)
2322            .border_style(Style::default().fg(c_border()))
2323            .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2324            .title(Span::styled(" Updated ", Style::default().fg(c_text_dim())));
2325        let updated_content = Paragraph::new(updated_str)
2326            .block(updated_block)
2327            .style(Style::default().fg(c_text()));
2328        f.render_widget(updated_content, inner[4]);
2329
2330        let actions = Paragraph::new(Line::from(vec![
2331            Span::styled("[t/Enter]", Style::default().fg(c_warn()).add_modifier(Modifier::BOLD)),
2332            Span::styled(" Toggle visibility   ", Style::default().fg(c_text_dim())),
2333            Span::styled("[c]", Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
2334            Span::styled(" Copy   ", Style::default().fg(c_text_dim())),
2335            Span::styled("[Esc]", Style::default().fg(c_err()).add_modifier(Modifier::BOLD)),
2336            Span::styled(" Close", Style::default().fg(c_text_dim())),
2337        ]));
2338        f.render_widget(actions, inner[5]);
2339    }
2340}
2341
2342fn draw_edit_item(f: &mut Frame, app: &App) {
2343    if let Some(item) = &app.edit_item {
2344        let area = centered_rect(70, 50, f.area());
2345        f.render_widget(Clear, area);
2346
2347        let block = Block::default()
2348            .borders(Borders::ALL)
2349            .border_type(BorderType::Rounded)
2350            .border_style(Style::default().fg(c_border()))
2351            .style(Style::default().bg(c_bg_panel()).fg(c_text()))
2352            .title(Span::styled(
2353                " Edit Item ",
2354                Style::default().fg(c_accent2()).add_modifier(Modifier::BOLD),
2355            ));
2356        f.render_widget(block, area);
2357
2358        let inner = Layout::default()
2359            .direction(Direction::Vertical)
2360            .constraints([
2361                Constraint::Length(2),
2362                Constraint::Length(3),
2363                Constraint::Length(3),
2364                Constraint::Length(6),
2365                Constraint::Length(2),
2366            ])
2367            .split(pad(area, 2, 2));
2368
2369        let subtitle = Paragraph::new(Line::from(vec![
2370            Span::styled("Editing: ", Style::default().fg(c_text_dim())),
2371            Span::styled(&item.name, Style::default().fg(c_accent()).add_modifier(Modifier::BOLD)),
2372        ]));
2373        f.render_widget(subtitle, inner[0]);
2374
2375        let name_block = Block::default()
2376            .borders(Borders::ALL)
2377            .border_type(BorderType::Rounded)
2378            .border_style(Style::default().fg(c_border()))
2379            .style(Style::default().bg(Color::Rgb(40, 42, 50)))
2380            .title(Span::styled(" Name (read-only) ", Style::default().fg(c_text_dim())));
2381        let name_content = Paragraph::new(&*item.name)
2382            .block(name_block)
2383            .style(Style::default().fg(c_text_dim()));
2384        f.render_widget(name_content, inner[1]);
2385
2386        let (badge, color) = match item.kind.as_str() {
2387            "password" => ("Password", c_badge_pwd()),
2388            "env" => ("Environment Variable", c_badge_env()),
2389            _ => ("Note", c_badge_note()),
2390        };
2391        let kind_block = Block::default()
2392            .borders(Borders::ALL)
2393            .border_type(BorderType::Rounded)
2394            .border_style(Style::default().fg(c_border()))
2395            .style(Style::default().bg(Color::Rgb(40, 42, 50)))
2396            .title(Span::styled(" Kind (read-only) ", Style::default().fg(c_text_dim())));
2397        let kind_content = Paragraph::new(Line::from(vec![Span::styled(
2398            format!(" {badge} "),
2399            Style::default().bg(color).fg(Color::Black).add_modifier(Modifier::BOLD),
2400        )]))
2401        .block(kind_block);
2402        f.render_widget(kind_content, inner[2]);
2403
2404        let value_block = Block::default()
2405            .borders(Borders::ALL)
2406            .border_type(BorderType::Rounded)
2407            .border_style(Style::default().fg(c_accent()))
2408            .style(Style::default().bg(Color::Rgb(30, 32, 40)))
2409            .title(Span::styled(" New Value ", Style::default().fg(c_accent())));
2410        let value_content = Paragraph::new(&*app.edit_value)
2411            .block(value_block)
2412            .style(Style::default().fg(c_text()))
2413            .wrap(Wrap { trim: true });
2414        f.render_widget(value_content, inner[3]);
2415
2416        let actions = Paragraph::new(Line::from(vec![
2417            Span::styled("[Enter]", Style::default().fg(c_ok()).add_modifier(Modifier::BOLD)),
2418            Span::styled(" Save changes   ", Style::default().fg(c_text_dim())),
2419            Span::styled("[Esc]", Style::default().fg(c_err()).add_modifier(Modifier::BOLD)),
2420            Span::styled(" Cancel   ", Style::default().fg(c_text_dim())),
2421            Span::styled("Type to edit value", Style::default().fg(c_text_dim())),
2422        ]));
2423        f.render_widget(actions, inner[4]);
2424
2425        if let Some(err) = &app.error {
2426            let error_area = Rect {
2427                x: area.x + 2,
2428                y: area.y + area.height - 3,
2429                width: area.width - 4,
2430                height: 1,
2431            };
2432            let err_p = Paragraph::new(Span::styled(
2433                err.clone(),
2434                Style::default().fg(c_err()).add_modifier(Modifier::BOLD),
2435            ));
2436            f.render_widget(err_p, error_area);
2437        }
2438    }
2439}
2440
2441fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
2442    let v = Layout::default()
2443        .direction(Direction::Vertical)
2444        .constraints([
2445            Constraint::Percentage((100 - percent_y) / 2),
2446            Constraint::Percentage(percent_y),
2447            Constraint::Percentage((100 - percent_y) / 2),
2448        ])
2449        .split(r);
2450    let h = Layout::default()
2451        .direction(Direction::Horizontal)
2452        .constraints([
2453            Constraint::Percentage((100 - percent_x) / 2),
2454            Constraint::Percentage(percent_x),
2455            Constraint::Percentage((100 - percent_x) / 2),
2456        ])
2457        .split(v[1]);
2458    h[1]
2459}
2460
2461const fn pad(r: Rect, x: u16, y: u16) -> Rect {
2462    Rect {
2463        x: r.x.saturating_add(x),
2464        y: r.y.saturating_add(y),
2465        width: r.width.saturating_sub(x.saturating_mul(2)),
2466        height: r.height.saturating_sub(y.saturating_mul(2)),
2467    }
2468}
2469
2470fn field_box(content: &str, focused: bool) -> String {
2471    if focused {
2472        format!("[{content}]")
2473    } else {
2474        format!(" {content} ")
2475    }
2476}
2477
2478fn mask(s: &str) -> String {
2479    if s.is_empty() {
2480        String::new()
2481    } else {
2482        "•".repeat(s.chars().count())
2483    }
2484}