From 2ee751cae81e6063982c3554efe9c9771bb38c2c Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 22 Feb 2026 22:26:02 +0530 Subject: [PATCH 01/48] UPDATE: UI update --- src/ui/widgets/menu.rs | 437 ++++++++++++++++++++++++++++++++++------- 1 file changed, 367 insertions(+), 70 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index dcbf749..b5bdfe4 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -11,93 +11,277 @@ use ratatui::{ #[allow(clippy::too_many_lines)] pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { + let block = Block::default() + .borders(Borders::NONE) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, size); + let main_layout = Layout::default() .direction(Direction::Vertical) .margin(1) - .constraints([Constraint::Length(5), Constraint::Min(10)]) + .constraints([ + Constraint::Length(7), // banner + Constraint::Length(5), // status boxes + Constraint::Length(6), // status + Constraint::Min(8), // menu + tag distribution + Constraint::Length(3), // footer + ]) .split(size); - let header_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.yellow())) - .style(Style::default().bg(app.theme.bg0())); + let banner_lines = vec![ + Line::from(vec![Span::styled( + "██████╗ █████╗ ███████╗███████╗██╗ ██████╗ ██████╗██╗ ██╗", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██╔═══██╗██╔════╝██║ ██╔╝", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██████╔╝███████║███████╗███████╗██║ ██║ ██║██║ █████╔╝ ", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██╔═══╝ ██╔══██║╚════██║╚════██║██║ ██║ ██║██║ ██╔═██╗ ", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██║ ██║ ██║███████║███████║███████╗╚██████╔╝╚██████╗██║ ██╗", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + " Secure Password Manager v2.3.5", + Style::default().fg(app.theme.gray()), + )]), + ]; + let banner = Paragraph::new(banner_lines).alignment(Alignment::Center); + f.render_widget(banner, main_layout[0]); - let vault_info = if let Some(ref vault) = app.vault { - let tag_count = app.all_tags.len(); - let disp_count = app.entry_disp.len(); - let total_count = vault.e.len(); - let filter_indicator = if disp_count == total_count { - String::new() - } else { - format!(" (Filtered: {disp_count})") - }; - vec![ - Line::from(vec![Span::styled( - "█▓▒░ PASSLOCK ░▒▓█", - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(""), + let status_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(main_layout[1]); + + if let Some(ref vault) = app.vault { + let vault_lines = vec![ Line::from(vec![ - Span::styled("Vault: ", Style::default().fg(app.theme.gray())), + Span::styled("Status: ", Style::default().fg(app.theme.gray())), Span::styled( - "UNLOCKED ", + "UNLOCKED ✓", Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD), ), - Span::styled("│ ", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled("Location: ", Style::default().fg(app.theme.gray())), + Span::styled("~/.passlock.vault", Style::default().fg(app.theme.blue())), + ]), + Line::from(vec![ + Span::styled("Size: ", Style::default().fg(app.theme.gray())), + Span::styled("24.3 KB", Style::default().fg(app.theme.yellow())), + Span::styled(" │ ", Style::default().fg(app.theme.gray())), + Span::styled("Modified: ", Style::default().fg(app.theme.gray())), + Span::styled("Today", Style::default().fg(app.theme.green())), + ]), + ]; + let vault_status = Paragraph::new(vault_lines).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.green())) + .title(" VAULT STATUS "), + ); + f.render_widget(vault_status, status_layout[0]); + + let security_lines = vec![ + Line::from(vec![ + Span::styled("Encryption: ", Style::default().fg(app.theme.gray())), + Span::styled( + "AES-256-GCM", + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + ), + ]), + Line::from(vec![ + Span::styled("KDF: ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{total_count} passwords{filter_indicator} "), - Style::default().fg(app.theme.blue()), + "Argon2id (64MB / 3 iter)", + Style::default().fg(app.theme.aqua()), ), - Span::styled("│ ", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled("Clipboard: ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{tag_count} tags"), - Style::default().fg(app.theme.purple()), + if app.clipboard_countdown.is_some() { + "Active" + } else { + "Empty" + }, + if app.clipboard_countdown.is_some() { + Style::default().fg(app.theme.orange()) + } else { + Style::default().fg(app.theme.gray()) + }, + ), + Span::styled(" │ Timeout: ", Style::default().fg(app.theme.gray())), + Span::styled( + if app.clipboard_timeout == 0 { + "Never".to_string() + } else { + format!("{}s", app.clipboard_timeout) + }, + Style::default().fg(app.theme.yellow()), ), ]), - ] - } else { - vec![ - Line::from(Span::styled( - "PASSLOCK", - Style::default().fg(app.theme.red()), - )), - Line::from(""), - Line::from("No vault loaded"), - ] - }; - - let header = Paragraph::new(vault_info) - .block(header_block) - .alignment(Alignment::Center); - f.render_widget(header, main_layout[0]); + ]; + let security_status = Paragraph::new(security_lines).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.purple())) + .title(" SECURITY METRICS "), + ); + f.render_widget(security_status, status_layout[1]); + + let total = vault.e.len(); + let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); + let with_url = vault.e.iter().filter(|e| e.url.is_some()).count(); + let tags = app.all_tags.len(); + + let stats_lines = vec![ + Line::from(vec![ + Span::styled("Total: ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{total} "), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + ), + Span::styled("passwords", Style::default().fg(app.theme.fg())), + Span::styled(" │ ", Style::default().fg(app.theme.gray())), + Span::styled("Tags: ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{tags}"), + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + ), + ]), + Line::from(vec![ + Span::styled("2FA Enabled: ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{with_2fa} "), + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!( + "({}%) ", + if total > 0 { + (with_2fa * 100) / total + } else { + 0 + } + ), + Style::default().fg(app.theme.gray()), + ), + Span::raw(create_bar(with_2fa, total, 10)), + ]), + Line::from(vec![ + Span::styled("With URL: ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{with_url} "), + Style::default() + .fg(app.theme.blue()) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!( + "({}%) ", + if total > 0 { + (with_url * 100) / total + } else { + 0 + } + ), + Style::default().fg(app.theme.gray()), + ), + Span::raw(create_bar(with_url, total, 10)), + ]), + Line::from(vec![ + Span::styled("Theme: ", Style::default().fg(app.theme.gray())), + Span::styled(app.theme.name(), Style::default().fg(app.theme.aqua())), + Span::styled(" │ Press ", Style::default().fg(app.theme.gray())), + Span::styled( + "T/8", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" to change", Style::default().fg(app.theme.gray())), + ]), + ]; + let stats = Paragraph::new(stats_lines).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.yellow())) + .title(" STATISTICS "), + ); + f.render_widget(stats, main_layout[2]); + } let content_layout = Layout::default() .direction(Direction::Horizontal) - .margin(0) + .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) + .split(main_layout[3]); + + let menu_layout = Layout::default() + .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(main_layout[1]); + .split(content_layout[0]); let left_block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(app.theme.green())) - .title("═══ PASSWORDS ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); + .title(" PASSWORD MANAGEMENT "); let left_items = [ - ("1", "View All", "Browse vault"), - ("2", "Add New", "Create entry"), - ("3", "Search", "Find passwords"), + ( + "1", + "View All", + "Browse vault", + format!("{} entries", app.vault.as_ref().map_or(0, |v| v.e.len())), + ), + ("2", "Add New", "Create entry", "Quick: Ctrl+N".to_string()), + ( + "3", + "Search", + "Find passwords", + "Type to filter".to_string(), + ), ]; let left_list: Vec = left_items .iter() .enumerate() - .map(|(i, (num, title, desc))| { + .map(|(i, (num, title, desc, _hint))| { let is_selected = app.selected_section == 0 && i == app.selected_menu; let style = if is_selected { Style::default() @@ -124,26 +308,34 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .collect(); let left = List::new(left_list).block(left_block); - f.render_widget(left, content_layout[0]); + f.render_widget(left, menu_layout[0]); let right_block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(app.theme.purple())) - .title("═══ TOOLS ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); + .title(" TOOLS "); let right_items = [ - ("4", "Filter Tags", "Sort by tags"), - ("5", "Generate", "Random password"), - ("6", "Delete", "Remove entry"), - ("7", "Exit", "Lock & quit"), + ( + "4", + "Filter Tags", + "Sort by tags", + format!("{} tags", app.all_tags.len()), + ), + ( + "5", + "Generate", + "Random password", + "Default: 16 chars".to_string(), + ), + ("6", "Delete", "Remove entry", "⚠ Permanent".to_string()), + ("7", "Exit", "Lock & quit", "Auto-save".to_string()), ]; let right_list: Vec = right_items .iter() .enumerate() - .map(|(i, (num, title, desc))| { + .map(|(i, (num, title, desc, _hint))| { let is_selected = app.selected_section == 1 && i == app.selected_menu - 3; let style = if is_selected { Style::default() @@ -170,23 +362,128 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .collect(); let right = List::new(right_list).block(right_block); - f.render_widget(right, content_layout[1]); + f.render_widget(right, menu_layout[1]); + + if let Some(ref _vault) = app.vault { + let mut tag_lines = vec![]; + let max_count = app + .all_tags + .iter() + .map(|(_, c)| c) + .max() + .copied() + .unwrap_or(1); + + for (tag, count) in app.all_tags.iter().take(5) { + tag_lines.push(Line::from(vec![ + Span::styled( + format!("{tag:12} "), + Style::default().fg(app.theme.purple()), + ), + Span::raw(create_bar(*count, max_count, 8)), + Span::styled( + format!(" {count}"), + Style::default().fg(app.theme.yellow()), + ), + ])); + } + + if app.all_tags.len() > 5 { + tag_lines.push(Line::from(vec![Span::styled( + format!("... {} more tags", app.all_tags.len() - 5), + Style::default().fg(app.theme.gray()), + )])); + } + + let tag_dist = Paragraph::new(tag_lines).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.orange())) + .title(" TAG DISTRIBUTION "), + ); + f.render_widget(tag_dist, content_layout[1]); + } + + let footer_text = vec![Line::from(vec![ + Span::styled( + "↑↓/←→", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(": Navigate ", Style::default().fg(app.theme.gray())), + Span::styled( + "Enter", + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(": Select ", Style::default().fg(app.theme.gray())), + Span::styled( + "Esc", + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(": Options ", Style::default().fg(app.theme.gray())), + Span::styled( + "1-7", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(": Quick select", Style::default().fg(app.theme.gray())), + ])]; + + let footer = Paragraph::new(footer_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.gray())), + ); + f.render_widget(footer, main_layout[4]); if !app.msg.is_empty() { - let msg_area = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)]) - .split(main_layout[1])[1]; + let msg_area = Rect { + x: size.width / 4, + y: size.height - 5, + width: size.width / 2, + height: 3, + }; + let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), MessageType::Error => Style::default().fg(app.theme.red()), MessageType::Info => Style::default().fg(app.theme.blue()), MessageType::None => Style::default().fg(app.theme.fg()), }; + let msg = Paragraph::new(app.msg.as_str()) .style(msg_style) .alignment(Alignment::Center) - .wrap(Wrap { trim: true }); + .wrap(Wrap { trim: true }) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(msg_style), + ); f.render_widget(msg, msg_area); } } + +fn create_bar(value: usize, max: usize, width: usize) -> String { + if max == 0 { + return "░".repeat(width); + } + let filled = (value * width) / max; + let mut bar = String::new(); + for i in 0..width { + if i < filled { + bar.push('▓'); + } else { + bar.push('░'); + } + } + bar +} From ca85436308b00d7594f3eb0bc1be7412ff8b2e4e Mon Sep 17 00:00:00 2001 From: hachimamma Date: Mon, 23 Feb 2026 18:45:21 +0530 Subject: [PATCH 02/48] FIX: Fixed UI (it is actually bearable now) --- src/ui/widgets/menu.rs | 487 ++++++++++------------------------------- 1 file changed, 117 insertions(+), 370 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index b5bdfe4..538f290 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -1,11 +1,12 @@ use super::super::app::App; use super::super::colors::ThemeColors; use super::super::screens::MessageType; +use crate::vault_ffi; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, Frame, }; @@ -18,17 +19,15 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let main_layout = Layout::default() .direction(Direction::Vertical) - .margin(1) + .margin(2) .constraints([ - Constraint::Length(7), // banner - Constraint::Length(5), // status boxes - Constraint::Length(6), // status - Constraint::Min(8), // menu + tag distribution - Constraint::Length(3), // footer + Constraint::Length(5), // title + Constraint::Length(12), // space + Constraint::Min(20), // menu ]) .split(size); - let banner_lines = vec![ + let title_lines = vec![ Line::from(vec![Span::styled( "██████╗ █████╗ ███████╗███████╗██╗ ██████╗ ██████╗██╗ ██╗", Style::default() @@ -59,396 +58,160 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .fg(app.theme.red()) .add_modifier(Modifier::BOLD), )]), - Line::from(vec![Span::styled( - "╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(vec![Span::styled( - " Secure Password Manager v2.3.5", - Style::default().fg(app.theme.gray()), - )]), ]; - let banner = Paragraph::new(banner_lines).alignment(Alignment::Center); - f.render_widget(banner, main_layout[0]); - let status_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(main_layout[1]); + let title = Paragraph::new(title_lines).alignment(Alignment::Center); + f.render_widget(title, main_layout[0]); + + let all_items = [ + ("1", "View Passwords", "Browse all entries"), + ("2", "Add Password", "Create new entry"), + ("3", "Search", "Find passwords"), + ("4", "Filter by Tags", "Sort entries"), + ("5", "Generate Password", "Random secure"), + ("6", "Delete Entry", "Remove password"), + ("7", "Exit", "Lock vault"), + ]; - if let Some(ref vault) = app.vault { - let vault_lines = vec![ - Line::from(vec![ - Span::styled("Status: ", Style::default().fg(app.theme.gray())), - Span::styled( - "UNLOCKED ✓", - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("Location: ", Style::default().fg(app.theme.gray())), - Span::styled("~/.passlock.vault", Style::default().fg(app.theme.blue())), - ]), - Line::from(vec![ - Span::styled("Size: ", Style::default().fg(app.theme.gray())), - Span::styled("24.3 KB", Style::default().fg(app.theme.yellow())), - Span::styled(" │ ", Style::default().fg(app.theme.gray())), - Span::styled("Modified: ", Style::default().fg(app.theme.gray())), - Span::styled("Today", Style::default().fg(app.theme.green())), - ]), - ]; - let vault_status = Paragraph::new(vault_lines).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.green())) - .title(" VAULT STATUS "), - ); - f.render_widget(vault_status, status_layout[0]); + let menu_list: Vec = all_items + .iter() + .enumerate() + .map(|(i, (num, title, desc))| { + let is_selected = i == app.selected_menu; - let security_lines = vec![ - Line::from(vec![ - Span::styled("Encryption: ", Style::default().fg(app.theme.gray())), - Span::styled( - "AES-256-GCM", - Style::default() - .fg(app.theme.purple()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("KDF: ", Style::default().fg(app.theme.gray())), - Span::styled( - "Argon2id (64MB / 3 iter)", - Style::default().fg(app.theme.aqua()), - ), - ]), - Line::from(vec![ - Span::styled("Clipboard: ", Style::default().fg(app.theme.gray())), - Span::styled( - if app.clipboard_countdown.is_some() { - "Active" - } else { - "Empty" - }, - if app.clipboard_countdown.is_some() { - Style::default().fg(app.theme.orange()) - } else { - Style::default().fg(app.theme.gray()) - }, - ), - Span::styled(" │ Timeout: ", Style::default().fg(app.theme.gray())), - Span::styled( - if app.clipboard_timeout == 0 { - "Never".to_string() - } else { - format!("{}s", app.clipboard_timeout) - }, - Style::default().fg(app.theme.yellow()), - ), - ]), - ]; - let security_status = Paragraph::new(security_lines).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.purple())) - .title(" SECURITY METRICS "), - ); - f.render_widget(security_status, status_layout[1]); + let title_style = if is_selected { + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + .fg(app.theme.fg()) + .add_modifier(Modifier::BOLD) + }; + + let prefix = if is_selected { "▶ " } else { " " }; + + ListItem::new(vec![ + Line::from(vec![ + Span::styled(prefix, Style::default().fg(app.theme.yellow())), + Span::styled( + format!("[{num}] "), + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(*title, title_style), + ]), + Line::from(vec![ + Span::raw(" "), + Span::styled(*desc, Style::default().fg(app.theme.gray())), + ]), + Line::from(""), + ]) + }) + .collect(); + let menu_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + .title(Span::styled( + " [ Menu ] ", + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + )); + + let menu = List::new(menu_list).block(menu_block); + f.render_widget(menu, main_layout[2]); + + if let Some(ref vault) = app.vault { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); - let with_url = vault.e.iter().filter(|e| e.url.is_some()).count(); let tags = app.all_tags.len(); - let stats_lines = vec![ + let cipher = vault_ffi::get_cipher(); + + let info_area = Rect { + x: 4, + y: main_layout[0].y + main_layout[0].height + 1, + width: 52, + height: 10, + }; + + let info_lines = vec![ + Line::from(vec![Span::styled( + "System Info", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(""), Line::from(vec![ - Span::styled("Total: ", Style::default().fg(app.theme.gray())), + Span::styled("Version ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{total} "), + "v2.3.5", Style::default() .fg(app.theme.yellow()) .add_modifier(Modifier::BOLD), ), - Span::styled("passwords", Style::default().fg(app.theme.fg())), - Span::styled(" │ ", Style::default().fg(app.theme.gray())), - Span::styled("Tags: ", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled("Encryption ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{tags}"), + cipher, Style::default() .fg(app.theme.purple()) .add_modifier(Modifier::BOLD), ), ]), Line::from(vec![ - Span::styled("2FA Enabled: ", Style::default().fg(app.theme.gray())), + Span::styled("Passwords ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{with_2fa} "), + format!("{total}"), Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD), ), - Span::styled( - format!( - "({}%) ", - if total > 0 { - (with_2fa * 100) / total - } else { - 0 - } - ), - Style::default().fg(app.theme.gray()), - ), - Span::raw(create_bar(with_2fa, total, 10)), ]), Line::from(vec![ - Span::styled("With URL: ", Style::default().fg(app.theme.gray())), + Span::styled("2FA Enabled ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{with_url} "), + format!("{with_2fa}"), Style::default() - .fg(app.theme.blue()) + .fg(app.theme.orange()) .add_modifier(Modifier::BOLD), ), - Span::styled( - format!( - "({}%) ", - if total > 0 { - (with_url * 100) / total - } else { - 0 - } - ), - Style::default().fg(app.theme.gray()), - ), - Span::raw(create_bar(with_url, total, 10)), ]), Line::from(vec![ - Span::styled("Theme: ", Style::default().fg(app.theme.gray())), - Span::styled(app.theme.name(), Style::default().fg(app.theme.aqua())), - Span::styled(" │ Press ", Style::default().fg(app.theme.gray())), + Span::styled("Tags ", Style::default().fg(app.theme.gray())), Span::styled( - "T/8", + format!("{tags}"), Style::default() - .fg(app.theme.orange()) + .fg(app.theme.blue()) .add_modifier(Modifier::BOLD), ), - Span::styled(" to change", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled("Theme ", Style::default().fg(app.theme.gray())), + Span::styled(app.theme.name(), Style::default().fg(app.theme.aqua())), ]), ]; - let stats = Paragraph::new(stats_lines).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.yellow())) - .title(" STATISTICS "), - ); - f.render_widget(stats, main_layout[2]); - } - - let content_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) - .split(main_layout[3]); - - let menu_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(content_layout[0]); - - let left_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.green())) - .title(" PASSWORD MANAGEMENT "); - let left_items = [ - ( - "1", - "View All", - "Browse vault", - format!("{} entries", app.vault.as_ref().map_or(0, |v| v.e.len())), - ), - ("2", "Add New", "Create entry", "Quick: Ctrl+N".to_string()), - ( - "3", - "Search", - "Find passwords", - "Type to filter".to_string(), - ), - ]; - - let left_list: Vec = left_items - .iter() - .enumerate() - .map(|(i, (num, title, desc, _hint))| { - let is_selected = app.selected_section == 0 && i == app.selected_menu; - let style = if is_selected { - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(app.theme.fg()) - }; - let prefix = if is_selected { "▶ " } else { " " }; - let lines = vec![ - Line::from(vec![ - Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled(format!("[{num}] "), Style::default().fg(app.theme.orange())), - Span::styled(*title, style), - ]), - Line::from(vec![ - Span::raw(" "), - Span::styled(*desc, Style::default().fg(app.theme.gray())), - ]), - Line::from(""), - ]; - ListItem::new(lines) - }) - .collect(); - - let left = List::new(left_list).block(left_block); - f.render_widget(left, menu_layout[0]); - - let right_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.purple())) - .title(" TOOLS "); - - let right_items = [ - ( - "4", - "Filter Tags", - "Sort by tags", - format!("{} tags", app.all_tags.len()), - ), - ( - "5", - "Generate", - "Random password", - "Default: 16 chars".to_string(), - ), - ("6", "Delete", "Remove entry", "⚠ Permanent".to_string()), - ("7", "Exit", "Lock & quit", "Auto-save".to_string()), - ]; - - let right_list: Vec = right_items - .iter() - .enumerate() - .map(|(i, (num, title, desc, _hint))| { - let is_selected = app.selected_section == 1 && i == app.selected_menu - 3; - let style = if is_selected { - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(app.theme.fg()) - }; - let prefix = if is_selected { "▶ " } else { " " }; - let lines = vec![ - Line::from(vec![ - Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled(format!("[{num}] "), Style::default().fg(app.theme.orange())), - Span::styled(*title, style), - ]), - Line::from(vec![ - Span::raw(" "), - Span::styled(*desc, Style::default().fg(app.theme.gray())), - ]), - Line::from(""), - ]; - ListItem::new(lines) - }) - .collect(); - - let right = List::new(right_list).block(right_block); - f.render_widget(right, menu_layout[1]); - - if let Some(ref _vault) = app.vault { - let mut tag_lines = vec![]; - let max_count = app - .all_tags - .iter() - .map(|(_, c)| c) - .max() - .copied() - .unwrap_or(1); - - for (tag, count) in app.all_tags.iter().take(5) { - tag_lines.push(Line::from(vec![ - Span::styled( - format!("{tag:12} "), - Style::default().fg(app.theme.purple()), - ), - Span::raw(create_bar(*count, max_count, 8)), - Span::styled( - format!(" {count}"), - Style::default().fg(app.theme.yellow()), - ), - ])); - } - - if app.all_tags.len() > 5 { - tag_lines.push(Line::from(vec![Span::styled( - format!("... {} more tags", app.all_tags.len() - 5), - Style::default().fg(app.theme.gray()), - )])); - } - - let tag_dist = Paragraph::new(tag_lines).block( + let info_box = Paragraph::new(info_lines).block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.orange())) - .title(" TAG DISTRIBUTION "), + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())), ); - f.render_widget(tag_dist, content_layout[1]); + f.render_widget(info_box, info_area); } - let footer_text = vec![Line::from(vec![ - Span::styled( - "↑↓/←→", - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(": Navigate ", Style::default().fg(app.theme.gray())), - Span::styled( - "Enter", - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(": Select ", Style::default().fg(app.theme.gray())), - Span::styled( - "Esc", - Style::default() - .fg(app.theme.purple()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(": Options ", Style::default().fg(app.theme.gray())), - Span::styled( - "1-7", - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(": Quick select", Style::default().fg(app.theme.gray())), - ])]; - - let footer = Paragraph::new(footer_text) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.gray())), - ); - f.render_widget(footer, main_layout[4]); - if !app.msg.is_empty() { - let msg_area = Rect { - x: size.width / 4, - y: size.height - 5, - width: size.width / 2, + let status_area = Rect { + x: size.width.saturating_sub(22), + y: main_layout[0].y + main_layout[0].height + 1, + width: 20, height: 3, }; @@ -459,31 +222,15 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { MessageType::None => Style::default().fg(app.theme.fg()), }; - let msg = Paragraph::new(app.msg.as_str()) - .style(msg_style) + let status_msg = Paragraph::new(app.msg.as_str()) + .style(msg_style.add_modifier(Modifier::BOLD)) .alignment(Alignment::Center) - .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(msg_style), ); - f.render_widget(msg, msg_area); - } -} - -fn create_bar(value: usize, max: usize, width: usize) -> String { - if max == 0 { - return "░".repeat(width); - } - let filled = (value * width) / max; - let mut bar = String::new(); - for i in 0..width { - if i < filled { - bar.push('▓'); - } else { - bar.push('░'); - } + f.render_widget(status_msg, status_area); } - bar } From fa92de7752cc279f710d15deb09063f3b3aef0f8 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 09:43:50 +0530 Subject: [PATCH 03/48] FIX: Fixed UI - Made splash message adapatable - Removed PASSLOCK ASCII - Uplifted System Info bar above Menu --- src/ui/widgets/menu.rs | 241 +++++++++++++++-------------------------- 1 file changed, 90 insertions(+), 151 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 538f290..a05d4d1 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -6,7 +6,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, + widgets::{Block, Borders, BorderType, List, ListItem, Paragraph}, Frame, }; @@ -17,180 +17,64 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .style(Style::default().bg(app.theme.bg0())); f.render_widget(block, size); + // Main layout let main_layout = Layout::default() .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(5), // title - Constraint::Length(12), // space - Constraint::Min(20), // menu + Constraint::Length(3), // Title + Constraint::Length(11), // System info area + Constraint::Min(20), // Menu ]) .split(size); - let title_lines = vec![ - Line::from(vec![Span::styled( - "██████╗ █████╗ ███████╗███████╗██╗ ██████╗ ██████╗██╗ ██╗", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(vec![Span::styled( - "██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██╔═══██╗██╔════╝██║ ██╔╝", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(vec![Span::styled( - "██████╔╝███████║███████╗███████╗██║ ██║ ██║██║ █████╔╝ ", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(vec![Span::styled( - "██╔═══╝ ██╔══██║╚════██║╚════██║██║ ██║ ██║██║ ██╔═██╗ ", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(vec![Span::styled( - "██║ ██║ ██║███████║███████║███████╗╚██████╔╝╚██████╗██║ ██╗", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - ]; - - let title = Paragraph::new(title_lines).alignment(Alignment::Center); + // ═══ TITLE (centered) ═══ + let title = Paragraph::new(Line::from(vec![ + Span::styled("PASSLOCK", Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD)), + ])) + .alignment(Alignment::Center); f.render_widget(title, main_layout[0]); - let all_items = [ - ("1", "View Passwords", "Browse all entries"), - ("2", "Add Password", "Create new entry"), - ("3", "Search", "Find passwords"), - ("4", "Filter by Tags", "Sort entries"), - ("5", "Generate Password", "Random secure"), - ("6", "Delete Entry", "Remove password"), - ("7", "Exit", "Lock vault"), - ]; - - let menu_list: Vec = all_items - .iter() - .enumerate() - .map(|(i, (num, title, desc))| { - let is_selected = i == app.selected_menu; - - let title_style = if is_selected { - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD) - } else { - Style::default() - .fg(app.theme.fg()) - .add_modifier(Modifier::BOLD) - }; - - let prefix = if is_selected { "▶ " } else { " " }; - - ListItem::new(vec![ - Line::from(vec![ - Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled( - format!("[{num}] "), - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(*title, title_style), - ]), - Line::from(vec![ - Span::raw(" "), - Span::styled(*desc, Style::default().fg(app.theme.gray())), - ]), - Line::from(""), - ]) - }) - .collect(); - - let menu_block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.green())) - .title(Span::styled( - " [ Menu ] ", - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - )); - - let menu = List::new(menu_list).block(menu_block); - f.render_widget(menu, main_layout[2]); - + // ═══ SYSTEM INFO (left side of info area) ═══ if let Some(ref vault) = app.vault { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let tags = app.all_tags.len(); - let cipher = vault_ffi::get_cipher(); - + let info_area = Rect { - x: 4, - y: main_layout[0].y + main_layout[0].height + 1, + x: main_layout[1].x, + y: main_layout[1].y, width: 52, height: 10, }; let info_lines = vec![ - Line::from(vec![Span::styled( - "System Info", - Style::default() - .fg(app.theme.aqua()) - .add_modifier(Modifier::BOLD), - )]), + Line::from(vec![ + Span::styled("System Info", Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD)), + ]), Line::from(""), Line::from(vec![ Span::styled("Version ", Style::default().fg(app.theme.gray())), - Span::styled( - "v2.3.5", - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - ), + Span::styled("v2.3.5", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD)), ]), Line::from(vec![ Span::styled("Encryption ", Style::default().fg(app.theme.gray())), - Span::styled( - cipher, - Style::default() - .fg(app.theme.purple()) - .add_modifier(Modifier::BOLD), - ), + Span::styled(cipher, Style::default().fg(app.theme.purple()).add_modifier(Modifier::BOLD)), ]), Line::from(vec![ Span::styled("Passwords ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{total}"), - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - ), + Span::styled(format!("{}", total), Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD)), ]), Line::from(vec![ Span::styled("2FA Enabled ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{with_2fa}"), - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), + Span::styled(format!("{}", with_2fa), Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), ]), Line::from(vec![ Span::styled("Tags ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{tags}"), - Style::default() - .fg(app.theme.blue()) - .add_modifier(Modifier::BOLD), - ), + Span::styled(format!("{}", tags), Style::default().fg(app.theme.blue()).add_modifier(Modifier::BOLD)), ]), Line::from(vec![ Span::styled("Theme ", Style::default().fg(app.theme.gray())), @@ -198,20 +82,23 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { ]), ]; - let info_box = Paragraph::new(info_lines).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())), - ); + let info_box = Paragraph::new(info_lines) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())) + ); f.render_widget(info_box, info_area); } + // ═══ STATUS MESSAGE (right side of info area) ═══ if !app.msg.is_empty() { + let msg_width = app.msg.len().max(30).min(60) as u16 + 4; let status_area = Rect { - x: size.width.saturating_sub(22), - y: main_layout[0].y + main_layout[0].height + 1, - width: 20, + x: main_layout[1].x + main_layout[1].width - msg_width, + y: main_layout[1].y, + width: msg_width, height: 3, }; @@ -229,8 +116,60 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(msg_style), + .border_style(msg_style) ); f.render_widget(status_msg, status_area); } -} + + // ═══ MENU ═══ + let all_items = [ + ("1", "View Passwords", "Browse all entries"), + ("2", "Add Password", "Create new entry"), + ("3", "Search", "Find passwords"), + ("4", "Filter by Tags", "Sort entries"), + ("5", "Generate Password", "Random secure"), + ("6", "Delete Entry", "Remove password"), + ("7", "Exit", "Lock vault"), + ]; + + let menu_list: Vec = all_items + .iter() + .enumerate() + .map(|(i, (num, title, desc))| { + let is_selected = i == app.selected_menu; + + let title_style = if is_selected { + Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(app.theme.fg()).add_modifier(Modifier::BOLD) + }; + + let prefix = if is_selected { "▶ " } else { " " }; + + ListItem::new(vec![ + Line::from(vec![ + Span::styled(prefix, Style::default().fg(app.theme.yellow())), + Span::styled(format!("[{}] ", num), Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled(*title, title_style), + ]), + Line::from(vec![ + Span::raw(" "), + Span::styled(*desc, Style::default().fg(app.theme.gray())), + ]), + Line::from(""), + ]) + }) + .collect(); + + let menu_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + .title(Span::styled( + " [ Menu ] ", + Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD) + )); + + let menu = List::new(menu_list).block(menu_block); + f.render_widget(menu, main_layout[2]); +} \ No newline at end of file From fe9a6057e5b8de759db1c7f878e44d0101b94fc7 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 09:52:52 +0530 Subject: [PATCH 04/48] FIX: Fixed formatting for title PASSLOCK in main menu --- src/ui/widgets/menu.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index a05d4d1..273f70d 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -17,27 +17,29 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .style(Style::default().bg(app.theme.bg0())); f.render_widget(block, size); - // Main layout let main_layout = Layout::default() .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(3), // Title - Constraint::Length(11), // System info area - Constraint::Min(20), // Menu + Constraint::Length(3), // title + Constraint::Length(11), // system info + Constraint::Min(20), // menu ]) .split(size); - // ═══ TITLE (centered) ═══ - let title = Paragraph::new(Line::from(vec![ - Span::styled("PASSLOCK", Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD)), - ])) - .alignment(Alignment::Center); + let title_lines = vec![ + Line::from(""), + Line::from(vec![ + Span::styled("PASSLOCK", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD)), + ]), + Line::from(""), + ]; + let title = Paragraph::new(title_lines).alignment(Alignment::Center); f.render_widget(title, main_layout[0]); - // ═══ SYSTEM INFO (left side of info area) ═══ if let Some(ref vault) = app.vault { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); @@ -92,7 +94,6 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { f.render_widget(info_box, info_area); } - // ═══ STATUS MESSAGE (right side of info area) ═══ if !app.msg.is_empty() { let msg_width = app.msg.len().max(30).min(60) as u16 + 4; let status_area = Rect { @@ -121,7 +122,6 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { f.render_widget(status_msg, status_area); } - // ═══ MENU ═══ let all_items = [ ("1", "View Passwords", "Browse all entries"), ("2", "Add Password", "Create new entry"), From 1cbd4b59a940a6069b334f39327ccea8d864be36 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 09:57:34 +0530 Subject: [PATCH 05/48] FIX: Fixed formatting and clippy --- src/ui/widgets/menu.rs | 111 ++++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 36 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 273f70d..f6ad3f4 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -6,7 +6,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, BorderType, List, ListItem, Paragraph}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, Frame, }; @@ -21,20 +21,20 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(3), // title - Constraint::Length(11), // system info - Constraint::Min(20), // menu + Constraint::Length(3), // title + Constraint::Length(11), // system info + Constraint::Min(20), // menu ]) .split(size); let title_lines = vec![ Line::from(""), - Line::from(vec![ - Span::styled("PASSLOCK", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD)), - ]), + Line::from(vec![Span::styled( + "PASSLOCK", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), ]; let title = Paragraph::new(title_lines).alignment(Alignment::Center); @@ -45,7 +45,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let tags = app.all_tags.len(); let cipher = vault_ffi::get_cipher(); - + let info_area = Rect { x: main_layout[1].x, y: main_layout[1].y, @@ -54,29 +54,57 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { }; let info_lines = vec![ - Line::from(vec![ - Span::styled("System Info", Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD)), - ]), + Line::from(vec![Span::styled( + "System Info", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), Line::from(vec![ Span::styled("Version ", Style::default().fg(app.theme.gray())), - Span::styled("v2.3.5", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD)), + Span::styled( + "v2.3.5", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + ), ]), Line::from(vec![ Span::styled("Encryption ", Style::default().fg(app.theme.gray())), - Span::styled(cipher, Style::default().fg(app.theme.purple()).add_modifier(Modifier::BOLD)), + Span::styled( + cipher, + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + ), ]), Line::from(vec![ Span::styled("Passwords ", Style::default().fg(app.theme.gray())), - Span::styled(format!("{}", total), Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD)), + Span::styled( + format!("{total}"), + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + ), ]), Line::from(vec![ Span::styled("2FA Enabled ", Style::default().fg(app.theme.gray())), - Span::styled(format!("{}", with_2fa), Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + format!("{with_2fa}"), + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), ]), Line::from(vec![ Span::styled("Tags ", Style::default().fg(app.theme.gray())), - Span::styled(format!("{}", tags), Style::default().fg(app.theme.blue()).add_modifier(Modifier::BOLD)), + Span::styled( + format!("{tags}"), + Style::default() + .fg(app.theme.blue()) + .add_modifier(Modifier::BOLD), + ), ]), Line::from(vec![ Span::styled("Theme ", Style::default().fg(app.theme.gray())), @@ -84,18 +112,18 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { ]), ]; - let info_box = Paragraph::new(info_lines) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())) - ); + let info_box = Paragraph::new(info_lines).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())), + ); f.render_widget(info_box, info_area); } if !app.msg.is_empty() { - let msg_width = app.msg.len().max(30).min(60) as u16 + 4; + #[allow(clippy::cast_possible_truncation)] + let msg_width = (app.msg.len().clamp(30, 60) as u16) + 4; let status_area = Rect { x: main_layout[1].x + main_layout[1].width - msg_width, y: main_layout[1].y, @@ -117,7 +145,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(msg_style) + .border_style(msg_style), ); f.render_widget(status_msg, status_area); } @@ -137,19 +165,28 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .enumerate() .map(|(i, (num, title, desc))| { let is_selected = i == app.selected_menu; - + let title_style = if is_selected { - Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD) } else { - Style::default().fg(app.theme.fg()).add_modifier(Modifier::BOLD) + Style::default() + .fg(app.theme.fg()) + .add_modifier(Modifier::BOLD) }; - + let prefix = if is_selected { "▶ " } else { " " }; - + ListItem::new(vec![ Line::from(vec![ Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled(format!("[{}] ", num), Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + format!("[{num}] "), + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(*title, title_style), ]), Line::from(vec![ @@ -167,9 +204,11 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .border_style(Style::default().fg(app.theme.green())) .title(Span::styled( " [ Menu ] ", - Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD) + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), )); let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, main_layout[2]); -} \ No newline at end of file +} From 24fb6c76b5f0f6ffea7c14183c09c87acb612cbe Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 10:14:54 +0530 Subject: [PATCH 06/48] UPDATE: Second Iteration of UI (Much better) --- src/ui/widgets/menu.rs | 215 +++++++++++++++++++++++++++++++---------- 1 file changed, 163 insertions(+), 52 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index f6ad3f4..cf4d295 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -18,27 +18,31 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { f.render_widget(block, size); let main_layout = Layout::default() - .direction(Direction::Vertical) + .direction(Direction::Horizontal) .margin(2) + .constraints([ + Constraint::Percentage(65), // left + Constraint::Percentage(35), // right + ]) + .split(size); + + let left_layout = Layout::default() + .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // title Constraint::Length(11), // system info Constraint::Min(20), // menu ]) - .split(size); + .split(main_layout[0]); - let title_lines = vec![ - Line::from(""), - Line::from(vec![Span::styled( - "PASSLOCK", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(""), - ]; - let title = Paragraph::new(title_lines).alignment(Alignment::Center); - f.render_widget(title, main_layout[0]); + let title = Paragraph::new(Line::from(vec![Span::styled( + "PASSLOCK", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )])) + .alignment(Alignment::Center); + f.render_widget(title, left_layout[0]); if let Some(ref vault) = app.vault { let total = vault.e.len(); @@ -46,13 +50,6 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let tags = app.all_tags.len(); let cipher = vault_ffi::get_cipher(); - let info_area = Rect { - x: main_layout[1].x, - y: main_layout[1].y, - width: 52, - height: 10, - }; - let info_lines = vec![ Line::from(vec![Span::styled( "System Info", @@ -118,36 +115,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.theme.aqua())), ); - f.render_widget(info_box, info_area); - } - - if !app.msg.is_empty() { - #[allow(clippy::cast_possible_truncation)] - let msg_width = (app.msg.len().clamp(30, 60) as u16) + 4; - let status_area = Rect { - x: main_layout[1].x + main_layout[1].width - msg_width, - y: main_layout[1].y, - width: msg_width, - height: 3, - }; - - let msg_style = match app.msg_type { - MessageType::Success => Style::default().fg(app.theme.green()), - MessageType::Error => Style::default().fg(app.theme.red()), - MessageType::Info => Style::default().fg(app.theme.blue()), - MessageType::None => Style::default().fg(app.theme.fg()), - }; - - let status_msg = Paragraph::new(app.msg.as_str()) - .style(msg_style.add_modifier(Modifier::BOLD)) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(msg_style), - ); - f.render_widget(status_msg, status_area); + f.render_widget(info_box, left_layout[1]); } let all_items = [ @@ -210,5 +178,148 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { )); let menu = List::new(menu_list).block(menu_block); - f.render_widget(menu, main_layout[2]); + f.render_widget(menu, left_layout[2]); + + let right_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // status msg + Constraint::Length(1), // spacer + Constraint::Min(10), // pwd strength stats + Constraint::Length(1), // spacer + Constraint::Min(8), // recent activity + ]) + .split(main_layout[1]); + + if !app.msg.is_empty() { + let msg_style = match app.msg_type { + MessageType::Success => Style::default().fg(app.theme.green()), + MessageType::Error => Style::default().fg(app.theme.red()), + MessageType::Info => Style::default().fg(app.theme.blue()), + MessageType::None => Style::default().fg(app.theme.fg()), + }; + + let status_msg = Paragraph::new(app.msg.as_str()) + .style(msg_style.add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(msg_style), + ); + f.render_widget(status_msg, right_layout[0]); + } + + if let Some(ref vault) = app.vault { + let total = vault.e.len(); + let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); + let with_url = vault.e.iter().filter(|e| e.url.is_some()).count(); + + let stats_lines = vec![ + Line::from(vec![Span::styled( + "Password Stats", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(""), + Line::from(vec![ + Span::styled("Total ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{total}"), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + ), + ]), + Line::from(vec![ + Span::styled("With 2FA ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{with_2fa} "), + Style::default().fg(app.theme.green()), + ), + Span::styled( + format!( + "({}%)", + if total > 0 { + (with_2fa * 100) / total + } else { + 0 + } + ), + Style::default().fg(app.theme.gray()), + ), + ]), + Line::from(vec![ + Span::styled("With URL ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{with_url} "), + Style::default().fg(app.theme.blue()), + ), + Span::styled( + format!( + "({}%)", + if total > 0 { + (with_url * 100) / total + } else { + 0 + } + ), + Style::default().fg(app.theme.gray()), + ), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("▓", Style::default().fg(app.theme.green())), + Span::raw(" 2FA "), + Span::styled("▓", Style::default().fg(app.theme.blue())), + Span::raw(" URL"), + ]), + ]; + + let stats_box = Paragraph::new(stats_lines).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.yellow())), + ); + f.render_widget(stats_box, right_layout[2]); + + let tips = vec![ + Line::from(vec![Span::styled( + "Quick Tips", + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(""), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::styled( + "Press T/8 to change theme", + Style::default().fg(app.theme.fg()), + ), + ]), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::styled( + "Right-click for quick menu", + Style::default().fg(app.theme.fg()), + ), + ]), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::styled("Esc for options", Style::default().fg(app.theme.fg())), + ]), + ]; + + let tips_box = Paragraph::new(tips).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.purple())), + ); + f.render_widget(tips_box, right_layout[4]); + } } From 61a177f461468bf3d32360d840b1a038c60efbde Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 10:39:29 +0530 Subject: [PATCH 07/48] UPDATE: FINAL Iteration of Main Menu --- src/ui/widgets/menu.rs | 338 +++++++++++++++++++++++------------------ 1 file changed, 192 insertions(+), 146 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index cf4d295..5d54b9d 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -20,18 +20,18 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let main_layout = Layout::default() .direction(Direction::Horizontal) .margin(2) - .constraints([ - Constraint::Percentage(65), // left - Constraint::Percentage(35), // right - ]) + .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) .split(size); + let has_vault = app.vault.is_some(); + let system_info_height = if has_vault { 8 } else { 4 }; + let left_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // title - Constraint::Length(11), // system info - Constraint::Min(20), // menu + Constraint::Length(3), // title + Constraint::Length(system_info_height), // system info and crypto + Constraint::Min(20), // menu ]) .split(main_layout[0]); @@ -44,6 +44,14 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Center); f.render_widget(title, left_layout[0]); + let info_row_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(55), // system info + Constraint::Percentage(45), // crypto + ]) + .split(left_layout[1]); + if let Some(ref vault) = app.vault { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); @@ -58,64 +66,80 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .add_modifier(Modifier::BOLD), )]), Line::from(""), + Line::from(vec![Span::styled( + "v2.3.5", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + cipher, + Style::default().fg(app.theme.purple()), + )]), + Line::from(vec![Span::styled( + format!("{total} passwords"), + Style::default().fg(app.theme.green()), + )]), + Line::from(vec![Span::styled( + format!("{with_2fa} 2FA │ {tags} tags"), + Style::default().fg(app.theme.blue()), + )]), + ]; + + let info_box = Paragraph::new(info_lines).block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())), + ); + f.render_widget(info_box, info_row_layout[0]); + + let is_aes = vault_ffi::aes_sup(); + let crypto_lines = vec![ + Line::from(vec![Span::styled( + "Crypto", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), + Line::from(""), + Line::from(vec![Span::styled( + "┌─────────┐", + Style::default().fg(app.theme.gray()), + )]), Line::from(vec![ - Span::styled("Version ", Style::default().fg(app.theme.gray())), - Span::styled( - "v2.3.5", - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("Encryption ", Style::default().fg(app.theme.gray())), - Span::styled( - cipher, - Style::default() - .fg(app.theme.purple()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("Passwords ", Style::default().fg(app.theme.gray())), + Span::styled("│ ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{total}"), + if is_aes { "AES-NI" } else { "ChaCha" }, Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD), ), + Span::styled(" │", Style::default().fg(app.theme.gray())), ]), - Line::from(vec![ - Span::styled("2FA Enabled ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{with_2fa}"), - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("Tags ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{tags}"), - Style::default() - .fg(app.theme.blue()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("Theme ", Style::default().fg(app.theme.gray())), - Span::styled(app.theme.name(), Style::default().fg(app.theme.aqua())), - ]), + Line::from(vec![Span::styled( + "└─────────┘", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + "[ Hardware ]", + Style::default().fg(if is_aes { + app.theme.green() + } else { + app.theme.red() + }), + )]), ]; - let info_box = Paragraph::new(info_lines).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())), - ); - f.render_widget(info_box, left_layout[1]); + let crypto_box = Paragraph::new(crypto_lines) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.red())), + ); + f.render_widget(crypto_box, info_row_layout[1]); } let all_items = [ @@ -185,9 +209,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .constraints([ Constraint::Length(3), // status msg Constraint::Length(1), // spacer - Constraint::Min(10), // pwd strength stats - Constraint::Length(1), // spacer - Constraint::Min(8), // recent activity + Constraint::Min(10), // pwd stats ]) .split(main_layout[1]); @@ -215,111 +237,135 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let with_url = vault.e.iter().filter(|e| e.url.is_some()).count(); + let with_notes = vault + .e + .iter() + .filter(|e| e.nt.as_ref().is_some_and(|n| !n.is_empty())) + .count(); + + let two_fa_pct = if total > 0 { + (with_2fa * 100) / total + } else { + 0 + }; + let url_pct = if total > 0 { + (with_url * 100) / total + } else { + 0 + }; + let notes_pct = if total > 0 { + (with_notes * 100) / total + } else { + 0 + }; - let stats_lines = vec![ + let create_bar = |pct: usize| -> String { + let filled = pct / 10; + let mut bar = String::new(); + for i in 0..10 { + bar.push(if i < filled { '█' } else { '░' }); + } + bar + }; + + let mut stats_lines = vec![ Line::from(vec![Span::styled( - "Password Stats", + "Password Statistics", Style::default() .fg(app.theme.yellow()) .add_modifier(Modifier::BOLD), )]), Line::from(""), - Line::from(vec![ - Span::styled("Total ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{total}"), - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - ), - ]), - Line::from(vec![ - Span::styled("With 2FA ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{with_2fa} "), - Style::default().fg(app.theme.green()), - ), - Span::styled( - format!( - "({}%)", - if total > 0 { - (with_2fa * 100) / total - } else { - 0 - } - ), - Style::default().fg(app.theme.gray()), - ), - ]), - Line::from(vec![ - Span::styled("With URL ", Style::default().fg(app.theme.gray())), - Span::styled( - format!("{with_url} "), - Style::default().fg(app.theme.blue()), - ), - Span::styled( - format!( - "({}%)", - if total > 0 { - (with_url * 100) / total - } else { - 0 - } - ), - Style::default().fg(app.theme.gray()), - ), - ]), + Line::from(vec![Span::styled( + "━━━━━━━━━━━━━━━━━━━━━━━", + Style::default().fg(app.theme.gray()), + )]), Line::from(""), - Line::from(vec![ - Span::styled("▓", Style::default().fg(app.theme.green())), - Span::raw(" 2FA "), - Span::styled("▓", Style::default().fg(app.theme.blue())), - Span::raw(" URL"), - ]), - ]; - - let stats_box = Paragraph::new(stats_lines).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.yellow())), - ); - f.render_widget(stats_box, right_layout[2]); - - let tips = vec![ Line::from(vec![Span::styled( - "Quick Tips", + "Total Entries", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {total}"), Style::default() - .fg(app.theme.purple()) + .fg(app.theme.yellow()) .add_modifier(Modifier::BOLD), )]), Line::from(""), - Line::from(vec![ - Span::styled("• ", Style::default().fg(app.theme.orange())), - Span::styled( - "Press T/8 to change theme", - Style::default().fg(app.theme.fg()), - ), - ]), - Line::from(vec![ - Span::styled("• ", Style::default().fg(app.theme.orange())), - Span::styled( - "Right-click for quick menu", - Style::default().fg(app.theme.fg()), - ), - ]), - Line::from(vec![ - Span::styled("• ", Style::default().fg(app.theme.orange())), - Span::styled("Esc for options", Style::default().fg(app.theme.fg())), - ]), + Line::from(vec![Span::styled( + "2FA Protection", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {}", create_bar(two_fa_pct)), + Style::default().fg(app.theme.green()), + )]), + Line::from(vec![Span::styled( + format!(" {with_2fa} entries ({two_fa_pct}%)"), + Style::default().fg(app.theme.green()), + )]), + Line::from(""), + Line::from(vec![Span::styled( + "With URLs", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {}", create_bar(url_pct)), + Style::default().fg(app.theme.blue()), + )]), + Line::from(vec![Span::styled( + format!(" {with_url} entries ({url_pct}%)"), + Style::default().fg(app.theme.blue()), + )]), + Line::from(""), + Line::from(vec![Span::styled( + "With Notes", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {}", create_bar(notes_pct)), + Style::default().fg(app.theme.purple()), + )]), + Line::from(vec![Span::styled( + format!(" {with_notes} entries ({notes_pct}%)"), + Style::default().fg(app.theme.purple()), + )]), ]; - let tips_box = Paragraph::new(tips).block( + if !app.all_tags.is_empty() { + stats_lines.push(Line::from("")); + stats_lines.push(Line::from(vec![Span::styled( + "━━━━━━━━━━━━━━━━━━━━━━━", + Style::default().fg(app.theme.gray()), + )])); + stats_lines.push(Line::from("")); + stats_lines.push(Line::from(vec![Span::styled( + "Top Tags", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )])); + + for (tag, count) in app.all_tags.iter().take(3) { + stats_lines.push(Line::from(vec![ + Span::styled( + format!(" {tag} "), + Style::default().fg(app.theme.purple()), + ), + Span::styled( + format!("({count})"), + Style::default().fg(app.theme.gray()), + ), + ])); + } + } + + let stats_box = Paragraph::new(stats_lines).block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.purple())), + .border_style(Style::default().fg(app.theme.yellow())), ); - f.render_widget(tips_box, right_layout[4]); + f.render_widget(stats_box, right_layout[2]); } } From be8b03f95d9b014706feffd7f3de300762262d95 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 11:00:27 +0530 Subject: [PATCH 08/48] FIX: Fixed the formatting in Main Menu --- src/ui/widgets/menu.rs | 339 ++++++++++++++++------------------------- 1 file changed, 129 insertions(+), 210 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 5d54b9d..3b14893 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -6,7 +6,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, + widgets::{Block, Borders, BorderType, List, ListItem, Paragraph}, Frame, }; @@ -20,35 +20,37 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let main_layout = Layout::default() .direction(Direction::Horizontal) .margin(2) - .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) + .constraints([ + Constraint::Percentage(65), + Constraint::Percentage(35), + ]) .split(size); let has_vault = app.vault.is_some(); let system_info_height = if has_vault { 8 } else { 4 }; - + let left_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // title - Constraint::Length(system_info_height), // system info and crypto - Constraint::Min(20), // menu + Constraint::Length(3), // title + Constraint::Length(system_info_height), // system info and crypto + Constraint::Min(20), // menu ]) .split(main_layout[0]); - let title = Paragraph::new(Line::from(vec![Span::styled( - "PASSLOCK", - Style::default() + let title = Paragraph::new(Line::from(vec![ + Span::styled("PASSLOCK", Style::default() .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )])) + .add_modifier(Modifier::BOLD)), + ])) .alignment(Alignment::Center); f.render_widget(title, left_layout[0]); let info_row_layout = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Percentage(55), // system info - Constraint::Percentage(45), // crypto + Constraint::Percentage(55), // system info + Constraint::Percentage(45), // crypto ]) .split(left_layout[1]); @@ -57,88 +59,65 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let tags = app.all_tags.len(); let cipher = vault_ffi::get_cipher(); - + let info_lines = vec![ - Line::from(vec![Span::styled( - "System Info", - Style::default() - .fg(app.theme.aqua()) - .add_modifier(Modifier::BOLD), - )]), + Line::from(vec![ + Span::styled("System Info", Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD)), + ]), Line::from(""), - Line::from(vec![Span::styled( - "v2.3.5", - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - )]), - Line::from(vec![Span::styled( - cipher, - Style::default().fg(app.theme.purple()), - )]), - Line::from(vec![Span::styled( - format!("{total} passwords"), - Style::default().fg(app.theme.green()), - )]), - Line::from(vec![Span::styled( - format!("{with_2fa} 2FA │ {tags} tags"), - Style::default().fg(app.theme.blue()), - )]), + Line::from(vec![ + Span::styled("Build: v", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), + Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(app.theme.yellow())), + ]), + Line::from(vec![ + Span::styled("Cipher: ", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), + Span::styled(cipher, Style::default().fg(app.theme.purple())), + ]), + Line::from(vec![ + Span::styled("Entries: ", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), + Span::styled(format!("{}", total), Style::default().fg(app.theme.green())), + ]), + Line::from(vec![ + Span::styled("Stats: ", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), + Span::styled(format!("{} 2FA │ {} tags", with_2fa, tags), Style::default().fg(app.theme.blue())), + ]), ]; - let info_box = Paragraph::new(info_lines).block( - Block::default() + let info_box = Paragraph::new(info_lines) + .block(Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())), - ); + .border_style(Style::default().fg(app.theme.aqua()))); f.render_widget(info_box, info_row_layout[0]); let is_aes = vault_ffi::aes_sup(); let crypto_lines = vec![ - Line::from(vec![Span::styled( - "Crypto", - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - )]), + Line::from(vec![ + Span::styled("Crypto", Style::default().fg(app.theme.red()).add_modifier(Modifier::BOLD)), + ]), Line::from(""), - Line::from(vec![Span::styled( - "┌─────────┐", - Style::default().fg(app.theme.gray()), - )]), Line::from(vec![ - Span::styled("│ ", Style::default().fg(app.theme.gray())), - Span::styled( - if is_aes { "AES-NI" } else { "ChaCha" }, - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - ), + Span::styled("┌──────────┐", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled("│ ", Style::default().fg(app.theme.gray())), + Span::styled(if is_aes { "AES-NI" } else { "ChaCha" }, Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD)), Span::styled(" │", Style::default().fg(app.theme.gray())), ]), - Line::from(vec![Span::styled( - "└─────────┘", - Style::default().fg(app.theme.gray()), - )]), - Line::from(vec![Span::styled( - "[ Hardware ]", - Style::default().fg(if is_aes { - app.theme.green() - } else { - app.theme.red() - }), - )]), + Line::from(vec![ + Span::styled("└──────────┘", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled("Hardware", Style::default().fg(if is_aes { app.theme.green() } else { app.theme.red() })), + ]), ]; let crypto_box = Paragraph::new(crypto_lines) .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.red())), - ); + .block(Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.red()))); f.render_widget(crypto_box, info_row_layout[1]); } @@ -157,28 +136,19 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .enumerate() .map(|(i, (num, title, desc))| { let is_selected = i == app.selected_menu; - + let title_style = if is_selected { - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD) + Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) } else { - Style::default() - .fg(app.theme.fg()) - .add_modifier(Modifier::BOLD) + Style::default().fg(app.theme.fg()).add_modifier(Modifier::BOLD) }; - + let prefix = if is_selected { "▶ " } else { " " }; - + ListItem::new(vec![ Line::from(vec![ Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled( - format!("[{num}] "), - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), + Span::styled(format!("[{}] ", num), Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), Span::styled(*title, title_style), ]), Line::from(vec![ @@ -194,12 +164,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.theme.green())) - .title(Span::styled( - " [ Menu ] ", - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - )); + .title(Span::styled(" [ Menu ] ", Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD))); let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, left_layout[2]); @@ -207,9 +172,9 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let right_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // status msg - Constraint::Length(1), // spacer - Constraint::Min(10), // pwd stats + Constraint::Length(3), // status msg + Constraint::Length(1), // spacer + Constraint::Min(10), // pwd stats ]) .split(main_layout[1]); @@ -224,12 +189,10 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let status_msg = Paragraph::new(app.msg.as_str()) .style(msg_style.add_modifier(Modifier::BOLD)) .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(msg_style), - ); + .block(Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(msg_style)); f.render_widget(status_msg, right_layout[0]); } @@ -237,28 +200,12 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let with_url = vault.e.iter().filter(|e| e.url.is_some()).count(); - let with_notes = vault - .e - .iter() - .filter(|e| e.nt.as_ref().is_some_and(|n| !n.is_empty())) - .count(); - - let two_fa_pct = if total > 0 { - (with_2fa * 100) / total - } else { - 0 - }; - let url_pct = if total > 0 { - (with_url * 100) / total - } else { - 0 - }; - let notes_pct = if total > 0 { - (with_notes * 100) / total - } else { - 0 - }; - + let with_notes = vault.e.iter().filter(|e| e.nt.as_ref().map_or(false, |n| !n.is_empty())).count(); + + let two_fa_pct = if total > 0 { (with_2fa * 100) / total } else { 0 }; + let url_pct = if total > 0 { (with_url * 100) / total } else { 0 }; + let notes_pct = if total > 0 { (with_notes * 100) / total } else { 0 }; + let create_bar = |pct: usize| -> String { let filled = pct / 10; let mut bar = String::new(); @@ -267,105 +214,77 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { } bar }; - + let mut stats_lines = vec![ - Line::from(vec![Span::styled( - "Password Statistics", - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - )]), + Line::from(vec![ + Span::styled("Password Statistics", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD)), + ]), Line::from(""), - Line::from(vec![Span::styled( - "━━━━━━━━━━━━━━━━━━━━━━━", - Style::default().fg(app.theme.gray()), - )]), + Line::from(vec![ + Span::styled("━━━━━━━━━━━━━━━━━━━━━━━", Style::default().fg(app.theme.gray())), + ]), Line::from(""), - Line::from(vec![Span::styled( - "Total Entries", - Style::default().fg(app.theme.gray()), - )]), - Line::from(vec![Span::styled( - format!(" {total}"), - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - )]), + Line::from(vec![ + Span::styled("Total Entries", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled(format!(" {}", total), Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD)), + ]), Line::from(""), - Line::from(vec![Span::styled( - "2FA Protection", - Style::default().fg(app.theme.gray()), - )]), - Line::from(vec![Span::styled( - format!(" {}", create_bar(two_fa_pct)), - Style::default().fg(app.theme.green()), - )]), - Line::from(vec![Span::styled( - format!(" {with_2fa} entries ({two_fa_pct}%)"), - Style::default().fg(app.theme.green()), - )]), + Line::from(vec![ + Span::styled("2FA Protection", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled(format!(" {}", create_bar(two_fa_pct)), Style::default().fg(app.theme.green())), + ]), + Line::from(vec![ + Span::styled(format!(" {} entries ({}%)", with_2fa, two_fa_pct), Style::default().fg(app.theme.green())), + ]), Line::from(""), - Line::from(vec![Span::styled( - "With URLs", - Style::default().fg(app.theme.gray()), - )]), - Line::from(vec![Span::styled( - format!(" {}", create_bar(url_pct)), - Style::default().fg(app.theme.blue()), - )]), - Line::from(vec![Span::styled( - format!(" {with_url} entries ({url_pct}%)"), - Style::default().fg(app.theme.blue()), - )]), + Line::from(vec![ + Span::styled("With URLs", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled(format!(" {}", create_bar(url_pct)), Style::default().fg(app.theme.blue())), + ]), + Line::from(vec![ + Span::styled(format!(" {} entries ({}%)", with_url, url_pct), Style::default().fg(app.theme.blue())), + ]), Line::from(""), - Line::from(vec![Span::styled( - "With Notes", - Style::default().fg(app.theme.gray()), - )]), - Line::from(vec![Span::styled( - format!(" {}", create_bar(notes_pct)), - Style::default().fg(app.theme.purple()), - )]), - Line::from(vec![Span::styled( - format!(" {with_notes} entries ({notes_pct}%)"), - Style::default().fg(app.theme.purple()), - )]), + Line::from(vec![ + Span::styled("With Notes", Style::default().fg(app.theme.gray())), + ]), + Line::from(vec![ + Span::styled(format!(" {}", create_bar(notes_pct)), Style::default().fg(app.theme.purple())), + ]), + Line::from(vec![ + Span::styled(format!(" {} entries ({}%)", with_notes, notes_pct), Style::default().fg(app.theme.purple())), + ]), ]; if !app.all_tags.is_empty() { stats_lines.push(Line::from("")); - stats_lines.push(Line::from(vec![Span::styled( - "━━━━━━━━━━━━━━━━━━━━━━━", - Style::default().fg(app.theme.gray()), - )])); + stats_lines.push(Line::from(vec![ + Span::styled("━━━━━━━━━━━━━━━━━━━━━━━", Style::default().fg(app.theme.gray())), + ])); stats_lines.push(Line::from("")); - stats_lines.push(Line::from(vec![Span::styled( - "Top Tags", - Style::default() - .fg(app.theme.aqua()) - .add_modifier(Modifier::BOLD), - )])); - + stats_lines.push(Line::from(vec![ + Span::styled("Top Tags", Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD)), + ])); + for (tag, count) in app.all_tags.iter().take(3) { stats_lines.push(Line::from(vec![ - Span::styled( - format!(" {tag} "), - Style::default().fg(app.theme.purple()), - ), - Span::styled( - format!("({count})"), - Style::default().fg(app.theme.gray()), - ), + Span::styled(format!(" {} ", tag), Style::default().fg(app.theme.purple())), + Span::styled(format!("({})", count), Style::default().fg(app.theme.gray())), ])); } } - let stats_box = Paragraph::new(stats_lines).block( - Block::default() + let stats_box = Paragraph::new(stats_lines) + .block(Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.yellow())), - ); + .border_style(Style::default().fg(app.theme.yellow()))); f.render_widget(stats_box, right_layout[2]); } -} +} \ No newline at end of file From b0423163d65cfe33000995c84b897994fc640213 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 11:06:48 +0530 Subject: [PATCH 09/48] FIX: Fixed the formatting in Main Menu --- src/ui/widgets/menu.rs | 343 +++++++++++++++++++++++++++-------------- 1 file changed, 224 insertions(+), 119 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 3b14893..c3ee815 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -6,7 +6,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, BorderType, List, ListItem, Paragraph}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, Frame, }; @@ -20,37 +20,35 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let main_layout = Layout::default() .direction(Direction::Horizontal) .margin(2) - .constraints([ - Constraint::Percentage(65), - Constraint::Percentage(35), - ]) + .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) .split(size); let has_vault = app.vault.is_some(); let system_info_height = if has_vault { 8 } else { 4 }; - + let left_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // title - Constraint::Length(system_info_height), // system info and crypto - Constraint::Min(20), // menu + Constraint::Length(3), // title + Constraint::Length(system_info_height), // system info and crypto + Constraint::Min(20), // menu ]) .split(main_layout[0]); - let title = Paragraph::new(Line::from(vec![ - Span::styled("PASSLOCK", Style::default() + let title = Paragraph::new(Line::from(vec![Span::styled( + "PASSLOCK", + Style::default() .fg(app.theme.red()) - .add_modifier(Modifier::BOLD)), - ])) + .add_modifier(Modifier::BOLD), + )])) .alignment(Alignment::Center); f.render_widget(title, left_layout[0]); let info_row_layout = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Percentage(55), // system info - Constraint::Percentage(45), // crypto + Constraint::Percentage(55), // system info + Constraint::Percentage(45), // crypto ]) .split(left_layout[1]); @@ -59,65 +57,112 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let tags = app.all_tags.len(); let cipher = vault_ffi::get_cipher(); - + let info_lines = vec![ - Line::from(vec![ - Span::styled("System Info", Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD)), - ]), + Line::from(vec![Span::styled( + "System Info", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), Line::from(vec![ - Span::styled("Build: v", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), - Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(app.theme.yellow())), + Span::styled( + "Build: v", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + env!("CARGO_PKG_VERSION"), + Style::default().fg(app.theme.yellow()), + ), ]), Line::from(vec![ - Span::styled("Cipher: ", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), + Span::styled( + "Cipher: ", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + ), Span::styled(cipher, Style::default().fg(app.theme.purple())), ]), Line::from(vec![ - Span::styled("Entries: ", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), - Span::styled(format!("{}", total), Style::default().fg(app.theme.green())), + Span::styled( + "Entries: ", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(format!("{total}"), Style::default().fg(app.theme.green())), ]), Line::from(vec![ - Span::styled("Stats: ", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD)), - Span::styled(format!("{} 2FA │ {} tags", with_2fa, tags), Style::default().fg(app.theme.blue())), + Span::styled( + "Stats: ", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!("{with_2fa} 2FA │ {tags} tags"), + Style::default().fg(app.theme.blue()), + ), ]), ]; - let info_box = Paragraph::new(info_lines) - .block(Block::default() + let info_box = Paragraph::new(info_lines).block( + Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua()))); + .border_style(Style::default().fg(app.theme.aqua())), + ); f.render_widget(info_box, info_row_layout[0]); let is_aes = vault_ffi::aes_sup(); let crypto_lines = vec![ - Line::from(vec![ - Span::styled("Crypto", Style::default().fg(app.theme.red()).add_modifier(Modifier::BOLD)), - ]), + Line::from(vec![Span::styled( + "Crypto", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), - Line::from(vec![ - Span::styled("┌──────────┐", Style::default().fg(app.theme.gray())), - ]), + Line::from(vec![Span::styled( + "┌──────────┐", + Style::default().fg(app.theme.gray()), + )]), Line::from(vec![ Span::styled("│ ", Style::default().fg(app.theme.gray())), - Span::styled(if is_aes { "AES-NI" } else { "ChaCha" }, Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD)), + Span::styled( + if is_aes { "AES-NI" } else { "ChaCha" }, + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" │", Style::default().fg(app.theme.gray())), ]), - Line::from(vec![ - Span::styled("└──────────┘", Style::default().fg(app.theme.gray())), - ]), - Line::from(vec![ - Span::styled("Hardware", Style::default().fg(if is_aes { app.theme.green() } else { app.theme.red() })), - ]), + Line::from(vec![Span::styled( + "└──────────┘", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + "Hardware", + Style::default().fg(if is_aes { + app.theme.green() + } else { + app.theme.red() + }), + )]), ]; let crypto_box = Paragraph::new(crypto_lines) .alignment(Alignment::Center) - .block(Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.red()))); + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.red())), + ); f.render_widget(crypto_box, info_row_layout[1]); } @@ -136,19 +181,28 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .enumerate() .map(|(i, (num, title, desc))| { let is_selected = i == app.selected_menu; - + let title_style = if is_selected { - Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD) } else { - Style::default().fg(app.theme.fg()).add_modifier(Modifier::BOLD) + Style::default() + .fg(app.theme.fg()) + .add_modifier(Modifier::BOLD) }; - + let prefix = if is_selected { "▶ " } else { " " }; - + ListItem::new(vec![ Line::from(vec![ Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled(format!("[{}] ", num), Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + format!("[{num}] "), + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(*title, title_style), ]), Line::from(vec![ @@ -164,7 +218,12 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.theme.green())) - .title(Span::styled(" [ Menu ] ", Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD))); + .title(Span::styled( + " [ Menu ] ", + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + )); let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, left_layout[2]); @@ -172,9 +231,9 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let right_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // status msg - Constraint::Length(1), // spacer - Constraint::Min(10), // pwd stats + Constraint::Length(3), // status msg + Constraint::Length(1), // spacer + Constraint::Min(10), // pwd stats ]) .split(main_layout[1]); @@ -189,10 +248,12 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let status_msg = Paragraph::new(app.msg.as_str()) .style(msg_style.add_modifier(Modifier::BOLD)) .alignment(Alignment::Center) - .block(Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(msg_style)); + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(msg_style), + ); f.render_widget(status_msg, right_layout[0]); } @@ -200,12 +261,28 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let total = vault.e.len(); let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let with_url = vault.e.iter().filter(|e| e.url.is_some()).count(); - let with_notes = vault.e.iter().filter(|e| e.nt.as_ref().map_or(false, |n| !n.is_empty())).count(); - - let two_fa_pct = if total > 0 { (with_2fa * 100) / total } else { 0 }; - let url_pct = if total > 0 { (with_url * 100) / total } else { 0 }; - let notes_pct = if total > 0 { (with_notes * 100) / total } else { 0 }; - + let with_notes = vault + .e + .iter() + .filter(|e| e.nt.as_ref().is_some_and(|n| !n.is_empty())) + .count(); + + let two_fa_pct = if total > 0 { + (with_2fa * 100) / total + } else { + 0 + }; + let url_pct = if total > 0 { + (with_url * 100) / total + } else { + 0 + }; + let notes_pct = if total > 0 { + (with_notes * 100) / total + } else { + 0 + }; + let create_bar = |pct: usize| -> String { let filled = pct / 10; let mut bar = String::new(); @@ -214,77 +291,105 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { } bar }; - + let mut stats_lines = vec![ - Line::from(vec![ - Span::styled("Password Statistics", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD)), - ]), + Line::from(vec![Span::styled( + "Password Statistics", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), - Line::from(vec![ - Span::styled("━━━━━━━━━━━━━━━━━━━━━━━", Style::default().fg(app.theme.gray())), - ]), + Line::from(vec![Span::styled( + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + Style::default().fg(app.theme.gray()), + )]), Line::from(""), - Line::from(vec![ - Span::styled("Total Entries", Style::default().fg(app.theme.gray())), - ]), - Line::from(vec![ - Span::styled(format!(" {}", total), Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD)), - ]), + Line::from(vec![Span::styled( + "Total Entries", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {total}"), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), - Line::from(vec![ - Span::styled("2FA Protection", Style::default().fg(app.theme.gray())), - ]), - Line::from(vec![ - Span::styled(format!(" {}", create_bar(two_fa_pct)), Style::default().fg(app.theme.green())), - ]), - Line::from(vec![ - Span::styled(format!(" {} entries ({}%)", with_2fa, two_fa_pct), Style::default().fg(app.theme.green())), - ]), + Line::from(vec![Span::styled( + "2FA Protection", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {}", create_bar(two_fa_pct)), + Style::default().fg(app.theme.green()), + )]), + Line::from(vec![Span::styled( + format!(" {with_2fa} entries ({two_fa_pct}%)"), + Style::default().fg(app.theme.green()), + )]), Line::from(""), - Line::from(vec![ - Span::styled("With URLs", Style::default().fg(app.theme.gray())), - ]), - Line::from(vec![ - Span::styled(format!(" {}", create_bar(url_pct)), Style::default().fg(app.theme.blue())), - ]), - Line::from(vec![ - Span::styled(format!(" {} entries ({}%)", with_url, url_pct), Style::default().fg(app.theme.blue())), - ]), + Line::from(vec![Span::styled( + "With URLs", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {}", create_bar(url_pct)), + Style::default().fg(app.theme.blue()), + )]), + Line::from(vec![Span::styled( + format!(" {with_url} entries ({url_pct}%)"), + Style::default().fg(app.theme.blue()), + )]), Line::from(""), - Line::from(vec![ - Span::styled("With Notes", Style::default().fg(app.theme.gray())), - ]), - Line::from(vec![ - Span::styled(format!(" {}", create_bar(notes_pct)), Style::default().fg(app.theme.purple())), - ]), - Line::from(vec![ - Span::styled(format!(" {} entries ({}%)", with_notes, notes_pct), Style::default().fg(app.theme.purple())), - ]), + Line::from(vec![Span::styled( + "With Notes", + Style::default().fg(app.theme.gray()), + )]), + Line::from(vec![Span::styled( + format!(" {}", create_bar(notes_pct)), + Style::default().fg(app.theme.purple()), + )]), + Line::from(vec![Span::styled( + format!(" {with_notes} entries ({notes_pct}%)"), + Style::default().fg(app.theme.purple()), + )]), ]; if !app.all_tags.is_empty() { stats_lines.push(Line::from("")); - stats_lines.push(Line::from(vec![ - Span::styled("━━━━━━━━━━━━━━━━━━━━━━━", Style::default().fg(app.theme.gray())), - ])); + stats_lines.push(Line::from(vec![Span::styled( + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + Style::default().fg(app.theme.gray()), + )])); stats_lines.push(Line::from("")); - stats_lines.push(Line::from(vec![ - Span::styled("Top Tags", Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD)), - ])); - + stats_lines.push(Line::from(vec![Span::styled( + "Top Tags", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )])); + for (tag, count) in app.all_tags.iter().take(3) { stats_lines.push(Line::from(vec![ - Span::styled(format!(" {} ", tag), Style::default().fg(app.theme.purple())), - Span::styled(format!("({})", count), Style::default().fg(app.theme.gray())), + Span::styled( + format!(" {tag} "), + Style::default().fg(app.theme.purple()), + ), + Span::styled( + format!("({count})"), + Style::default().fg(app.theme.gray()), + ), ])); } } - let stats_box = Paragraph::new(stats_lines) - .block(Block::default() + let stats_box = Paragraph::new(stats_lines).block( + Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.yellow()))); + .border_style(Style::default().fg(app.theme.yellow())), + ); f.render_widget(stats_box, right_layout[2]); } -} \ No newline at end of file +} From ca742412e0f4cec40ab8cd361e92d680efad0f42 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 11:14:10 +0530 Subject: [PATCH 10/48] FIX: fmt and clippy --- src/ui/widgets/menu.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index c3ee815..07393cb 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -376,10 +376,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { format!(" {tag} "), Style::default().fg(app.theme.purple()), ), - Span::styled( - format!("({count})"), - Style::default().fg(app.theme.gray()), - ), + Span::styled(format!("({count})"), Style::default().fg(app.theme.gray())), ])); } } From a491f0defd7902b546caccfb03c45694f4310bac Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 12:59:49 +0530 Subject: [PATCH 11/48] FIX: fmt and clippy --- src/ui/widgets/menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 07393cb..6fe3381 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -389,4 +389,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { ); f.render_widget(stats_box, right_layout[2]); } -} +} \ No newline at end of file From 0ca87314aa3e7c5cd0ff2f11611e7c768a31b659 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Tue, 24 Feb 2026 13:01:39 +0530 Subject: [PATCH 12/48] FIX: fmt and clippy --- src/ui/widgets/menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 6fe3381..07393cb 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -389,4 +389,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { ); f.render_widget(stats_box, right_layout[2]); } -} \ No newline at end of file +} From f071ad07abac6e5fdcdb4e81d3a9bd494dd2176c Mon Sep 17 00:00:00 2001 From: hachimamma Date: Wed, 25 Feb 2026 20:02:22 +0530 Subject: [PATCH 13/48] FIX: Fixed UI Added Flags to test for lateral space available - If space is not adequate, cipher will be shortened - For AES, HW will be displayed appropriately - If enough space is available, full cipher test will be displayed Increased spacing between all selectable functions Increased spacing between [ Menu ] and View Passwords --- src/ui/widgets/menu.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 07393cb..d12e97b 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -24,13 +24,13 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .split(size); let has_vault = app.vault.is_some(); - let system_info_height = if has_vault { 8 } else { 4 }; + let sysinfo_height = if has_vault { 8 } else { 4 }; let left_layout = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // title - Constraint::Length(system_info_height), // system info and crypto + Constraint::Length(sysinfo_height), // system info and crypto Constraint::Min(20), // menu ]) .split(main_layout[0]); @@ -44,7 +44,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Center); f.render_widget(title, left_layout[0]); - let info_row_layout = Layout::default() + let info_rl = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(55), // system info @@ -57,6 +57,14 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let tags = app.all_tags.len(); let cipher = vault_ffi::get_cipher(); + + let cipher_display = if info_rl[0].width < 30 { + if vault_ffi::aes_sup() { "AES-256-GCM" } else { "ChaCha20-Poly1305" } + } else if info_rl[0].width < 45 { + if vault_ffi::aes_sup() { "AES-256-GCM (HW)" } else { "ChaCha20-Poly1305" } + } else { + cipher + }; let info_lines = vec![ Line::from(vec![Span::styled( @@ -85,7 +93,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .fg(app.theme.gray()) .add_modifier(Modifier::BOLD), ), - Span::styled(cipher, Style::default().fg(app.theme.purple())), + Span::styled(cipher_display, Style::default().fg(app.theme.purple())), ]), Line::from(vec![ Span::styled( @@ -116,7 +124,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.theme.aqua())), ); - f.render_widget(info_box, info_row_layout[0]); + f.render_widget(info_box, info_rl[0]); let is_aes = vault_ffi::aes_sup(); let crypto_lines = vec![ @@ -163,7 +171,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.theme.red())), ); - f.render_widget(crypto_box, info_row_layout[1]); + f.render_widget(crypto_box, info_rl[1]); } let all_items = [ @@ -194,7 +202,13 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let prefix = if is_selected { "▶ " } else { " " }; - ListItem::new(vec![ + let mut lines = vec![]; + + if i == 0 { + lines.push(Line::from("")); + } + + lines.extend(vec![ Line::from(vec![ Span::styled(prefix, Style::default().fg(app.theme.yellow())), Span::styled( @@ -210,7 +224,10 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { Span::styled(*desc, Style::default().fg(app.theme.gray())), ]), Line::from(""), - ]) + Line::from(""), + ]); + + ListItem::new(lines) }) .collect(); @@ -389,4 +406,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { ); f.render_widget(stats_box, right_layout[2]); } -} +} \ No newline at end of file From d6324b588b14b6cc596eac9bbce17c84ae811327 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Wed, 25 Feb 2026 20:03:30 +0530 Subject: [PATCH 14/48] FIX: Formatting and Clippy --- src/ui/widgets/menu.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index d12e97b..43521be 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -29,9 +29,9 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let left_layout = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // title + Constraint::Length(3), // title Constraint::Length(sysinfo_height), // system info and crypto - Constraint::Min(20), // menu + Constraint::Min(20), // menu ]) .split(main_layout[0]); @@ -57,11 +57,19 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let with_2fa = vault.e.iter().filter(|e| e.totp_secret.is_some()).count(); let tags = app.all_tags.len(); let cipher = vault_ffi::get_cipher(); - + let cipher_display = if info_rl[0].width < 30 { - if vault_ffi::aes_sup() { "AES-256-GCM" } else { "ChaCha20-Poly1305" } + if vault_ffi::aes_sup() { + "AES-256-GCM" + } else { + "ChaCha20-Poly1305" + } } else if info_rl[0].width < 45 { - if vault_ffi::aes_sup() { "AES-256-GCM (HW)" } else { "ChaCha20-Poly1305" } + if vault_ffi::aes_sup() { + "AES-256-GCM (HW)" + } else { + "ChaCha20-Poly1305" + } } else { cipher }; @@ -203,11 +211,11 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { let prefix = if is_selected { "▶ " } else { " " }; let mut lines = vec![]; - + if i == 0 { lines.push(Line::from("")); } - + lines.extend(vec![ Line::from(vec![ Span::styled(prefix, Style::default().fg(app.theme.yellow())), @@ -226,7 +234,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { Line::from(""), Line::from(""), ]); - + ListItem::new(lines) }) .collect(); @@ -406,4 +414,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { ); f.render_widget(stats_box, right_layout[2]); } -} \ No newline at end of file +} From 8aa42c5262f4eff4ef201ce931c1fc6df6ff751a Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 10:03:32 +0530 Subject: [PATCH 15/48] UPDATE: Re-iterated SearchPasswords and FilterByTags screens --- src/ui/widgets/utility.rs | 357 ++++++++++++++++++++++++++------------ 1 file changed, 247 insertions(+), 110 deletions(-) diff --git a/src/ui/widgets/utility.rs b/src/ui/widgets/utility.rs index 11280e0..1b59589 100644 --- a/src/ui/widgets/utility.rs +++ b/src/ui/widgets/utility.rs @@ -28,38 +28,70 @@ pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { } pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.blue())) + .title(Span::styled( + " [ Search Passwords ] ", + Style::default() + .fg(app.theme.blue()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, size); + let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) + .margin(2) .constraints([ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Min(5), - Constraint::Length(3), + Constraint::Length(3), // search bar + Constraint::Length(1), // spacer + Constraint::Min(5), // results ]) .split(size); - let block = Block::default() + + let search_area = chunks[0]; + let search_block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.blue())) - .title("═══ SEARCH PASSWORDS ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, size); - let title = Paragraph::new("Search by name, username, URL, or tags") - .style(Style::default().fg(app.theme.yellow())) - .alignment(Alignment::Center); - f.render_widget(title, chunks[0]); - let search = Paragraph::new(format!("Search: {}", app.search_query)).style( - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - ); - f.render_widget(search, chunks[1]); + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(if app.search_query.is_empty() { + app.theme.gray() + } else { + app.theme.green() + })) + .title(Span::styled( + " Search ", + Style::default().fg(app.theme.yellow()), + )); + + let search_text = if app.search_query.is_empty() { + Span::styled("Type to search...", Style::default().fg(app.theme.gray())) + } else { + Span::styled(&app.search_query, Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD)) + }; + + let search_widget = Paragraph::new(search_text) + .block(search_block) + .alignment(Alignment::Left); + f.render_widget(search_widget, search_area); + if app.entry_disp.is_empty() && !app.search_query.is_empty() { - let empty = Paragraph::new("[ No matches found ]") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(empty, chunks[2]); + let empty_area = centered_rect(60, 20, chunks[2]); + let empty = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled("No matches found", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(""), + Line::from(Span::styled("Try a different search term", Style::default().fg(app.theme.gray()))), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); + f.render_widget(empty, empty_area); } else if !app.entry_disp.is_empty() { let items: Vec = app .entry_disp @@ -67,7 +99,7 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { .map(|entry| { let mut lines = vec![ Line::from(vec![ - Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::styled("▶ ", Style::default().fg(app.theme.orange())), Span::styled( &entry.n, Style::default() @@ -76,34 +108,76 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { ), ]), Line::from(vec![ - Span::styled(" User: ", Style::default().fg(app.theme.gray())), + Span::styled(" Username: ", Style::default().fg(app.theme.gray())), Span::styled(&entry.u, Style::default().fg(app.theme.blue())), ]), - Line::from(vec![ - Span::styled(" Pass: ", Style::default().fg(app.theme.gray())), - Span::styled(&entry.p, Style::default().fg(app.theme.green())), - ]), ]; + + if let Some(ref url) = entry.url { + lines.push(Line::from(vec![ + Span::styled(" URL: ", Style::default().fg(app.theme.gray())), + Span::styled(url, Style::default().fg(app.theme.aqua())), + ])); + } + if !entry.tags.is_empty() { lines.push(Line::from(vec![ Span::styled(" Tags: ", Style::default().fg(app.theme.gray())), Span::styled( entry.tags.join(", "), - Style::default().fg(app.theme.orange()), + Style::default().fg(app.theme.purple()), ), ])); } + lines.push(Line::from("")); ListItem::new(lines) }) .collect(); - let list = List::new(items).block(Block::default().borders(Borders::NONE)); + + let list = List::new(items).block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + .title(Span::styled( + format!(" [ {} Results ] ", app.entry_disp.len()), + Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), + )) + ); f.render_widget(list, chunks[2]); + } else { + let tips_area = centered_rect(70, 30, chunks[2]); + let tips = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled("Quick Search Tips", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD))), + Line::from(""), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::raw("Search by "), + Span::styled("name, username, URL, or tags", Style::default().fg(app.theme.green())), + ]), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::raw("Results update "), + Span::styled("as you type", Style::default().fg(app.theme.blue())), + ]), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::raw("Press "), + Span::styled("Enter", Style::default().fg(app.theme.purple())), + Span::raw(" to view detailed results"), + ]), + ]) + .alignment(Alignment::Left) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())) + ); + f.render_widget(tips, tips_area); } - let help = Paragraph::new("Type to search │ Enter: View results │ Esc: Back") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[3]); } pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { @@ -169,87 +243,148 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { } pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([ - Constraint::Length(3), - Constraint::Min(5), - Constraint::Length(3), - Constraint::Length(3), - ]) - .split(size); let block = Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.purple())) - .title("═══ FILTER BY TAG ═══") - .title_alignment(Alignment::Center) + .title(Span::styled( + " [ Filter by Tags ] ", + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + )) .style(Style::default().bg(app.theme.bg0())); f.render_widget(block, size); - let title = if let Some(ref tag) = app.active_tf { - format!("Filtering by: [{}] ({} entries)", tag, app.entry_disp.len()) - } else { - "Select a tag to filter".to_string() - }; - let title_widget = Paragraph::new(title) - .style(Style::default().fg(app.theme.yellow())) + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + Constraint::Length(3), // status or active filter + Constraint::Length(1), // spacer + Constraint::Min(5), // tags list + ]) + .split(size); + + if let Some(ref tag) = app.active_tf { + let status = Paragraph::new(vec![ + Line::from(vec![ + Span::styled("Active Filter: ", Style::default().fg(app.theme.gray())), + Span::styled(tag, Style::default().fg(app.theme.purple()).add_modifier(Modifier::BOLD)), + Span::styled(format!(" ({} entries)", app.entry_disp.len()), Style::default().fg(app.theme.aqua())), + ]), + ]) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + ) .alignment(Alignment::Center); - f.render_widget(title_widget, chunks[0]); - if app.all_tags.is_empty() { - let empty = Paragraph::new("[ No tags available - Add tags to your passwords first ]") + f.render_widget(status, chunks[0]); + } else { + let status = Paragraph::new("No active filter") .style(Style::default().fg(app.theme.gray())) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ) .alignment(Alignment::Center); - f.render_widget(empty, chunks[1]); + f.render_widget(status, chunks[0]); + } + + if app.all_tags.is_empty() { + let empty_area = centered_rect(60, 30, chunks[2]); + let empty = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled("No tags available", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(""), + Line::from(Span::styled("Add tags to your passwords first", Style::default().fg(app.theme.gray()))), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); + f.render_widget(empty, empty_area); } else { - let mut items = vec![ListItem::new(Line::from(vec![ - Span::styled( - if app.select_tf == 0 { "▶ " } else { " " }, - Style::default().fg(app.theme.yellow()), - ), - Span::styled( - format!( - "All ({} total)", - app.vault.as_ref().map_or(0, |v| v.e.len()) + let mut items = vec![ListItem::new(vec![ + Line::from(vec![ + Span::styled( + if app.select_tf == 0 { "▶ " } else { " " }, + Style::default().fg(app.theme.yellow()), ), - if app.select_tf == 0 { - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(app.theme.fg()) - }, - ), - ]))]; - for (idx, (tag, count)) in app.all_tags.iter().enumerate() { - let is_selected = idx + 1 == app.select_tf; - let prefix = if is_selected { "▶ " } else { " " }; - items.push(ListItem::new(Line::from(vec![ - Span::styled(prefix, Style::default().fg(app.theme.yellow())), Span::styled( - format!("[{tag}] ({count} entries)"), - if is_selected { + "[ALL]", + if app.select_tf == 0 { Style::default() - .fg(app.theme.orange()) + .fg(app.theme.green()) .add_modifier(Modifier::BOLD) } else { Style::default().fg(app.theme.fg()) }, ), - ]))); + Span::raw(" "), + Span::styled( + format!("({} total)", app.vault.as_ref().map_or(0, |v| v.e.len())), + Style::default().fg(app.theme.gray()), + ), + ]), + Line::from(""), + ])]; + + for (idx, (tag, count)) in app.all_tags.iter().enumerate() { + let is_selected = idx + 1 == app.select_tf; + let is_active = app.active_tf.as_ref().map_or(false, |t| t == tag); + + let prefix = if is_selected { "▶ " } else { " " }; + let tag_style = if is_selected { + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD) + } else if is_active { + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(app.theme.fg()) + }; + + items.push(ListItem::new(vec![ + Line::from(vec![ + Span::styled(prefix, Style::default().fg(app.theme.yellow())), + Span::styled(format!("[{}]", tag), tag_style), + Span::raw(" "), + Span::styled( + format!("({} entries)", count), + Style::default().fg(app.theme.gray()), + ), + if is_active { + Span::styled(" ✓", Style::default().fg(app.theme.green())) + } else { + Span::raw("") + }, + ]), + Line::from(""), + ])); } - let list = List::new(items).block(Block::default().borders(Borders::NONE)); - f.render_widget(list, chunks[1]); - } - if app.active_tf.is_some() { - let filter_info = Paragraph::new("Press V to view filtered passwords") - .style(Style::default().fg(app.theme.aqua())) - .alignment(Alignment::Center); - f.render_widget(filter_info, chunks[2]); + + let list = List::new(items).block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.purple())) + .title(Span::styled( + format!(" [ {} Tags Available ] ", app.all_tags.len()), + Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD), + )) + ); + f.render_widget(list, chunks[2]); } - let help = Paragraph::new("Enter: Apply │ V: View │ Esc: Back") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[3]); } #[allow(clippy::too_many_lines)] @@ -462,7 +597,7 @@ pub fn draw_options_menu(f: &mut Frame, area: Rect, app: &App) { .add_modifier(Modifier::BOLD), )]), Line::from(vec![Span::styled( - " v2.3.5", + " v2.3.4", Style::default().fg(app.theme.gray()), )]), ]; @@ -549,11 +684,7 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(3), - Constraint::Min(5), - Constraint::Length(3), - ]) + .constraints([Constraint::Length(3), Constraint::Min(5), Constraint::Length(3)]) .split(inner); let title = Paragraph::new("Preferences") @@ -566,16 +697,16 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { f.render_widget(title, chunks[0]); let current_theme = app.theme.name(); - let cltime_disp = if app.clipboard_timeout == 0 { + let clipboard_timeout_display = if app.clipboard_timeout == 0 { "Never".to_string() } else { format!("{}s", app.clipboard_timeout) }; - + let items = [ ("1", "Theme Selector", format!("Current: {current_theme}")), - ("2", "Auto-Save", "Coming soon".to_string()), - ("3", "Clipboard Timeout", format!("Current: {cltime_disp}")), + ("2", "Clipboard Timeout", format!("Current: {clipboard_timeout_display}")), + ("3", "Future Feature", "Coming soon".to_string()), ]; let list_items: Vec = items @@ -612,6 +743,12 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { let list = List::new(list_items); f.render_widget(list, chunks[1]); + + let help_text = "Press number or Enter to select | ↑↓: Navigate | Esc: Back"; + let help = Paragraph::new(help_text) + .style(Style::default().fg(app.theme.gray())) + .alignment(Alignment::Center); + f.render_widget(help, chunks[2]); } #[allow(clippy::too_many_lines)] @@ -772,4 +909,4 @@ pub fn draw_help_screen(f: &mut Frame, area: Rect, app: &App) { .style(Style::default().fg(app.theme.gray())) .alignment(Alignment::Center); f.render_widget(footer, chunks[1]); -} +} \ No newline at end of file From 173f236ef2f5b7a9ff4a147166f18bc6d599e05e Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 10:22:03 +0530 Subject: [PATCH 16/48] FIX: Fixed SearchPasswords screen by resetting search && FilterByTags screen by adding spacing --- src/ui/widgets/utility.rs | 88 ++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/src/ui/widgets/utility.rs b/src/ui/widgets/utility.rs index 1b59589..806e10b 100644 --- a/src/ui/widgets/utility.rs +++ b/src/ui/widgets/utility.rs @@ -76,7 +76,38 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Left); f.render_widget(search_widget, search_area); - if app.entry_disp.is_empty() && !app.search_query.is_empty() { + if app.search_query.is_empty() { + let tips_area = centered_rect(70, 30, chunks[2]); + let tips = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled("Quick Search Tips", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD))), + Line::from(""), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::raw("Search by "), + Span::styled("name, username, URL, or tags", Style::default().fg(app.theme.green())), + ]), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::raw("Results update "), + Span::styled("as you type", Style::default().fg(app.theme.blue())), + ]), + Line::from(vec![ + Span::styled("• ", Style::default().fg(app.theme.orange())), + Span::raw("Press "), + Span::styled("Enter", Style::default().fg(app.theme.purple())), + Span::raw(" to view detailed results"), + ]), + ]) + .alignment(Alignment::Left) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())) + ); + f.render_widget(tips, tips_area); + } else if app.entry_disp.is_empty() { let empty_area = centered_rect(60, 20, chunks[2]); let empty = Paragraph::new(vec![ Line::from(""), @@ -312,30 +343,33 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { ); f.render_widget(empty, empty_area); } else { - let mut items = vec![ListItem::new(vec![ - Line::from(vec![ - Span::styled( - if app.select_tf == 0 { "▶ " } else { " " }, - Style::default().fg(app.theme.yellow()), - ), - Span::styled( - "[ALL]", - if app.select_tf == 0 { - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(app.theme.fg()) - }, - ), - Span::raw(" "), - Span::styled( - format!("({} total)", app.vault.as_ref().map_or(0, |v| v.e.len())), - Style::default().fg(app.theme.gray()), - ), - ]), - Line::from(""), - ])]; + let mut items = vec![ + ListItem::new(vec![Line::from("")]), + ListItem::new(vec![ + Line::from(vec![ + Span::styled( + if app.select_tf == 0 { "▶ " } else { " " }, + Style::default().fg(app.theme.yellow()), + ), + Span::styled( + "[ALL]", + if app.select_tf == 0 { + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(app.theme.fg()) + }, + ), + Span::raw(" "), + Span::styled( + format!("({} total)", app.vault.as_ref().map_or(0, |v| v.e.len())), + Style::default().fg(app.theme.gray()), + ), + ]), + Line::from(""), + ]) + ]; for (idx, (tag, count)) in app.all_tags.iter().enumerate() { let is_selected = idx + 1 == app.select_tf; @@ -825,6 +859,10 @@ pub fn draw_help_screen(f: &mut Frame, area: Rect, app: &App) { Span::styled(" F ", Style::default().fg(app.theme.orange())), Span::raw("Clear filters"), ]), + Line::from(vec![ + Span::styled(" V ", Style::default().fg(app.theme.orange())), + Span::raw("View filtered passwords (when in Filter Tags screen)"), + ]), Line::from(vec![ Span::styled(" Esc ", Style::default().fg(app.theme.orange())), Span::raw("Back to main menu"), From 93d33ea1b47b8f0bfa27ed58630673b48485818c Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 10:43:05 +0530 Subject: [PATCH 17/48] UPDATE: Updated GenPassword, ThemeSelector and Settings screens --- src/ui/widgets/utility.rs | 125 ++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/src/ui/widgets/utility.rs b/src/ui/widgets/utility.rs index 806e10b..b0bee4c 100644 --- a/src/ui/widgets/utility.rs +++ b/src/ui/widgets/utility.rs @@ -217,24 +217,32 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(6), - Constraint::Min(1), - Constraint::Length(3), + Constraint::Length(3), // title + Constraint::Length(1), // spacer + Constraint::Length(3), // length input + Constraint::Min(5), // gen pwd area + Constraint::Length(1), // spacer ]) .split(area); + let block = Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.aqua())) - .title("═══ GENERATE PASSWORD ═══") - .title_alignment(Alignment::Center) + .title(Span::styled( + " [ Generate Password ] ", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )) .style(Style::default().bg(app.theme.bg0())); f.render_widget(block, area); + let title = Paragraph::new("Enter password length (4-64)") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); + let length_input = Paragraph::new(format!( "Length: {}", if app.input_buffer.is_empty() { @@ -247,8 +255,16 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD), - ); - f.render_widget(length_input, chunks[1]); + ) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ) + .alignment(Alignment::Center); + f.render_widget(length_input, chunks[2]); + if !app.gen_pwd.is_empty() { let generated = Paragraph::new(vec![ Line::from(""), @@ -264,13 +280,15 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { .add_modifier(Modifier::BOLD), )), ]) - .alignment(Alignment::Center); - f.render_widget(generated, chunks[2]); + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + ); + f.render_widget(generated, chunks[3]); } - let help = Paragraph::new("| Enter: Generate │ Esc: Back |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[4]); } pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { @@ -293,7 +311,7 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { .constraints([ Constraint::Length(3), // status or active filter Constraint::Length(1), // spacer - Constraint::Min(5), // tags list + Constraint::Min(5), // tags filter ]) .split(size); @@ -431,7 +449,6 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { .constraints([ Constraint::Length(3), Constraint::Min(10), - Constraint::Length(3), ]) .split(area); @@ -445,6 +462,7 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.aqua())), ); f.render_widget(title, chunks[0]); @@ -493,8 +511,12 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { let list = List::new(items).block( Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.blue())) - .title(" Themes "), + .title(Span::styled( + " Themes ", + Style::default().fg(app.theme.blue()).add_modifier(Modifier::BOLD), + )), ); f.render_widget(list, content_chunks[0]); @@ -512,30 +534,31 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { Line::from(""), Line::from(vec![Span::styled( "Colors:", - Style::default().fg(app.theme.gray()), + Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD), )]), + Line::from(""), Line::from(vec![ - Span::raw(" Red: "), + Span::raw(" Red "), Span::styled("███", Style::default().fg(preview_theme.red())), ]), Line::from(vec![ - Span::raw(" Green: "), + Span::raw(" Green "), Span::styled("███", Style::default().fg(preview_theme.green())), ]), Line::from(vec![ - Span::raw(" Yellow: "), + Span::raw(" Yellow "), Span::styled("███", Style::default().fg(preview_theme.yellow())), ]), Line::from(vec![ - Span::raw(" Blue: "), + Span::raw(" Blue "), Span::styled("███", Style::default().fg(preview_theme.blue())), ]), Line::from(vec![ - Span::raw(" Purple: "), + Span::raw(" Purple "), Span::styled("███", Style::default().fg(preview_theme.purple())), ]), Line::from(vec![ - Span::raw(" Aqua: "), + Span::raw(" Aqua "), Span::styled("███", Style::default().fg(preview_theme.aqua())), ]), ]; @@ -544,28 +567,15 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.purple())) - .title(" Preview "), + .title(Span::styled( + " Preview ", + Style::default().fg(app.theme.purple()).add_modifier(Modifier::BOLD), + )), ) .alignment(Alignment::Left); f.render_widget(preview, content_chunks[1]); - - let help_text = vec![ - Span::styled("Enter", Style::default().fg(app.theme.green())), - Span::raw(": Apply "), - Span::styled("Esc", Style::default().fg(app.theme.red())), - Span::raw(": Cancel"), - ]; - - let help = Paragraph::new(Line::from(help_text)) - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.gray())), - ); - f.render_widget(help, chunks[2]); } #[allow(clippy::too_many_lines)] @@ -707,9 +717,14 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { let block = Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.blue())) - .title("═══ SETTINGS ═══") - .title_alignment(Alignment::Center) + .title(Span::styled( + " [ Settings ] ", + Style::default() + .fg(app.theme.blue()) + .add_modifier(Modifier::BOLD), + )) .style(Style::default().bg(app.theme.bg0())); let inner = block.inner(centered_area); @@ -718,7 +733,10 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([Constraint::Length(3), Constraint::Min(5), Constraint::Length(3)]) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + ]) .split(inner); let title = Paragraph::new("Preferences") @@ -777,12 +795,6 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { let list = List::new(list_items); f.render_widget(list, chunks[1]); - - let help_text = "Press number or Enter to select | ↑↓: Navigate | Esc: Back"; - let help = Paragraph::new(help_text) - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[2]); } #[allow(clippy::too_many_lines)] @@ -791,9 +803,14 @@ pub fn draw_help_screen(f: &mut Frame, area: Rect, app: &App) { let block = Block::default() .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded) .border_style(Style::default().fg(app.theme.green())) - .title("═══ PASSLOCK HELP ═══") - .title_alignment(Alignment::Center) + .title(Span::styled( + " [ PassLock Help ] ", + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + )) .style(Style::default().bg(app.theme.bg0())); let inner = block.inner(area); From b5184d7fe9a0896cbe670cd62027ef7fe7c97c12 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 10:58:09 +0530 Subject: [PATCH 18/48] UPDATE: Updated ViewPasswords, AddPassword, EditPassword, ViewHistory, DeletePassword, ContextMenu --- src/ui/widgets/passwords.rs | 579 ++++++++++++++++++++++-------------- 1 file changed, 355 insertions(+), 224 deletions(-) diff --git a/src/ui/widgets/passwords.rs b/src/ui/widgets/passwords.rs index fd885bf..a4c3513 100644 --- a/src/ui/widgets/passwords.rs +++ b/src/ui/widgets/passwords.rs @@ -7,7 +7,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, + widgets::{Block, Borders, BorderType, List, ListItem, Paragraph, Wrap}, Frame, }; @@ -15,22 +15,27 @@ use ratatui::{ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { app.entry_row_map.clear(); + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + .title(Span::styled( + " [ Passwords ] ", + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, size); + let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) + .margin(2) .constraints([ - Constraint::Length(3), + Constraint::Length(4), Constraint::Min(5), - Constraint::Length(3), ]) .split(size); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.green())) - .title("═══ PASSWORDS ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block.clone(), size); let filter_status = if let Some(ref tag) = app.active_tf { format!(" (Filtered by: {tag})") @@ -40,10 +45,10 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { String::new() }; - let clipboard_status = if let Some(expires_at) = app.clipboard_countdown { + let cb_stats = if let Some(expires_at) = app.clipboard_countdown { let now = crate::get_timestamp(); if expires_at > now { - format!(" | Clipboard clears in {}s", expires_at - now) + format!(" │ Clipboard: {}s", expires_at - now) } else { String::new() } @@ -51,29 +56,53 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { String::new() }; - let title = Paragraph::new(format!( - "Total: {} entries{}{} | Right-click entry for options", - app.entry_disp.len(), - filter_status, - clipboard_status - )) - .style(Style::default().fg(app.theme.yellow())) - .alignment(Alignment::Center); + let title = Paragraph::new(vec![ + Line::from(vec![ + Span::styled( + format!("Total: {} entries", app.entry_disp.len()), + Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD), + ), + Span::styled(filter_status, Style::default().fg(app.theme.purple())), + Span::styled(cb_stats, Style::default().fg(app.theme.aqua())), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Right-click for options", Style::default().fg(app.theme.gray())), + ]), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); f.render_widget(title, chunks[0]); if app.entry_disp.is_empty() { - let empty = if app.active_tf.is_some() || !app.search_query.is_empty() { - "[ No matching entries found ]" + let empty_area = centered_rect(60, 20, chunks[1]); + let empty_msg = if app.active_tf.is_some() || !app.search_query.is_empty() { + "No matching entries found" } else { - "[ No passwords saved yet ]" + "No passwords saved yet" }; - let empty_paragraph = Paragraph::new(empty) - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(empty_paragraph, chunks[1]); + let empty = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled(empty_msg, Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(""), + Line::from(Span::styled("Press '2' to add your first password", Style::default().fg(app.theme.gray()))), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); + f.render_widget(empty, empty_area); } else { - let content_area_top = chunks[1].y; - let mut current_row = content_area_top; + let ca_top = chunks[1].y; + let mut current_row = ca_top; let mut entry_lines_vec: Vec> = Vec::new(); for (i, entry) in app.entry_disp.iter().enumerate() { @@ -86,8 +115,7 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Span::styled(prefix, Style::default().fg(app.theme.yellow())), Span::styled( format!("[{}] ", i + 1), - Style::default().fg(app.theme.orange()), - ), + Style::default().fg(app.theme.orange())), Span::styled( &entry.n, if is_selected { @@ -207,71 +235,84 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { let list = List::new(items).block(Block::default().borders(Borders::NONE)); f.render_widget(list, chunks[1]); } - let help = Paragraph::new("| F: Clear filter │ Esc: Back |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[2]); } #[allow(clippy::too_many_lines, clippy::cast_sign_loss)] pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { let area = centered_rect(80, 85, size); + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.green())) + .title(Span::styled( + " [ Add New Password ] ", + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, area); + let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) .constraints([ Constraint::Length(2), // 0: title Constraint::Length(1), // 1: spacer - Constraint::Length(2), // 2: name - Constraint::Length(2), // 3: username - Constraint::Length(2), // 4: pwd + Constraint::Length(3), // 2: name + Constraint::Length(3), // 3: username + Constraint::Length(3), // 4: pwd Constraint::Length(2), // 5: strength bar - Constraint::Length(2), // 6: strength feedbkac - Constraint::Length(2), // 7: url - Constraint::Length(2), // 8: totp - Constraint::Length(2), // 9: tags input + Constraint::Length(2), // 6: strength feedback + Constraint::Length(3), // 7: url + Constraint::Length(3), // 8: totp + Constraint::Length(3), // 9: tags input Constraint::Length(3), // 10: Tags display Constraint::Length(4), // 11: notes Constraint::Min(1), // 12: msg - Constraint::Length(2), // 13: help ]) .split(area); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.green())) - .title("═══ ADD NEW PASSWORD ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, area); + let title = Paragraph::new("Fill in the details below") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - let name_field = - Paragraph::new(format!("Name: {}", app.n_entry_name)).style(if app.add_fi == 0 { - active_style - } else { - inactive_style - }); + + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) + .style(if app.add_fi == 0 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(name_field, chunks[2]); - let user_field = - Paragraph::new(format!("Username: {}", app.n_entry_user)).style(if app.add_fi == 1 { - active_style - } else { - inactive_style - }); + + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) + .style(if app.add_fi == 1 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(user_field, chunks[3]); - let pass_field = - Paragraph::new(format!("Password: {}", app.n_entry_pass)).style(if app.add_fi == 2 { - active_style - } else { - inactive_style - }); + + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) + .style(if app.add_fi == 2 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(pass_field, chunks[4]); + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -290,10 +331,11 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { strength.percentage, strength.strength ); - let strength_display = Paragraph::new(bar) + let strenth_disp = Paragraph::new(bar) .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); - f.render_widget(strength_display, chunks[5]); + f.render_widget(strenth_disp, chunks[5]); + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -303,21 +345,25 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - let url_field = - Paragraph::new(format!("URL (optional): {}", app.n_entry_url)).style(if app.add_fi == 3 { - active_style - } else { - inactive_style - }); + + let url_field = Paragraph::new(format!("URL (optional): {}", app.n_entry_url)) + .style(if app.add_fi == 3 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(url_field, chunks[7]); - let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)).style( - if app.add_fi == 4 { - active_style - } else { - inactive_style - }, - ); + let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) + .style(if app.add_fi == 4 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(totp_field, chunks[8]); let tags_text = if app.add_fi == 5 { @@ -326,34 +372,41 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { - active_style - } else { - inactive_style - }) - .wrap(Wrap { trim: true }); + .style(if app.add_fi == 5 { active_style } else { inactive_style }) + .wrap(Wrap { trim: true }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(tags_input, chunks[9]); + if !app.n_entry_tags.is_empty() { - let tags_display = app + let tags_disp = app .n_entry_tags .iter() .enumerate() .map(|(i, tag)| format!("[{}]{} ", i + 1, tag)) .collect::>() .join(" "); - let tags_widget = Paragraph::new(format!("Added: {tags_display}")) + let tags_widget = Paragraph::new(format!("Added: {tags_disp}")) .style(Style::default().fg(app.theme.orange())) .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } + let notes = Paragraph::new(format!("Notes:\n{}", app.n_entry_notes)) - .style(if app.add_fi == 5 { - active_style - } else { - inactive_style - }) - .wrap(Wrap { trim: false }); + .style(if app.add_fi == 6 { active_style } else { inactive_style }) + .wrap(Wrap { trim: false }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(notes, chunks[11]); + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -366,72 +419,84 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Center); f.render_widget(msg, chunks[12]); } - let help = - Paragraph::new("| Tab: Next field │ Enter: Add tag/Save │ 1-9: Remove tag │ Esc: Cancel |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[13]); } #[allow(clippy::too_many_lines, clippy::cast_sign_loss)] pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { let area = centered_rect(80, 85, size); + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.orange())) + .title(Span::styled( + " [ Edit Password ] ", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, area); + let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) .constraints([ Constraint::Length(2), // 0: title Constraint::Length(1), // 1: spacer - Constraint::Length(2), // 2: name - Constraint::Length(2), // 3: username - Constraint::Length(2), // 4: pwd + Constraint::Length(3), // 2: name + Constraint::Length(3), // 3: username + Constraint::Length(3), // 4: pwd Constraint::Length(2), // 5: strength bar Constraint::Length(2), // 6: strength feedback - Constraint::Length(2), // 7: url - Constraint::Length(2), // 8: totp - Constraint::Length(2), // 9: tags input + Constraint::Length(3), // 7: url + Constraint::Length(3), // 8: totp + Constraint::Length(3), // 9: tags input Constraint::Length(3), // 10: tags display Constraint::Length(4), // 11: notes Constraint::Min(1), // 12: msg - Constraint::Length(2), // 13: help ]) .split(area); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.orange())) - .title("═══ EDIT PASSWORD ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, area); + let title = Paragraph::new("Edit entry details (password changes are tracked)") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - let name_field = - Paragraph::new(format!("Name: {}", app.n_entry_name)).style(if app.add_fi == 0 { - active_style - } else { - inactive_style - }); + + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) + .style(if app.add_fi == 0 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(name_field, chunks[2]); - let user_field = - Paragraph::new(format!("Username: {}", app.n_entry_user)).style(if app.add_fi == 1 { - active_style - } else { - inactive_style - }); + + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) + .style(if app.add_fi == 1 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(user_field, chunks[3]); - let pass_field = - Paragraph::new(format!("Password: {}", app.n_entry_pass)).style(if app.add_fi == 2 { - active_style - } else { - inactive_style - }); + + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) + .style(if app.add_fi == 2 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(pass_field, chunks[4]); + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -450,10 +515,11 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { strength.percentage, strength.strength ); - let strength_display = Paragraph::new(bar) + let strenth_disp = Paragraph::new(bar) .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); - f.render_widget(strength_display, chunks[5]); + f.render_widget(strenth_disp, chunks[5]); + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -463,20 +529,25 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - let url_field = Paragraph::new(format!("URL: {}", app.n_entry_url)).style(if app.add_fi == 3 { - active_style - } else { - inactive_style - }); + + let url_field = Paragraph::new(format!("URL: {}", app.n_entry_url)) + .style(if app.add_fi == 3 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(url_field, chunks[7]); - let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)).style( - if app.add_fi == 4 { - active_style - } else { - inactive_style - }, - ); + let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) + .style(if app.add_fi == 4 { active_style } else { inactive_style }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(totp_field, chunks[8]); let tags_text = if app.add_fi == 5 { @@ -485,34 +556,41 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { - active_style - } else { - inactive_style - }) - .wrap(Wrap { trim: true }); + .style(if app.add_fi == 5 { active_style } else { inactive_style }) + .wrap(Wrap { trim: true }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(tags_input, chunks[9]); + if !app.n_entry_tags.is_empty() { - let tags_display = app + let tags_disp = app .n_entry_tags .iter() .enumerate() .map(|(i, tag)| format!("[{}]{} ", i + 1, tag)) .collect::>() .join(" "); - let tags_widget = Paragraph::new(format!("Tags: {tags_display}")) + let tags_widget = Paragraph::new(format!("Tags: {tags_disp}")) .style(Style::default().fg(app.theme.orange())) .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } + let notes = Paragraph::new(format!("Notes:\n{}", app.n_entry_notes)) - .style(if app.add_fi == 6 { - active_style - } else { - inactive_style - }) - .wrap(Wrap { trim: false }); + .style(if app.add_fi == 6 { active_style } else { inactive_style }) + .wrap(Wrap { trim: false }) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) + ); f.render_widget(notes, chunks[11]); + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -525,44 +603,70 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Center); f.render_widget(msg, chunks[12]); } - let help = - Paragraph::new("| Tab: Next │ Enter: Add tag/Save │ 1-9: Remove tag │ Esc: Cancel |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[13]); } pub fn draw_history(f: &mut Frame, size: Rect, app: &App) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.purple())) + .title(Span::styled( + " [ Password History ] ", + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, size); + let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) + .margin(2) .constraints([ Constraint::Length(3), Constraint::Min(5), - Constraint::Length(3), ]) .split(size); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.purple())) - .title("═══ PASSWORD HISTORY ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, size); + if let Some(ref vault) = app.vault { if app.selected_entry < app.entry_disp.len() { let entry = &app.entry_disp[app.selected_entry]; if let Some(vault_entry) = vault.e.iter().find(|e| e.id == entry.id) { - let title = - Paragraph::new(format!("History for: {} (Last 5 changes)", vault_entry.n)) - .style(Style::default().fg(app.theme.yellow())) - .alignment(Alignment::Center); + let title = Paragraph::new(vec![ + Line::from(vec![ + Span::styled( + format!("History for: {}", vault_entry.n), + Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) + ), + ]), + Line::from(""), + Line::from(vec![ + Span::styled("Last 5 changes", Style::default().fg(app.theme.gray())), + ]), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); f.render_widget(title, chunks[0]); + if vault_entry.history.is_empty() { - let empty = Paragraph::new("[ No password changes recorded ]") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(empty, chunks[1]); + let empty_area = centered_rect(60, 20, chunks[1]); + let empty = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled("No password changes recorded", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); + f.render_widget(empty, empty_area); } else { let items: Vec = vault_entry .history @@ -600,72 +704,98 @@ pub fn draw_history(f: &mut Frame, size: Rect, app: &App) { } } } - let help = Paragraph::new("| Esc: Back |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[2]); } pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.red())) + .title(Span::styled( + " [ Delete Password ] ", + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, size); + let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) + .margin(2) .constraints([ Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), - Constraint::Length(3), ]) .split(size); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.red())) - .title("═══ DELETE PASSWORD ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, size); - let title = Paragraph::new("⚠ Enter the number of the entry to delete") + + let title = Paragraph::new("Enter the number of the entry to delete") .style(Style::default().fg(app.theme.orange())) - .alignment(Alignment::Center); + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.orange())) + ); f.render_widget(title, chunks[0]); + let empty_vec = Vec::new(); - let entries_to_display = if app.entry_disp.is_empty() { + let entry_disp = if app.entry_disp.is_empty() { app.vault.as_ref().map_or(&empty_vec, |v| &v.e) } else { &app.entry_disp }; - if entries_to_display.is_empty() { - let empty = Paragraph::new("[ No passwords to delete ]") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(empty, chunks[1]); + + if entry_disp.is_empty() { + let empty_area = centered_rect(60, 20, chunks[1]); + let empty = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled("No passwords to delete", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.gray())) + ); + f.render_widget(empty, empty_area); } else { - let items: Vec = entries_to_display + let items: Vec = entry_disp .iter() .enumerate() .map(|(i, entry)| { - ListItem::new(Line::from(vec![ - Span::styled( - format!("[{}] ", i + 1), - Style::default().fg(app.theme.red()), - ), - Span::styled(&entry.n, Style::default().fg(app.theme.fg())), - ])) + ListItem::new(vec![ + Line::from(vec![ + Span::styled( + format!("[{}] ", i + 1), + Style::default().fg(app.theme.red()), + ), + Span::styled(&entry.n, Style::default().fg(app.theme.fg())), + ]), + Line::from(""), + ]) }) .collect(); let list = List::new(items).block(Block::default().borders(Borders::NONE)); f.render_widget(list, chunks[1]); } - let input = Paragraph::new(format!("Entry number: {}", app.input_buffer)).style( - Style::default() - .fg(app.theme.red()) - .add_modifier(Modifier::BOLD), - ); + + let input = Paragraph::new(format!("Entry number: {}", app.input_buffer)) + .style( + Style::default() + .fg(app.theme.red()) + .add_modifier(Modifier::BOLD), + ) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.red())) + ); f.render_widget(input, chunks[2]); - let help = Paragraph::new("Type number │ Enter: Delete │ Esc: Cancel") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[3]); } pub fn draw_context_menu(f: &mut Frame, app: &App) { @@ -728,10 +858,11 @@ pub fn draw_context_menu(f: &mut Frame, app: &App) { let block = Block::default() .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(Style::default().fg(orange)) .style(Style::default().bg(bg1)); let paragraph = Paragraph::new(menu_lines).block(block); f.render_widget(paragraph, area); -} +} \ No newline at end of file From b054fbd9af31599c8bb4368283a3f4934475cf70 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 11:07:40 +0530 Subject: [PATCH 19/48] FIX: Fixed Clipboard timeout sequence number --- src/ui/handlers.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ui/handlers.rs b/src/ui/handlers.rs index 518a685..b66d3ca 100644 --- a/src/ui/handlers.rs +++ b/src/ui/handlers.rs @@ -591,10 +591,6 @@ pub fn handle_settings_screen(app: &mut App, key: KeyCode) { .unwrap_or(0); } KeyCode::Char('2') => { - // auto save toggle (future feature) - app.set_msg("Auto-save coming soon!", MessageType::Info); - } - KeyCode::Char('3') => { app.clipboard_timeout = match app.clipboard_timeout { 10 => 30, 30 => 60, @@ -611,6 +607,10 @@ pub fn handle_settings_screen(app: &mut App, key: KeyCode) { }; app.set_msg(&msg, MessageType::Success); } + KeyCode::Char('3') => { + // auto save toggle (future feature) + app.set_msg("Auto-save coming soon!", MessageType::Info); + } KeyCode::Enter => match app.settings_menu_index { 0 => { use super::colors::Theme; @@ -629,9 +629,6 @@ pub fn handle_settings_screen(app: &mut App, key: KeyCode) { }; } 1 => { - app.set_msg("Auto-save coming soon!", MessageType::Info); - } - 2 => { app.clipboard_timeout = match app.clipboard_timeout { 10 => 30, 30 => 60, @@ -648,6 +645,9 @@ pub fn handle_settings_screen(app: &mut App, key: KeyCode) { }; app.set_msg(&msg, MessageType::Success); } + 2 => { + app.set_msg("Auto-save coming soon!", MessageType::Info); + } _ => {} }, KeyCode::Esc => { From c4659d43dc9a3a9770abeefaf29f750b267abe45 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 11:09:15 +0530 Subject: [PATCH 20/48] FIX: cargo fmt and cargo clippy --- src/ui/widgets/passwords.rs | 311 +++++++++++++++++++++++++----------- src/ui/widgets/utility.rs | 191 ++++++++++++++-------- 2 files changed, 342 insertions(+), 160 deletions(-) diff --git a/src/ui/widgets/passwords.rs b/src/ui/widgets/passwords.rs index a4c3513..40cd18f 100644 --- a/src/ui/widgets/passwords.rs +++ b/src/ui/widgets/passwords.rs @@ -7,7 +7,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, BorderType, List, ListItem, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap}, Frame, }; @@ -31,10 +31,7 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(4), - Constraint::Min(5), - ]) + .constraints([Constraint::Length(4), Constraint::Min(5)]) .split(size); let filter_status = if let Some(ref tag) = app.active_tf { @@ -60,22 +57,25 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Line::from(vec![ Span::styled( format!("Total: {} entries", app.entry_disp.len()), - Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), ), Span::styled(filter_status, Style::default().fg(app.theme.purple())), Span::styled(cb_stats, Style::default().fg(app.theme.aqua())), ]), Line::from(""), - Line::from(vec![ - Span::styled("Right-click for options", Style::default().fg(app.theme.gray())), - ]), + Line::from(vec![Span::styled( + "Right-click for options", + Style::default().fg(app.theme.gray()), + )]), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(title, chunks[0]); @@ -88,16 +88,24 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { }; let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled(empty_msg, Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + empty_msg, + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), Line::from(""), - Line::from(Span::styled("Press '2' to add your first password", Style::default().fg(app.theme.gray()))), + Line::from(Span::styled( + "Press '2' to add your first password", + Style::default().fg(app.theme.gray()), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -115,7 +123,8 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Span::styled(prefix, Style::default().fg(app.theme.yellow())), Span::styled( format!("[{}] ", i + 1), - Style::default().fg(app.theme.orange())), + Style::default().fg(app.theme.orange()), + ), Span::styled( &entry.n, if is_selected { @@ -272,47 +281,71 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Min(1), // 12: msg ]) .split(area); - + let title = Paragraph::new("Fill in the details below") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) - .style(if app.add_fi == 0 { active_style } else { inactive_style }) + .style(if app.add_fi == 0 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 0 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(name_field, chunks[2]); - + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) - .style(if app.add_fi == 1 { active_style } else { inactive_style }) + .style(if app.add_fi == 1 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 1 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(user_field, chunks[3]); - + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) - .style(if app.add_fi == 2 { active_style } else { inactive_style }) + .style(if app.add_fi == 2 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 2 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(pass_field, chunks[4]); - + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -335,7 +368,7 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); f.render_widget(strenth_disp, chunks[5]); - + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -345,24 +378,40 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - + let url_field = Paragraph::new(format!("URL (optional): {}", app.n_entry_url)) - .style(if app.add_fi == 3 { active_style } else { inactive_style }) + .style(if app.add_fi == 3 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 3 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(url_field, chunks[7]); let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) - .style(if app.add_fi == 4 { active_style } else { inactive_style }) + .style(if app.add_fi == 4 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 4 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(totp_field, chunks[8]); @@ -372,16 +421,24 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { active_style } else { inactive_style }) + .style(if app.add_fi == 5 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 5 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(tags_input, chunks[9]); - + if !app.n_entry_tags.is_empty() { let tags_disp = app .n_entry_tags @@ -395,18 +452,26 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } - + let notes = Paragraph::new(format!("Notes:\n{}", app.n_entry_notes)) - .style(if app.add_fi == 6 { active_style } else { inactive_style }) + .style(if app.add_fi == 6 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 6 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(notes, chunks[11]); - + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -456,47 +521,71 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Min(1), // 12: msg ]) .split(area); - + let title = Paragraph::new("Edit entry details (password changes are tracked)") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) - .style(if app.add_fi == 0 { active_style } else { inactive_style }) + .style(if app.add_fi == 0 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 0 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(name_field, chunks[2]); - + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) - .style(if app.add_fi == 1 { active_style } else { inactive_style }) + .style(if app.add_fi == 1 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 1 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(user_field, chunks[3]); - + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) - .style(if app.add_fi == 2 { active_style } else { inactive_style }) + .style(if app.add_fi == 2 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 2 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(pass_field, chunks[4]); - + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -519,7 +608,7 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); f.render_widget(strenth_disp, chunks[5]); - + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -529,24 +618,40 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - + let url_field = Paragraph::new(format!("URL: {}", app.n_entry_url)) - .style(if app.add_fi == 3 { active_style } else { inactive_style }) + .style(if app.add_fi == 3 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 3 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(url_field, chunks[7]); let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) - .style(if app.add_fi == 4 { active_style } else { inactive_style }) + .style(if app.add_fi == 4 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 4 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(totp_field, chunks[8]); @@ -556,16 +661,24 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { active_style } else { inactive_style }) + .style(if app.add_fi == 5 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 5 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(tags_input, chunks[9]); - + if !app.n_entry_tags.is_empty() { let tags_disp = app .n_entry_tags @@ -579,18 +692,26 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } - + let notes = Paragraph::new(format!("Notes:\n{}", app.n_entry_notes)) - .style(if app.add_fi == 6 { active_style } else { inactive_style }) + .style(if app.add_fi == 6 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 6 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(notes, chunks[11]); - + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -622,49 +743,52 @@ pub fn draw_history(f: &mut Frame, size: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(3), - Constraint::Min(5), - ]) + .constraints([Constraint::Length(3), Constraint::Min(5)]) .split(size); - + if let Some(ref vault) = app.vault { if app.selected_entry < app.entry_disp.len() { let entry = &app.entry_disp[app.selected_entry]; if let Some(vault_entry) = vault.e.iter().find(|e| e.id == entry.id) { let title = Paragraph::new(vec![ - Line::from(vec![ - Span::styled( - format!("History for: {}", vault_entry.n), - Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) - ), - ]), + Line::from(vec![Span::styled( + format!("History for: {}", vault_entry.n), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), - Line::from(vec![ - Span::styled("Last 5 changes", Style::default().fg(app.theme.gray())), - ]), + Line::from(vec![Span::styled( + "Last 5 changes", + Style::default().fg(app.theme.gray()), + )]), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(title, chunks[0]); - + if vault_entry.history.is_empty() { let empty_area = centered_rect(60, 20, chunks[1]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("No password changes recorded", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "No password changes recorded", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -729,7 +853,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), ]) .split(size); - + let title = Paragraph::new("Enter the number of the entry to delete") .style(Style::default().fg(app.theme.orange())) .alignment(Alignment::Center) @@ -737,29 +861,34 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.orange())) + .border_style(Style::default().fg(app.theme.orange())), ); f.render_widget(title, chunks[0]); - + let empty_vec = Vec::new(); let entry_disp = if app.entry_disp.is_empty() { app.vault.as_ref().map_or(&empty_vec, |v| &v.e) } else { &app.entry_disp }; - + if entry_disp.is_empty() { let empty_area = centered_rect(60, 20, chunks[1]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("No passwords to delete", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "No passwords to delete", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -782,7 +911,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { let list = List::new(items).block(Block::default().borders(Borders::NONE)); f.render_widget(list, chunks[1]); } - + let input = Paragraph::new(format!("Entry number: {}", app.input_buffer)) .style( Style::default() @@ -793,7 +922,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.red())) + .border_style(Style::default().fg(app.theme.red())), ); f.render_widget(input, chunks[2]); } @@ -865,4 +994,4 @@ pub fn draw_context_menu(f: &mut Frame, app: &App) { let paragraph = Paragraph::new(menu_lines).block(block); f.render_widget(paragraph, area); -} \ No newline at end of file +} diff --git a/src/ui/widgets/utility.rs b/src/ui/widgets/utility.rs index b0bee4c..e2f91e4 100644 --- a/src/ui/widgets/utility.rs +++ b/src/ui/widgets/utility.rs @@ -27,6 +27,7 @@ pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .split(popup_layout[1])[1] } +#[allow(clippy::too_many_lines)] pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { let block = Block::default() .borders(Borders::ALL) @@ -45,9 +46,9 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(3), // search bar - Constraint::Length(1), // spacer - Constraint::Min(5), // results + Constraint::Length(3), // search bar + Constraint::Length(1), // spacer + Constraint::Min(5), // results ]) .split(size); @@ -64,13 +65,18 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { " Search ", Style::default().fg(app.theme.yellow()), )); - + let search_text = if app.search_query.is_empty() { Span::styled("Type to search...", Style::default().fg(app.theme.gray())) } else { - Span::styled(&app.search_query, Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD)) + Span::styled( + &app.search_query, + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + ) }; - + let search_widget = Paragraph::new(search_text) .block(search_block) .alignment(Alignment::Left); @@ -80,12 +86,20 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { let tips_area = centered_rect(70, 30, chunks[2]); let tips = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("Quick Search Tips", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "Quick Search Tips", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )), Line::from(""), Line::from(vec![ Span::styled("• ", Style::default().fg(app.theme.orange())), Span::raw("Search by "), - Span::styled("name, username, URL, or tags", Style::default().fg(app.theme.green())), + Span::styled( + "name, username, URL, or tags", + Style::default().fg(app.theme.green()), + ), ]), Line::from(vec![ Span::styled("• ", Style::default().fg(app.theme.orange())), @@ -104,23 +118,31 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())) + .border_style(Style::default().fg(app.theme.aqua())), ); f.render_widget(tips, tips_area); } else if app.entry_disp.is_empty() { let empty_area = centered_rect(60, 20, chunks[2]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("No matches found", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "No matches found", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), Line::from(""), - Line::from(Span::styled("Try a different search term", Style::default().fg(app.theme.gray()))), + Line::from(Span::styled( + "Try a different search term", + Style::default().fg(app.theme.gray()), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else if !app.entry_disp.is_empty() { @@ -143,14 +165,14 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { Span::styled(&entry.u, Style::default().fg(app.theme.blue())), ]), ]; - + if let Some(ref url) = entry.url { lines.push(Line::from(vec![ Span::styled(" URL: ", Style::default().fg(app.theme.gray())), Span::styled(url, Style::default().fg(app.theme.aqua())), ])); } - + if !entry.tags.is_empty() { lines.push(Line::from(vec![ Span::styled(" Tags: ", Style::default().fg(app.theme.gray())), @@ -160,12 +182,12 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { ), ])); } - + lines.push(Line::from("")); ListItem::new(lines) }) .collect(); - + let list = List::new(items).block( Block::default() .borders(Borders::ALL) @@ -173,20 +195,30 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { .border_style(Style::default().fg(app.theme.green())) .title(Span::styled( format!(" [ {} Results ] ", app.entry_disp.len()), - Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), - )) + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + )), ); f.render_widget(list, chunks[2]); } else { let tips_area = centered_rect(70, 30, chunks[2]); let tips = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("Quick Search Tips", Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "Quick Search Tips", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )), Line::from(""), Line::from(vec![ Span::styled("• ", Style::default().fg(app.theme.orange())), Span::raw("Search by "), - Span::styled("name, username, URL, or tags", Style::default().fg(app.theme.green())), + Span::styled( + "name, username, URL, or tags", + Style::default().fg(app.theme.green()), + ), ]), Line::from(vec![ Span::styled("• ", Style::default().fg(app.theme.orange())), @@ -205,7 +237,7 @@ pub fn draw_search_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())) + .border_style(Style::default().fg(app.theme.aqua())), ); f.render_widget(tips, tips_area); } @@ -217,14 +249,14 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(3), // title - Constraint::Length(1), // spacer - Constraint::Length(3), // length input - Constraint::Min(5), // gen pwd area - Constraint::Length(1), // spacer + Constraint::Length(3), // title + Constraint::Length(1), // spacer + Constraint::Length(3), // length input + Constraint::Min(5), // gen pwd area + Constraint::Length(1), // spacer ]) .split(area); - + let block = Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) @@ -237,12 +269,12 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { )) .style(Style::default().bg(app.theme.bg0())); f.render_widget(block, area); - + let title = Paragraph::new("Enter password length (4-64)") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let length_input = Paragraph::new(format!( "Length: {}", if app.input_buffer.is_empty() { @@ -260,11 +292,11 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ) .alignment(Alignment::Center); f.render_widget(length_input, chunks[2]); - + if !app.gen_pwd.is_empty() { let generated = Paragraph::new(vec![ Line::from(""), @@ -285,12 +317,13 @@ pub fn draw_gen_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.green())) + .border_style(Style::default().fg(app.theme.green())), ); f.render_widget(generated, chunks[3]); } } +#[allow(clippy::too_many_lines)] pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { let block = Block::default() .borders(Borders::ALL) @@ -309,25 +342,31 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { .direction(Direction::Vertical) .margin(2) .constraints([ - Constraint::Length(3), // status or active filter - Constraint::Length(1), // spacer - Constraint::Min(5), // tags filter + Constraint::Length(3), // status or active filter + Constraint::Length(1), // spacer + Constraint::Min(5), // tags filter ]) .split(size); if let Some(ref tag) = app.active_tf { - let status = Paragraph::new(vec![ - Line::from(vec![ - Span::styled("Active Filter: ", Style::default().fg(app.theme.gray())), - Span::styled(tag, Style::default().fg(app.theme.purple()).add_modifier(Modifier::BOLD)), - Span::styled(format!(" ({} entries)", app.entry_disp.len()), Style::default().fg(app.theme.aqua())), - ]), - ]) + let status = Paragraph::new(vec![Line::from(vec![ + Span::styled("Active Filter: ", Style::default().fg(app.theme.gray())), + Span::styled( + tag, + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!(" ({} entries)", app.entry_disp.len()), + Style::default().fg(app.theme.aqua()), + ), + ])]) .block( Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.green())) + .border_style(Style::default().fg(app.theme.green())), ) .alignment(Alignment::Center); f.render_widget(status, chunks[0]); @@ -338,7 +377,7 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ) .alignment(Alignment::Center); f.render_widget(status, chunks[0]); @@ -348,16 +387,24 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { let empty_area = centered_rect(60, 30, chunks[2]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("No tags available", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "No tags available", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), Line::from(""), - Line::from(Span::styled("Add tags to your passwords first", Style::default().fg(app.theme.gray()))), + Line::from(Span::styled( + "Add tags to your passwords first", + Style::default().fg(app.theme.gray()), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -386,13 +433,13 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { ), ]), Line::from(""), - ]) + ]), ]; for (idx, (tag, count)) in app.all_tags.iter().enumerate() { let is_selected = idx + 1 == app.select_tf; - let is_active = app.active_tf.as_ref().map_or(false, |t| t == tag); - + let is_active = app.active_tf.as_ref() == Some(tag); + let prefix = if is_selected { "▶ " } else { " " }; let tag_style = if is_selected { Style::default() @@ -405,14 +452,14 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { } else { Style::default().fg(app.theme.fg()) }; - + items.push(ListItem::new(vec![ Line::from(vec![ Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled(format!("[{}]", tag), tag_style), + Span::styled(format!("[{tag}]"), tag_style), Span::raw(" "), Span::styled( - format!("({} entries)", count), + format!("({count} entries)"), Style::default().fg(app.theme.gray()), ), if is_active { @@ -432,8 +479,10 @@ pub fn draw_filter_tags(f: &mut Frame, size: Rect, app: &App) { .border_style(Style::default().fg(app.theme.purple())) .title(Span::styled( format!(" [ {} Tags Available ] ", app.all_tags.len()), - Style::default().fg(app.theme.aqua()).add_modifier(Modifier::BOLD), - )) + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )), ); f.render_widget(list, chunks[2]); } @@ -446,10 +495,7 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(3), - Constraint::Min(10), - ]) + .constraints([Constraint::Length(3), Constraint::Min(10)]) .split(area); let title = Paragraph::new("THEME SELECTOR") @@ -515,7 +561,9 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { .border_style(Style::default().fg(app.theme.blue())) .title(Span::styled( " Themes ", - Style::default().fg(app.theme.blue()).add_modifier(Modifier::BOLD), + Style::default() + .fg(app.theme.blue()) + .add_modifier(Modifier::BOLD), )), ); f.render_widget(list, content_chunks[0]); @@ -534,7 +582,9 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { Line::from(""), Line::from(vec![Span::styled( "Colors:", - Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD), + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), )]), Line::from(""), Line::from(vec![ @@ -571,7 +621,9 @@ pub fn draw_theme_selector(f: &mut Frame, area: Rect, app: &App) { .border_style(Style::default().fg(app.theme.purple())) .title(Span::styled( " Preview ", - Style::default().fg(app.theme.purple()).add_modifier(Modifier::BOLD), + Style::default() + .fg(app.theme.purple()) + .add_modifier(Modifier::BOLD), )), ) .alignment(Alignment::Left); @@ -733,10 +785,7 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(3), - Constraint::Min(5), - ]) + .constraints([Constraint::Length(3), Constraint::Min(5)]) .split(inner); let title = Paragraph::new("Preferences") @@ -754,10 +803,14 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { } else { format!("{}s", app.clipboard_timeout) }; - + let items = [ ("1", "Theme Selector", format!("Current: {current_theme}")), - ("2", "Clipboard Timeout", format!("Current: {clipboard_timeout_display}")), + ( + "2", + "Clipboard Timeout", + format!("Current: {clipboard_timeout_display}"), + ), ("3", "Future Feature", "Coming soon".to_string()), ]; @@ -964,4 +1017,4 @@ pub fn draw_help_screen(f: &mut Frame, area: Rect, app: &App) { .style(Style::default().fg(app.theme.gray())) .alignment(Alignment::Center); f.render_widget(footer, chunks[1]); -} \ No newline at end of file +} From e69307132b3b8a871d763aeaee4be7de95079c9c Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 11:20:02 +0530 Subject: [PATCH 21/48] FIX: Fixed Auto-Save text field --- src/ui/widgets/utility.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/widgets/utility.rs b/src/ui/widgets/utility.rs index e2f91e4..74bb86a 100644 --- a/src/ui/widgets/utility.rs +++ b/src/ui/widgets/utility.rs @@ -811,7 +811,7 @@ pub fn draw_settings_screen(f: &mut Frame, area: Rect, app: &App) { "Clipboard Timeout", format!("Current: {clipboard_timeout_display}"), ), - ("3", "Future Feature", "Coming soon".to_string()), + ("3", "Auto-Save", "Coming soon".to_string()), ]; let list_items: Vec = items From 779d1dd3ea8ba83eee18c00f583a91902ad63c2e Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 15:25:54 +0530 Subject: [PATCH 22/48] UPDATE: Added mouse compatibility to Main Menu --- src/ui/app.rs | 2 ++ src/ui/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ src/ui/widgets/menu.rs | 29 +++++++++++++++++++-- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 6066e36..5a593c2 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -50,6 +50,7 @@ pub struct App { pub n_entry_totp: String, pub show_totp_codes: bool, pub clipboard_timeout: u64, + pub menu_click_map: Vec<(u16, u16, u16, u16, usize)>, } impl App { @@ -98,6 +99,7 @@ impl App { n_entry_totp: String::new(), show_totp_codes: true, clipboard_timeout: 30, + menu_click_map: Vec::new(), } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2d96307..886bc88 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -208,6 +208,65 @@ fn run_app( MouseEventKind::Down(MouseButton::Left) => { if app.context_menu_visible { handle_context_menu_click(app, mouse_event.column, mouse_event.row); + } else if app.screen == Screen::MainMenu { + // ═══ HANDLE MAIN MENU CLICKS ═══ + let x = mouse_event.column; + let y = mouse_event.row; + + for (y_start, y_end, x_start, x_end, menu_idx) in &app.menu_click_map { + if y >= *y_start && y <= *y_end && x >= *x_start && x < *x_end { + // Set selected menu item + app.selected_menu = *menu_idx; + app.msg.clear(); + + // Trigger the menu action (same as pressing Enter) + match *menu_idx { + 0 => { + app.screen = Screen::ViewPasswords; + app.active_tf = None; + app.search_query.clear(); + if let Some(ref vault) = app.vault { + app.entry_disp = vault.e.clone(); + } + } + 1 => { + app.screen = Screen::AddPassword; + app.ca_form(); + } + 2 => { + app.screen = Screen::SearchPassword; + app.search_query.clear(); + app.entry_disp.clear(); + } + 3 => { + app.screen = Screen::FilterByTag; + app.select_tf = 0; + app.filter_bt(None); + } + 4 => { + app.screen = Screen::GeneratePassword; + app.input_buffer = String::from("16"); + app.gen_pwd.clear(); + } + 5 => { + app.screen = Screen::DeletePassword; + app.input_buffer.clear(); + if app.entry_disp.is_empty() { + if let Some(ref vault) = app.vault { + app.entry_disp = vault.e.clone(); + } + } + } + 6 => { + app.should_quit = true; + } + _ => {} + } + break; + } + } + + app.context_menu_visible = false; } else { app.context_menu_visible = false; } diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 43521be..974f2b2 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -11,7 +11,7 @@ use ratatui::{ }; #[allow(clippy::too_many_lines)] -pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { +pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { let block = Block::default() .borders(Borders::NONE) .style(Style::default().bg(app.theme.bg0())); @@ -250,8 +250,33 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &App) { .add_modifier(Modifier::BOLD), )); + let menu_area = left_layout[2]; + + // Mouse click execution + let menu_inner_area = menu_block.inner(menu_area); + let menu = List::new(menu_list).block(menu_block); - f.render_widget(menu, left_layout[2]); + f.render_widget(menu, menu_area); + + let mut current_y = menu_inner_area.y + 1; + + app.menu_click_map.clear(); + + for i in 0..all_items.len() { + let item_height = 4u16; + let clickable_y_start = current_y; + let clickable_y_end = current_y + 1; + + app.menu_click_map.push(( + clickable_y_start, + clickable_y_end, + menu_inner_area.x, + menu_inner_area.x + menu_inner_area.width, + i, + )); + + current_y += item_height; + } let right_layout = Layout::default() .direction(Direction::Vertical) From 0d7590d5b834e632c0ece16bb3049dcad979f8dc Mon Sep 17 00:00:00 2001 From: hachimamma Date: Thu, 26 Feb 2026 15:42:26 +0530 Subject: [PATCH 23/48] EDIT: Edited src/ui/mod.rs --- src/ui/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 886bc88..f9328d9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -209,17 +209,14 @@ fn run_app( if app.context_menu_visible { handle_context_menu_click(app, mouse_event.column, mouse_event.row); } else if app.screen == Screen::MainMenu { - // ═══ HANDLE MAIN MENU CLICKS ═══ let x = mouse_event.column; let y = mouse_event.row; for (y_start, y_end, x_start, x_end, menu_idx) in &app.menu_click_map { if y >= *y_start && y <= *y_end && x >= *x_start && x < *x_end { - // Set selected menu item app.selected_menu = *menu_idx; app.msg.clear(); - // Trigger the menu action (same as pressing Enter) match *menu_idx { 0 => { app.screen = Screen::ViewPasswords; From c6eb40d6bae647d17e7816b0b1459ef66c88db73 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Fri, 27 Feb 2026 16:16:33 +0530 Subject: [PATCH 24/48] FIX: Added another line to notes section for UX --- src/ui/widgets/passwords.rs | 375 +++++++++++++----------------------- 1 file changed, 133 insertions(+), 242 deletions(-) diff --git a/src/ui/widgets/passwords.rs b/src/ui/widgets/passwords.rs index 40cd18f..1fc61a8 100644 --- a/src/ui/widgets/passwords.rs +++ b/src/ui/widgets/passwords.rs @@ -7,7 +7,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap}, + widgets::{Block, Borders, BorderType, List, ListItem, Paragraph, Wrap}, Frame, }; @@ -31,7 +31,10 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([Constraint::Length(4), Constraint::Min(5)]) + .constraints([ + Constraint::Length(4), + Constraint::Min(5), + ]) .split(size); let filter_status = if let Some(ref tag) = app.active_tf { @@ -42,7 +45,7 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { String::new() }; - let cb_stats = if let Some(expires_at) = app.clipboard_countdown { + let clipboard_status = if let Some(expires_at) = app.clipboard_countdown { let now = crate::get_timestamp(); if expires_at > now { format!(" │ Clipboard: {}s", expires_at - now) @@ -57,25 +60,22 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Line::from(vec![ Span::styled( format!("Total: {} entries", app.entry_disp.len()), - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), + Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD), ), Span::styled(filter_status, Style::default().fg(app.theme.purple())), - Span::styled(cb_stats, Style::default().fg(app.theme.aqua())), + Span::styled(clipboard_status, Style::default().fg(app.theme.aqua())), ]), Line::from(""), - Line::from(vec![Span::styled( - "Right-click for options", - Style::default().fg(app.theme.gray()), - )]), + Line::from(vec![ + Span::styled("Right-click for options", Style::default().fg(app.theme.gray())), + ]), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())), + .border_style(Style::default().fg(app.theme.gray())) ); f.render_widget(title, chunks[0]); @@ -88,29 +88,21 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { }; let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled( - empty_msg, - Style::default() - .fg(app.theme.gray()) - .add_modifier(Modifier::BOLD), - )), + Line::from(Span::styled(empty_msg, Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), Line::from(""), - Line::from(Span::styled( - "Press '2' to add your first password", - Style::default().fg(app.theme.gray()), - )), + Line::from(Span::styled("Press '2' to add your first password", Style::default().fg(app.theme.gray()))), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())), + .border_style(Style::default().fg(app.theme.gray())) ); f.render_widget(empty, empty_area); } else { - let ca_top = chunks[1].y; - let mut current_row = ca_top; + let content_area_top = chunks[1].y; + let mut current_row = content_area_top; let mut entry_lines_vec: Vec> = Vec::new(); for (i, entry) in app.entry_disp.iter().enumerate() { @@ -123,8 +115,7 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Span::styled(prefix, Style::default().fg(app.theme.yellow())), Span::styled( format!("[{}] ", i + 1), - Style::default().fg(app.theme.orange()), - ), + Style::default().fg(app.theme.orange())), Span::styled( &entry.n, if is_selected { @@ -277,75 +268,51 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), // 8: totp Constraint::Length(3), // 9: tags input Constraint::Length(3), // 10: Tags display - Constraint::Length(4), // 11: notes - Constraint::Min(1), // 12: msg + Constraint::Min(4), // 11: notes + Constraint::Length(3), // 12: msg ]) .split(area); - + let title = Paragraph::new("Fill in the details below") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) - .style(if app.add_fi == 0 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 0 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 0 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(name_field, chunks[2]); - + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) - .style(if app.add_fi == 1 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 1 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 1 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(user_field, chunks[3]); - + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) - .style(if app.add_fi == 2 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 2 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 2 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(pass_field, chunks[4]); - + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -364,11 +331,11 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { strength.percentage, strength.strength ); - let strenth_disp = Paragraph::new(bar) + let strength_display = Paragraph::new(bar) .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); - f.render_widget(strenth_disp, chunks[5]); - + f.render_widget(strength_display, chunks[5]); + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -378,40 +345,24 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - + let url_field = Paragraph::new(format!("URL (optional): {}", app.n_entry_url)) - .style(if app.add_fi == 3 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 3 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 3 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(url_field, chunks[7]); let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) - .style(if app.add_fi == 4 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 4 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 4 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(totp_field, chunks[8]); @@ -421,57 +372,51 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 5 { active_style } else { inactive_style }) .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 5 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(tags_input, chunks[9]); - + if !app.n_entry_tags.is_empty() { - let tags_disp = app + let tags_display = app .n_entry_tags .iter() .enumerate() .map(|(i, tag)| format!("[{}]{} ", i + 1, tag)) .collect::>() .join(" "); - let tags_widget = Paragraph::new(format!("Added: {tags_disp}")) + let tags_widget = Paragraph::new(format!("Added: {tags_display}")) .style(Style::default().fg(app.theme.orange())) .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } - - let notes = Paragraph::new(format!("Notes:\n{}", app.n_entry_notes)) - .style(if app.add_fi == 6 { - active_style - } else { - inactive_style - }) + + let notes_lines: Vec = if app.n_entry_notes.is_empty() { + vec![Line::from("Notes:")] + } else { + let mut lines = vec![Line::from("Notes:")]; + for line in app.n_entry_notes.lines() { + lines.push(Line::from(line.to_string())); + } + lines + }; + + let notes = Paragraph::new(notes_lines) + .style(if app.add_fi == 6 { active_style } else { inactive_style }) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 6 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(notes, chunks[11]); - + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -517,75 +462,51 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), // 8: totp Constraint::Length(3), // 9: tags input Constraint::Length(3), // 10: tags display - Constraint::Length(4), // 11: notes - Constraint::Min(1), // 12: msg + Constraint::Min(4), // 11: notes + Constraint::Length(3), // 12: msg ]) .split(area); - + let title = Paragraph::new("Edit entry details (password changes are tracked)") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) - .style(if app.add_fi == 0 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 0 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 0 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(name_field, chunks[2]); - + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) - .style(if app.add_fi == 1 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 1 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 1 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(user_field, chunks[3]); - + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) - .style(if app.add_fi == 2 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 2 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 2 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(pass_field, chunks[4]); - + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -604,11 +525,11 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { strength.percentage, strength.strength ); - let strenth_disp = Paragraph::new(bar) + let strength_display = Paragraph::new(bar) .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); - f.render_widget(strenth_disp, chunks[5]); - + f.render_widget(strength_display, chunks[5]); + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -618,40 +539,24 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - + let url_field = Paragraph::new(format!("URL: {}", app.n_entry_url)) - .style(if app.add_fi == 3 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 3 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 3 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(url_field, chunks[7]); let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) - .style(if app.add_fi == 4 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 4 { active_style } else { inactive_style }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 4 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(totp_field, chunks[8]); @@ -661,57 +566,51 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { - active_style - } else { - inactive_style - }) + .style(if app.add_fi == 5 { active_style } else { inactive_style }) .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 5 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(tags_input, chunks[9]); - + if !app.n_entry_tags.is_empty() { - let tags_disp = app + let tags_display = app .n_entry_tags .iter() .enumerate() .map(|(i, tag)| format!("[{}]{} ", i + 1, tag)) .collect::>() .join(" "); - let tags_widget = Paragraph::new(format!("Tags: {tags_disp}")) + let tags_widget = Paragraph::new(format!("Tags: {tags_display}")) .style(Style::default().fg(app.theme.orange())) .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } - - let notes = Paragraph::new(format!("Notes:\n{}", app.n_entry_notes)) - .style(if app.add_fi == 6 { - active_style - } else { - inactive_style - }) + + let notes_lines: Vec = if app.n_entry_notes.is_empty() { + vec![Line::from("Notes:")] + } else { + let mut lines = vec![Line::from("Notes:")]; + for line in app.n_entry_notes.lines() { + lines.push(Line::from(line.to_string())); + } + lines + }; + + let notes = Paragraph::new(notes_lines) + .style(if app.add_fi == 6 { active_style } else { inactive_style }) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 6 { - app.theme.green() - } else { - app.theme.gray() - })), + .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) ); f.render_widget(notes, chunks[11]); - + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -743,52 +642,49 @@ pub fn draw_history(f: &mut Frame, size: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([Constraint::Length(3), Constraint::Min(5)]) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + ]) .split(size); - + if let Some(ref vault) = app.vault { if app.selected_entry < app.entry_disp.len() { let entry = &app.entry_disp[app.selected_entry]; if let Some(vault_entry) = vault.e.iter().find(|e| e.id == entry.id) { let title = Paragraph::new(vec![ - Line::from(vec![Span::styled( - format!("History for: {}", vault_entry.n), - Style::default() - .fg(app.theme.yellow()) - .add_modifier(Modifier::BOLD), - )]), + Line::from(vec![ + Span::styled( + format!("History for: {}", vault_entry.n), + Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) + ), + ]), Line::from(""), - Line::from(vec![Span::styled( - "Last 5 changes", - Style::default().fg(app.theme.gray()), - )]), + Line::from(vec![ + Span::styled("Last 5 changes", Style::default().fg(app.theme.gray())), + ]), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())), + .border_style(Style::default().fg(app.theme.gray())) ); f.render_widget(title, chunks[0]); - + if vault_entry.history.is_empty() { let empty_area = centered_rect(60, 20, chunks[1]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled( - "No password changes recorded", - Style::default() - .fg(app.theme.gray()) - .add_modifier(Modifier::BOLD), - )), + Line::from(Span::styled("No password changes recorded", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())), + .border_style(Style::default().fg(app.theme.gray())) ); f.render_widget(empty, empty_area); } else { @@ -853,46 +749,41 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), ]) .split(size); - - let title = Paragraph::new("Enter the number of the entry to delete") + + let title = Paragraph::new("⚠ Enter the number of the entry to delete") .style(Style::default().fg(app.theme.orange())) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.orange())), + .border_style(Style::default().fg(app.theme.orange())) ); f.render_widget(title, chunks[0]); - + let empty_vec = Vec::new(); - let entry_disp = if app.entry_disp.is_empty() { + let entries_to_display = if app.entry_disp.is_empty() { app.vault.as_ref().map_or(&empty_vec, |v| &v.e) } else { &app.entry_disp }; - - if entry_disp.is_empty() { + + if entries_to_display.is_empty() { let empty_area = centered_rect(60, 20, chunks[1]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled( - "No passwords to delete", - Style::default() - .fg(app.theme.gray()) - .add_modifier(Modifier::BOLD), - )), + Line::from(Span::styled("No passwords to delete", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())), + .border_style(Style::default().fg(app.theme.gray())) ); f.render_widget(empty, empty_area); } else { - let items: Vec = entry_disp + let items: Vec = entries_to_display .iter() .enumerate() .map(|(i, entry)| { @@ -911,7 +802,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { let list = List::new(items).block(Block::default().borders(Borders::NONE)); f.render_widget(list, chunks[1]); } - + let input = Paragraph::new(format!("Entry number: {}", app.input_buffer)) .style( Style::default() @@ -922,7 +813,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.red())), + .border_style(Style::default().fg(app.theme.red())) ); f.render_widget(input, chunks[2]); } @@ -994,4 +885,4 @@ pub fn draw_context_menu(f: &mut Frame, app: &App) { let paragraph = Paragraph::new(menu_lines).block(block); f.render_widget(paragraph, area); -} +} \ No newline at end of file From 40bd19ee22772e2554a21713a335748a21bb8e6e Mon Sep 17 00:00:00 2001 From: hachimamma Date: Fri, 27 Feb 2026 16:47:12 +0530 Subject: [PATCH 25/48] UPDATE: Updated Passlock Login --- src/ui/widgets/passwords.rs | 315 +++++++++++++++++++++++++----------- src/ui/widgets/vault.rs | 156 +++++++++++------- 2 files changed, 323 insertions(+), 148 deletions(-) diff --git a/src/ui/widgets/passwords.rs b/src/ui/widgets/passwords.rs index 1fc61a8..0a114c1 100644 --- a/src/ui/widgets/passwords.rs +++ b/src/ui/widgets/passwords.rs @@ -7,7 +7,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, BorderType, List, ListItem, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap}, Frame, }; @@ -31,10 +31,7 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(4), - Constraint::Min(5), - ]) + .constraints([Constraint::Length(4), Constraint::Min(5)]) .split(size); let filter_status = if let Some(ref tag) = app.active_tf { @@ -60,22 +57,25 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Line::from(vec![ Span::styled( format!("Total: {} entries", app.entry_disp.len()), - Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), ), Span::styled(filter_status, Style::default().fg(app.theme.purple())), Span::styled(clipboard_status, Style::default().fg(app.theme.aqua())), ]), Line::from(""), - Line::from(vec![ - Span::styled("Right-click for options", Style::default().fg(app.theme.gray())), - ]), + Line::from(vec![Span::styled( + "Right-click for options", + Style::default().fg(app.theme.gray()), + )]), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(title, chunks[0]); @@ -88,16 +88,24 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { }; let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled(empty_msg, Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + empty_msg, + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), Line::from(""), - Line::from(Span::styled("Press '2' to add your first password", Style::default().fg(app.theme.gray()))), + Line::from(Span::styled( + "Press '2' to add your first password", + Style::default().fg(app.theme.gray()), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -115,7 +123,8 @@ pub fn draw_view_pwds(f: &mut Frame, size: Rect, app: &mut App) { Span::styled(prefix, Style::default().fg(app.theme.yellow())), Span::styled( format!("[{}] ", i + 1), - Style::default().fg(app.theme.orange())), + Style::default().fg(app.theme.orange()), + ), Span::styled( &entry.n, if is_selected { @@ -272,47 +281,71 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), // 12: msg ]) .split(area); - + let title = Paragraph::new("Fill in the details below") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) - .style(if app.add_fi == 0 { active_style } else { inactive_style }) + .style(if app.add_fi == 0 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 0 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(name_field, chunks[2]); - + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) - .style(if app.add_fi == 1 { active_style } else { inactive_style }) + .style(if app.add_fi == 1 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 1 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(user_field, chunks[3]); - + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) - .style(if app.add_fi == 2 { active_style } else { inactive_style }) + .style(if app.add_fi == 2 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 2 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(pass_field, chunks[4]); - + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -335,7 +368,7 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); f.render_widget(strength_display, chunks[5]); - + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -345,24 +378,40 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - + let url_field = Paragraph::new(format!("URL (optional): {}", app.n_entry_url)) - .style(if app.add_fi == 3 { active_style } else { inactive_style }) + .style(if app.add_fi == 3 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 3 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(url_field, chunks[7]); let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) - .style(if app.add_fi == 4 { active_style } else { inactive_style }) + .style(if app.add_fi == 4 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 4 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(totp_field, chunks[8]); @@ -372,16 +421,24 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { active_style } else { inactive_style }) + .style(if app.add_fi == 5 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 5 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(tags_input, chunks[9]); - + if !app.n_entry_tags.is_empty() { let tags_display = app .n_entry_tags @@ -395,7 +452,7 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } - + let notes_lines: Vec = if app.n_entry_notes.is_empty() { vec![Line::from("Notes:")] } else { @@ -405,18 +462,26 @@ pub fn draw_add_pwd(f: &mut Frame, size: Rect, app: &App) { } lines }; - + let notes = Paragraph::new(notes_lines) - .style(if app.add_fi == 6 { active_style } else { inactive_style }) + .style(if app.add_fi == 6 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 6 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(notes, chunks[11]); - + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -466,47 +531,71 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), // 12: msg ]) .split(area); - + let title = Paragraph::new("Edit entry details (password changes are tracked)") .style(Style::default().fg(app.theme.yellow())) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - + let active_style = Style::default() .fg(app.theme.green()) .add_modifier(Modifier::BOLD); let inactive_style = Style::default().fg(app.theme.gray()); - + let name_field = Paragraph::new(format!("Name: {}", app.n_entry_name)) - .style(if app.add_fi == 0 { active_style } else { inactive_style }) + .style(if app.add_fi == 0 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 0 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 0 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(name_field, chunks[2]); - + let user_field = Paragraph::new(format!("Username: {}", app.n_entry_user)) - .style(if app.add_fi == 1 { active_style } else { inactive_style }) + .style(if app.add_fi == 1 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 1 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 1 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(user_field, chunks[3]); - + let pass_field = Paragraph::new(format!("Password: {}", app.n_entry_pass)) - .style(if app.add_fi == 2 { active_style } else { inactive_style }) + .style(if app.add_fi == 2 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 2 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 2 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(pass_field, chunks[4]); - + if !app.n_entry_pass.is_empty() && app.add_fi == 2 { let strength = crypto::calc_pwd_strength(&app.n_entry_pass); let strength_color = match strength.strength.as_str() { @@ -529,7 +618,7 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { .style(Style::default().fg(strength_color)) .alignment(Alignment::Center); f.render_widget(strength_display, chunks[5]); - + if !strength.feedback.is_empty() { let feedback_text = format!("↳ {}", strength.feedback.join(", ")); let feedback = Paragraph::new(feedback_text) @@ -539,24 +628,40 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { f.render_widget(feedback, chunks[6]); } } - + let url_field = Paragraph::new(format!("URL: {}", app.n_entry_url)) - .style(if app.add_fi == 3 { active_style } else { inactive_style }) + .style(if app.add_fi == 3 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 3 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 3 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(url_field, chunks[7]); let totp_field = Paragraph::new(format!("2FA Secret (optional): {}", app.n_entry_totp)) - .style(if app.add_fi == 4 { active_style } else { inactive_style }) + .style(if app.add_fi == 4 { + active_style + } else { + inactive_style + }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 4 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 4 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(totp_field, chunks[8]); @@ -566,16 +671,24 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { "Tags: (Tab to focus)".to_string() }; let tags_input = Paragraph::new(tags_text) - .style(if app.add_fi == 5 { active_style } else { inactive_style }) + .style(if app.add_fi == 5 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: true }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 5 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 5 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(tags_input, chunks[9]); - + if !app.n_entry_tags.is_empty() { let tags_display = app .n_entry_tags @@ -589,7 +702,7 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { .wrap(Wrap { trim: true }); f.render_widget(tags_widget, chunks[10]); } - + let notes_lines: Vec = if app.n_entry_notes.is_empty() { vec![Line::from("Notes:")] } else { @@ -599,18 +712,26 @@ pub fn draw_edit_pwd(f: &mut Frame, size: Rect, app: &App) { } lines }; - + let notes = Paragraph::new(notes_lines) - .style(if app.add_fi == 6 { active_style } else { inactive_style }) + .style(if app.add_fi == 6 { + active_style + } else { + inactive_style + }) .wrap(Wrap { trim: false }) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(if app.add_fi == 6 { app.theme.green() } else { app.theme.gray() })) + .border_style(Style::default().fg(if app.add_fi == 6 { + app.theme.green() + } else { + app.theme.gray() + })), ); f.render_widget(notes, chunks[11]); - + if !app.msg.is_empty() { let msg_style = match app.msg_type { MessageType::Success => Style::default().fg(app.theme.green()), @@ -642,49 +763,52 @@ pub fn draw_history(f: &mut Frame, size: Rect, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) - .constraints([ - Constraint::Length(3), - Constraint::Min(5), - ]) + .constraints([Constraint::Length(3), Constraint::Min(5)]) .split(size); - + if let Some(ref vault) = app.vault { if app.selected_entry < app.entry_disp.len() { let entry = &app.entry_disp[app.selected_entry]; if let Some(vault_entry) = vault.e.iter().find(|e| e.id == entry.id) { let title = Paragraph::new(vec![ - Line::from(vec![ - Span::styled( - format!("History for: {}", vault_entry.n), - Style::default().fg(app.theme.yellow()).add_modifier(Modifier::BOLD) - ), - ]), + Line::from(vec![Span::styled( + format!("History for: {}", vault_entry.n), + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )]), Line::from(""), - Line::from(vec![ - Span::styled("Last 5 changes", Style::default().fg(app.theme.gray())), - ]), + Line::from(vec![Span::styled( + "Last 5 changes", + Style::default().fg(app.theme.gray()), + )]), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(title, chunks[0]); - + if vault_entry.history.is_empty() { let empty_area = centered_rect(60, 20, chunks[1]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("No password changes recorded", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "No password changes recorded", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -749,7 +873,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Constraint::Length(3), ]) .split(size); - + let title = Paragraph::new("⚠ Enter the number of the entry to delete") .style(Style::default().fg(app.theme.orange())) .alignment(Alignment::Center) @@ -757,29 +881,34 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.orange())) + .border_style(Style::default().fg(app.theme.orange())), ); f.render_widget(title, chunks[0]); - + let empty_vec = Vec::new(); let entries_to_display = if app.entry_disp.is_empty() { app.vault.as_ref().map_or(&empty_vec, |v| &v.e) } else { &app.entry_disp }; - + if entries_to_display.is_empty() { let empty_area = centered_rect(60, 20, chunks[1]); let empty = Paragraph::new(vec![ Line::from(""), - Line::from(Span::styled("No passwords to delete", Style::default().fg(app.theme.gray()).add_modifier(Modifier::BOLD))), + Line::from(Span::styled( + "No passwords to delete", + Style::default() + .fg(app.theme.gray()) + .add_modifier(Modifier::BOLD), + )), ]) .alignment(Alignment::Center) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.gray())) + .border_style(Style::default().fg(app.theme.gray())), ); f.render_widget(empty, empty_area); } else { @@ -802,7 +931,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { let list = List::new(items).block(Block::default().borders(Borders::NONE)); f.render_widget(list, chunks[1]); } - + let input = Paragraph::new(format!("Entry number: {}", app.input_buffer)) .style( Style::default() @@ -813,7 +942,7 @@ pub fn draw_del_pwd(f: &mut Frame, size: Rect, app: &App) { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.red())) + .border_style(Style::default().fg(app.theme.red())), ); f.render_widget(input, chunks[2]); } @@ -885,4 +1014,4 @@ pub fn draw_context_menu(f: &mut Frame, app: &App) { let paragraph = Paragraph::new(menu_lines).block(block); f.render_widget(paragraph, area); -} \ No newline at end of file +} diff --git a/src/ui/widgets/vault.rs b/src/ui/widgets/vault.rs index 119e3fa..2cd3547 100644 --- a/src/ui/widgets/vault.rs +++ b/src/ui/widgets/vault.rs @@ -6,7 +6,8 @@ use crate::crypto; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, - widgets::{Block, Borders, Clear, Paragraph, Wrap}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap}, Frame, }; @@ -14,51 +15,83 @@ pub fn draw_loading(f: &mut Frame, size: Rect, app: &App) { let area = centered_rect(50, 30, size); let block = Block::default() .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.theme.yellow())) - .title(" PASSLOCK ") - .title_alignment(Alignment::Center) + .title(Span::styled( + " [ PassLock ] ", + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + )) .style(Style::default().bg(app.theme.bg0())); - let text = Paragraph::new("Initializing vault...") - .block(block) - .alignment(Alignment::Center) - .style(Style::default().fg(app.theme.fg())); + + let text = Paragraph::new(vec![ + Line::from(""), + Line::from(Span::styled( + "Initializing vault...", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from(Span::styled( + "Please wait", + Style::default().fg(app.theme.gray()), + )), + ]) + .block(block) + .alignment(Alignment::Center); + f.render_widget(Clear, area); f.render_widget(text, area); } -#[allow(clippy::cast_sign_loss)] +#[allow(clippy::cast_sign_loss, clippy::too_many_lines)] pub fn draw_create_vault(f: &mut Frame, size: Rect, app: &App) { let area = centered_rect(65, 70, size); + + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.orange())) + .title(Span::styled( + " [ Create Vault ] ", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, area); + let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) .constraints([ Constraint::Length(3), + Constraint::Length(2), Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), + Constraint::Length(2), + Constraint::Length(2), + Constraint::Length(2), Constraint::Length(1), - Constraint::Length(3), Constraint::Min(2), - Constraint::Length(3), ]) .split(area); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.orange())) - .title("═══ CREATE VAULT ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, area); - let title = Paragraph::new("Create your master password") - .style(Style::default().fg(app.theme.yellow())) + .style( + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + ) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - let pwd_text = format!("Password: {}", "•".repeat(app.input_buffer.len())); + let pwd_text = if app.input_buffer.is_empty() { + "Password: _".to_string() + } else { + format!("Password: {}", "•".repeat(app.input_buffer.len())) + }; let pwd_style = if app.input_field == InputField::Password { Style::default() .fg(app.theme.green()) @@ -66,8 +99,10 @@ pub fn draw_create_vault(f: &mut Frame, size: Rect, app: &App) { } else { Style::default().fg(app.theme.gray()) }; - let password_input = Paragraph::new(pwd_text).style(pwd_style); - f.render_widget(password_input, chunks[2]); + let password_input = Paragraph::new(pwd_text) + .style(pwd_style) + .alignment(Alignment::Left); + f.render_widget(password_input, chunks[1]); if !app.input_buffer.is_empty() && app.input_field == InputField::Password { let strength = crypto::calc_pwd_strength(&app.input_buffer); @@ -102,7 +137,11 @@ pub fn draw_create_vault(f: &mut Frame, size: Rect, app: &App) { } } - let confirm_text = format!("Confirm: {}", "•".repeat(app.input_buffer2.len())); + let confirm_text = if app.input_buffer2.is_empty() { + "Confirm: _".to_string() + } else { + format!("Confirm: {}", "•".repeat(app.input_buffer2.len())) + }; let confirm_style = if app.input_field == InputField::PasswordConfirm { Style::default() .fg(app.theme.green()) @@ -110,7 +149,9 @@ pub fn draw_create_vault(f: &mut Frame, size: Rect, app: &App) { } else { Style::default().fg(app.theme.gray()) }; - let confirm_input = Paragraph::new(confirm_text).style(confirm_style); + let confirm_input = Paragraph::new(confirm_text) + .style(confirm_style) + .alignment(Alignment::Left); f.render_widget(confirm_input, chunks[6]); if !app.msg.is_empty() { @@ -125,47 +166,57 @@ pub fn draw_create_vault(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Center); f.render_widget(msg, chunks[7]); } - - let help = Paragraph::new("| Tab: Switch | Enter: Create | Esc: Quit |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[8]); } pub fn draw_unlock_vault(f: &mut Frame, size: Rect, app: &App) { let area = centered_rect(60, 40, size); + + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())) + .title(Span::styled( + " [ Unlock Vault ] ", + Style::default() + .fg(app.theme.aqua()) + .add_modifier(Modifier::BOLD), + )) + .style(Style::default().bg(app.theme.bg0())); + f.render_widget(block, area); + let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) .constraints([ Constraint::Length(3), + Constraint::Length(2), Constraint::Length(1), - Constraint::Length(3), Constraint::Min(2), - Constraint::Length(3), ]) .split(area); - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(app.theme.aqua())) - .title("═══ UNLOCK VAULT ═══") - .title_alignment(Alignment::Center) - .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, area); - let title = Paragraph::new("Enter your master password") - .style(Style::default().fg(app.theme.yellow())) + .style( + Style::default() + .fg(app.theme.yellow()) + .add_modifier(Modifier::BOLD), + ) .alignment(Alignment::Center); f.render_widget(title, chunks[0]); - let pwd_text = format!("Password: {}", "•".repeat(app.input_buffer.len())); - let password_input = Paragraph::new(pwd_text).style( - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), - ); - f.render_widget(password_input, chunks[2]); + let pwd_text = if app.input_buffer.is_empty() { + "Password: _".to_string() + } else { + format!("Password: {}", "•".repeat(app.input_buffer.len())) + }; + let password_input = Paragraph::new(pwd_text) + .style( + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), + ) + .alignment(Alignment::Left); + f.render_widget(password_input, chunks[1]); if !app.msg.is_empty() { let msg_style = match app.msg_type { @@ -179,9 +230,4 @@ pub fn draw_unlock_vault(f: &mut Frame, size: Rect, app: &App) { .alignment(Alignment::Center); f.render_widget(msg, chunks[3]); } - - let help = Paragraph::new("| Enter: Unlock | Esc: Quit |") - .style(Style::default().fg(app.theme.gray())) - .alignment(Alignment::Center); - f.render_widget(help, chunks[4]); } From d78b9c85214381675233bfb96cd4e9367ca1ee1d Mon Sep 17 00:00:00 2001 From: hachimamma Date: Fri, 27 Feb 2026 23:25:23 +0530 Subject: [PATCH 26/48] UPDATE: Added Refresh rate counter --- src/ui/app.rs | 4 +++ src/ui/mod.rs | 32 ++++++++++++++++---- src/ui/widgets/menu.rs | 66 +++++++++++++++++++++++++++++++++++------- 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 5a593c2..24e0017 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -51,6 +51,8 @@ pub struct App { pub show_totp_codes: bool, pub clipboard_timeout: u64, pub menu_click_map: Vec<(u16, u16, u16, u16, usize)>, + pub rr_ms: u64, + pub rr_cmap: Vec<(u16, u16, u16, u16)>, } impl App { @@ -100,6 +102,8 @@ impl App { show_totp_codes: true, clipboard_timeout: 30, menu_click_map: Vec::new(), + rr_ms: 100, + rr_cmap: Vec::new(), } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f9328d9..b74e1af 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -177,7 +177,8 @@ fn run_app( terminal.draw(|f| ui(f, app))?; - match event::read()? { + if event::poll(std::time::Duration::from_millis(app.rr_ms))? { + match event::read()? { Event::Mouse(mouse_event) => match mouse_event.kind { MouseEventKind::Down(MouseButton::Right) => { if app.screen == Screen::ViewPasswords || app.screen == Screen::SearchPassword { @@ -209,14 +210,15 @@ fn run_app( if app.context_menu_visible { handle_context_menu_click(app, mouse_event.column, mouse_event.row); } else if app.screen == Screen::MainMenu { + // ═══ HANDLE MAIN MENU CLICKS ═══ let x = mouse_event.column; let y = mouse_event.row; - + for (y_start, y_end, x_start, x_end, menu_idx) in &app.menu_click_map { if y >= *y_start && y <= *y_end && x >= *x_start && x < *x_end { app.selected_menu = *menu_idx; app.msg.clear(); - + match *menu_idx { 0 => { app.screen = Screen::ViewPasswords; @@ -262,7 +264,26 @@ fn run_app( break; } } - + + // Check refresh rate control clicks + for (y_start, y_end, x_start, x_end) in &app.rr_cmap { + if y >= *y_start && y < *y_end && x >= *x_start && x < *x_end { + // Get the center of the widget to determine left/right side + let center_x = (x_start + x_end) / 2; + + if x < center_x { + // Left side - MINUS button + app.rr_ms = (app.rr_ms.saturating_sub(50)).max(50); + app.set_msg(&format!("Refresh: {}ms", app.rr_ms), screens::MessageType::Info); + } else { + // Right side - PLUS button + app.rr_ms = (app.rr_ms + 50).min(1000); + app.set_msg(&format!("Refresh: {}ms", app.rr_ms), screens::MessageType::Info); + } + break; + } + } + app.context_menu_visible = false; } else { app.context_menu_visible = false; @@ -436,6 +457,7 @@ fn run_app( } _ => {} } + } } Ok(()) } @@ -464,4 +486,4 @@ fn ui(f: &mut Frame, app: &mut App) { if app.context_menu_visible { draw_context_menu(f, app); } -} +} \ No newline at end of file diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 974f2b2..1c172df 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -32,6 +32,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { Constraint::Length(3), // title Constraint::Length(sysinfo_height), // system info and crypto Constraint::Min(20), // menu + Constraint::Length(3), // refresh rate control ]) .split(main_layout[0]); @@ -251,22 +252,28 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )); let menu_area = left_layout[2]; - - // Mouse click execution + + // Calculate inner area BEFORE moving menu_block let menu_inner_area = menu_block.inner(menu_area); - + let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); - let mut current_y = menu_inner_area.y + 1; - + // ═══ MOUSE CLICK TRACKING FOR MENU ITEMS ═══ + // Store clickable regions for mouse handling + // Each menu item takes 4 lines (title, desc, 2 blank lines), first item has extra blank line at top + let mut current_y = menu_inner_area.y + 1; // Start after the extra blank line before first item + + // Clear previous menu click map app.menu_click_map.clear(); - + for i in 0..all_items.len() { - let item_height = 4u16; + let item_height = 4u16; // Each item: title line + desc line + 2 blank lines let clickable_y_start = current_y; - let clickable_y_end = current_y + 1; - + let clickable_y_end = current_y + 1; // Title and description lines are clickable + + // Store the clickable region: (y_start, y_end, x_start, x_end, menu_index) + // x_start and x_end define the horizontal bounds within the menu app.menu_click_map.push(( clickable_y_start, clickable_y_end, @@ -274,7 +281,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { menu_inner_area.x + menu_inner_area.width, i, )); - + current_y += item_height; } @@ -439,4 +446,41 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { ); f.render_widget(stats_box, right_layout[2]); } -} + + // Refresh rate control widget + let refresh_ms = app.rr_ms; + let refresh_area = left_layout[3]; + let refresh_widget = Paragraph::new(Line::from(vec![ + Span::styled("[ ", Style::default().fg(app.theme.gray())), + Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled(" ] ", Style::default().fg(app.theme.gray())), + Span::styled( + format!("{}ms", refresh_ms), + Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), + ), + Span::styled(" [ ", Style::default().fg(app.theme.gray())), + Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled(" ]", Style::default().fg(app.theme.gray())), + ])) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.aqua())) + .title(Span::styled( + " Refresh Rate ", + Style::default().fg(app.theme.aqua()), + )), + ); + f.render_widget(refresh_widget, refresh_area); + + // Store clickable region for refresh rate control + app.rr_cmap.clear(); + app.rr_cmap.push(( + refresh_area.y, + refresh_area.y + refresh_area.height, + refresh_area.x, + refresh_area.x + refresh_area.width, + )); +} \ No newline at end of file From b0c9494fc264728474e135767ce9c2acd5188a7d Mon Sep 17 00:00:00 2001 From: hachimamma Date: Fri, 27 Feb 2026 23:26:33 +0530 Subject: [PATCH 27/48] FIX: cargo fmt and cargo clippy --- src/ui/mod.rs | 467 +++++++++++++++++++++-------------------- src/ui/widgets/menu.rs | 36 ++-- 2 files changed, 262 insertions(+), 241 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b74e1af..f14a7f5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -179,195 +179,169 @@ fn run_app( if event::poll(std::time::Duration::from_millis(app.rr_ms))? { match event::read()? { - Event::Mouse(mouse_event) => match mouse_event.kind { - MouseEventKind::Down(MouseButton::Right) => { - if app.screen == Screen::ViewPasswords || app.screen == Screen::SearchPassword { - let y = mouse_event.row; - let x = mouse_event.column; - - if (5..=170).contains(&x) { - for (start_row, end_row, entry_idx) in &app.entry_row_map { - if y >= *start_row && y <= *end_row { - app.context_menu_visible = true; - app.context_menu_entry_idx = *entry_idx; - app.context_menu_selected = 0; - - let term_width = 174u16; - let menu_width = 24u16; - app.context_menu_x = if x + menu_width > term_width { - term_width.saturating_sub(menu_width) - } else { - x - }; - app.context_menu_y = y; - break; + Event::Mouse(mouse_event) => match mouse_event.kind { + MouseEventKind::Down(MouseButton::Right) => { + if app.screen == Screen::ViewPasswords + || app.screen == Screen::SearchPassword + { + let y = mouse_event.row; + let x = mouse_event.column; + + if (5..=170).contains(&x) { + for (start_row, end_row, entry_idx) in &app.entry_row_map { + if y >= *start_row && y <= *end_row { + app.context_menu_visible = true; + app.context_menu_entry_idx = *entry_idx; + app.context_menu_selected = 0; + + let term_width = 174u16; + let menu_width = 24u16; + app.context_menu_x = if x + menu_width > term_width { + term_width.saturating_sub(menu_width) + } else { + x + }; + app.context_menu_y = y; + break; + } } } } } - } - MouseEventKind::Down(MouseButton::Left) => { - if app.context_menu_visible { - handle_context_menu_click(app, mouse_event.column, mouse_event.row); - } else if app.screen == Screen::MainMenu { - // ═══ HANDLE MAIN MENU CLICKS ═══ - let x = mouse_event.column; - let y = mouse_event.row; - - for (y_start, y_end, x_start, x_end, menu_idx) in &app.menu_click_map { - if y >= *y_start && y <= *y_end && x >= *x_start && x < *x_end { - app.selected_menu = *menu_idx; - app.msg.clear(); - - match *menu_idx { - 0 => { - app.screen = Screen::ViewPasswords; - app.active_tf = None; - app.search_query.clear(); - if let Some(ref vault) = app.vault { - app.entry_disp = vault.e.clone(); - } - } - 1 => { - app.screen = Screen::AddPassword; - app.ca_form(); - } - 2 => { - app.screen = Screen::SearchPassword; - app.search_query.clear(); - app.entry_disp.clear(); - } - 3 => { - app.screen = Screen::FilterByTag; - app.select_tf = 0; - app.filter_bt(None); - } - 4 => { - app.screen = Screen::GeneratePassword; - app.input_buffer = String::from("16"); - app.gen_pwd.clear(); - } - 5 => { - app.screen = Screen::DeletePassword; - app.input_buffer.clear(); - if app.entry_disp.is_empty() { + MouseEventKind::Down(MouseButton::Left) => { + if app.context_menu_visible { + handle_context_menu_click(app, mouse_event.column, mouse_event.row); + } else if app.screen == Screen::MainMenu { + // ═══ HANDLE MAIN MENU CLICKS ═══ + let x = mouse_event.column; + let y = mouse_event.row; + + for (y_start, y_end, x_start, x_end, menu_idx) in &app.menu_click_map { + if y >= *y_start && y <= *y_end && x >= *x_start && x < *x_end { + app.selected_menu = *menu_idx; + app.msg.clear(); + + match *menu_idx { + 0 => { + app.screen = Screen::ViewPasswords; + app.active_tf = None; + app.search_query.clear(); if let Some(ref vault) = app.vault { app.entry_disp = vault.e.clone(); } } + 1 => { + app.screen = Screen::AddPassword; + app.ca_form(); + } + 2 => { + app.screen = Screen::SearchPassword; + app.search_query.clear(); + app.entry_disp.clear(); + } + 3 => { + app.screen = Screen::FilterByTag; + app.select_tf = 0; + app.filter_bt(None); + } + 4 => { + app.screen = Screen::GeneratePassword; + app.input_buffer = String::from("16"); + app.gen_pwd.clear(); + } + 5 => { + app.screen = Screen::DeletePassword; + app.input_buffer.clear(); + if app.entry_disp.is_empty() { + if let Some(ref vault) = app.vault { + app.entry_disp = vault.e.clone(); + } + } + } + 6 => { + app.should_quit = true; + } + _ => {} } - 6 => { - app.should_quit = true; - } - _ => {} + break; } - break; } - } - - // Check refresh rate control clicks - for (y_start, y_end, x_start, x_end) in &app.rr_cmap { - if y >= *y_start && y < *y_end && x >= *x_start && x < *x_end { - // Get the center of the widget to determine left/right side - let center_x = (x_start + x_end) / 2; - - if x < center_x { - // Left side - MINUS button - app.rr_ms = (app.rr_ms.saturating_sub(50)).max(50); - app.set_msg(&format!("Refresh: {}ms", app.rr_ms), screens::MessageType::Info); - } else { - // Right side - PLUS button - app.rr_ms = (app.rr_ms + 50).min(1000); - app.set_msg(&format!("Refresh: {}ms", app.rr_ms), screens::MessageType::Info); + + // Check refresh rate control clicks + for (y_start, y_end, x_start, x_end) in &app.rr_cmap { + if y >= *y_start && y < *y_end && x >= *x_start && x < *x_end { + // Get the center of the widget to determine left/right side + let center_x = (x_start + x_end) / 2; + + if x < center_x { + // Left side - MINUS button + app.rr_ms = (app.rr_ms.saturating_sub(50)).max(50); + app.set_msg( + &format!("Refresh: {}ms", app.rr_ms), + screens::MessageType::Info, + ); + } else { + // Right side - PLUS button + app.rr_ms = (app.rr_ms + 50).min(1000); + app.set_msg( + &format!("Refresh: {}ms", app.rr_ms), + screens::MessageType::Info, + ); + } + break; } - break; } + + app.context_menu_visible = false; + } else { + app.context_menu_visible = false; } - - app.context_menu_visible = false; - } else { - app.context_menu_visible = false; } - } - MouseEventKind::ScrollUp => { - if app.screen == Screen::ViewPasswords && app.selected_entry > 0 { - app.selected_entry -= 1; + MouseEventKind::ScrollUp => { + if app.screen == Screen::ViewPasswords && app.selected_entry > 0 { + app.selected_entry -= 1; + } } - } - MouseEventKind::ScrollDown => { - if app.screen == Screen::ViewPasswords - && app.selected_entry < app.entry_disp.len().saturating_sub(1) - { - app.selected_entry += 1; + MouseEventKind::ScrollDown => { + if app.screen == Screen::ViewPasswords + && app.selected_entry < app.entry_disp.len().saturating_sub(1) + { + app.selected_entry += 1; + } } - } - _ => {} - }, - Event::Key(key_event) => { - if key_event.kind == KeyEventKind::Press { - if app.context_menu_visible { - use crate::ui::clipboard; - use screens::MessageType; - let timeout = app.clipboard_timeout; - - match key_event.code { - KeyCode::Down => { - if app.context_menu_selected < 4 { - app.context_menu_selected += 1; - } - } - KeyCode::Up => { - if app.context_menu_selected > 0 { - app.context_menu_selected -= 1; + _ => {} + }, + Event::Key(key_event) => { + if key_event.kind == KeyEventKind::Press { + if app.context_menu_visible { + use crate::ui::clipboard; + use screens::MessageType; + let timeout = app.clipboard_timeout; + + match key_event.code { + KeyCode::Down => { + if app.context_menu_selected < 4 { + app.context_menu_selected += 1; + } } - } - KeyCode::Enter => { - let entry_idx = app.context_menu_entry_idx; - if entry_idx >= app.entry_disp.len() { - app.context_menu_visible = false; - continue; + KeyCode::Up => { + if app.context_menu_selected > 0 { + app.context_menu_selected -= 1; + } } + KeyCode::Enter => { + let entry_idx = app.context_menu_entry_idx; + if entry_idx >= app.entry_disp.len() { + app.context_menu_visible = false; + continue; + } - let entry = app.entry_disp[entry_idx].clone(); + let entry = app.entry_disp[entry_idx].clone(); - match app.context_menu_selected { - 0 => { - let result = - clipboard::copy_with_timeout(&entry.p, timeout); - app.clipboard_countdown = if result.success { - Some(result.expires_at) - } else { - None - }; - app.set_msg( - &result.message, - if result.success { - MessageType::Success - } else { - MessageType::Error - }, - ); - } - 1 => { - let result = - clipboard::copy_with_timeout(&entry.u, timeout); - app.clipboard_countdown = if result.success { - Some(result.expires_at) - } else { - None - }; - app.set_msg( - &result.message, - if result.success { - MessageType::Success - } else { - MessageType::Error - }, - ); - } - 2 => { - if let Some(ref url) = entry.url { - let result = clipboard::copy_with_timeout(url, timeout); + match app.context_menu_selected { + 0 => { + let result = + clipboard::copy_with_timeout(&entry.p, timeout); app.clipboard_countdown = if result.success { Some(result.expires_at) } else { @@ -381,82 +355,117 @@ fn run_app( MessageType::Error }, ); - } else { + } + 1 => { + let result = + clipboard::copy_with_timeout(&entry.u, timeout); + app.clipboard_countdown = if result.success { + Some(result.expires_at) + } else { + None + }; app.set_msg( - "No URL for this entry!", - MessageType::Error, + &result.message, + if result.success { + MessageType::Success + } else { + MessageType::Error + }, ); } + 2 => { + if let Some(ref url) = entry.url { + let result = + clipboard::copy_with_timeout(url, timeout); + app.clipboard_countdown = if result.success { + Some(result.expires_at) + } else { + None + }; + app.set_msg( + &result.message, + if result.success { + MessageType::Success + } else { + MessageType::Error + }, + ); + } else { + app.set_msg( + "No URL for this entry!", + MessageType::Error, + ); + } + } + 3 => { + let entry_id = entry.id.clone(); + app.load_efe(&entry_id); + } + 4 => { + app.selected_entry = entry_idx; + app.screen = Screen::ViewHistory; + } + _ => {} } - 3 => { - let entry_id = entry.id.clone(); - app.load_efe(&entry_id); - } - 4 => { - app.selected_entry = entry_idx; - app.screen = Screen::ViewHistory; - } - _ => {} + app.context_menu_visible = false; + } + _ => { + app.context_menu_visible = false; } - app.context_menu_visible = false; - } - _ => { - app.context_menu_visible = false; } + continue; } - continue; - } - match app.screen { - Screen::VaultCheck => {} - Screen::CreateVault => handle_cvi(app, key_event.code), - Screen::UnlockVault => handle_uvi(app, key_event.code), - Screen::MainMenu => { - if handle_mmi(app, key_event.code) { - return Ok(()); + match app.screen { + Screen::VaultCheck => {} + Screen::CreateVault => handle_cvi(app, key_event.code), + Screen::UnlockVault => handle_uvi(app, key_event.code), + Screen::MainMenu => { + if handle_mmi(app, key_event.code) { + return Ok(()); + } } - } - Screen::ViewPasswords => handle_vpi(app, key_event.code), - Screen::AddPassword => { - if key_event.code == KeyCode::Char('s') - && key_event - .modifiers - .contains(crossterm::event::KeyModifiers::CONTROL) - { - app.add_entry(); - } else { - handle_api(app, key_event.code); + Screen::ViewPasswords => handle_vpi(app, key_event.code), + Screen::AddPassword => { + if key_event.code == KeyCode::Char('s') + && key_event + .modifiers + .contains(crossterm::event::KeyModifiers::CONTROL) + { + app.add_entry(); + } else { + handle_api(app, key_event.code); + } } - } - Screen::EditPassword => { - if key_event.code == KeyCode::Char('s') - && key_event - .modifiers - .contains(crossterm::event::KeyModifiers::CONTROL) - { - app.edit_entry(); - } else { - handle_epi(app, key_event.code); + Screen::EditPassword => { + if key_event.code == KeyCode::Char('s') + && key_event + .modifiers + .contains(crossterm::event::KeyModifiers::CONTROL) + { + app.edit_entry(); + } else { + handle_epi(app, key_event.code); + } } - } - Screen::ViewHistory => handle_vhi(app, key_event.code), - Screen::SearchPassword => handle_si(app, key_event.code), - Screen::GeneratePassword => handle_gi(app, key_event.code), - Screen::DeletePassword => handle_di(app, key_event.code), - Screen::FilterByTag => handle_tfi(app, key_event.code), - Screen::ThemeSelector => handle_theme_selector(app, key_event.code), - Screen::OptionsMenu => { - if handle_options_menu(app, key_event.code) { - return Ok(()); + Screen::ViewHistory => handle_vhi(app, key_event.code), + Screen::SearchPassword => handle_si(app, key_event.code), + Screen::GeneratePassword => handle_gi(app, key_event.code), + Screen::DeletePassword => handle_di(app, key_event.code), + Screen::FilterByTag => handle_tfi(app, key_event.code), + Screen::ThemeSelector => handle_theme_selector(app, key_event.code), + Screen::OptionsMenu => { + if handle_options_menu(app, key_event.code) { + return Ok(()); + } } + Screen::Help => handle_help_screen(app, key_event.code), + Screen::Settings => handle_settings_screen(app, key_event.code), } - Screen::Help => handle_help_screen(app, key_event.code), - Screen::Settings => handle_settings_screen(app, key_event.code), } } + _ => {} } - _ => {} - } } } Ok(()) @@ -486,4 +495,4 @@ fn ui(f: &mut Frame, app: &mut App) { if app.context_menu_visible { draw_context_menu(f, app); } -} \ No newline at end of file +} diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 1c172df..a910b37 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -252,10 +252,10 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )); let menu_area = left_layout[2]; - + // Calculate inner area BEFORE moving menu_block let menu_inner_area = menu_block.inner(menu_area); - + let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); @@ -263,15 +263,15 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { // Store clickable regions for mouse handling // Each menu item takes 4 lines (title, desc, 2 blank lines), first item has extra blank line at top let mut current_y = menu_inner_area.y + 1; // Start after the extra blank line before first item - + // Clear previous menu click map app.menu_click_map.clear(); - + for i in 0..all_items.len() { let item_height = 4u16; // Each item: title line + desc line + 2 blank lines let clickable_y_start = current_y; let clickable_y_end = current_y + 1; // Title and description lines are clickable - + // Store the clickable region: (y_start, y_end, x_start, x_end, menu_index) // x_start and x_end define the horizontal bounds within the menu app.menu_click_map.push(( @@ -281,7 +281,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { menu_inner_area.x + menu_inner_area.width, i, )); - + current_y += item_height; } @@ -452,14 +452,26 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { let refresh_area = left_layout[3]; let refresh_widget = Paragraph::new(Line::from(vec![ Span::styled("[ ", Style::default().fg(app.theme.gray())), - Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + "-", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" ] ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{}ms", refresh_ms), - Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), + format!("{refresh_ms}ms"), + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), ), Span::styled(" [ ", Style::default().fg(app.theme.gray())), - Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + "+", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" ]", Style::default().fg(app.theme.gray())), ])) .alignment(Alignment::Center) @@ -474,7 +486,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )), ); f.render_widget(refresh_widget, refresh_area); - + // Store clickable region for refresh rate control app.rr_cmap.clear(); app.rr_cmap.push(( @@ -483,4 +495,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { refresh_area.x, refresh_area.x + refresh_area.width, )); -} \ No newline at end of file +} From 5e7b02754606a17bee1094fff7d4b43b2a17e029 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Fri, 27 Feb 2026 23:42:54 +0530 Subject: [PATCH 28/48] FIX: Moved refresh rate counter to the right side for space management --- src/ui/mod.rs | 5 ---- src/ui/widgets/menu.rs | 57 +++++++++++++----------------------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f14a7f5..65dda66 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -212,7 +212,6 @@ fn run_app( if app.context_menu_visible { handle_context_menu_click(app, mouse_event.column, mouse_event.row); } else if app.screen == Screen::MainMenu { - // ═══ HANDLE MAIN MENU CLICKS ═══ let x = mouse_event.column; let y = mouse_event.row; @@ -267,21 +266,17 @@ fn run_app( } } - // Check refresh rate control clicks for (y_start, y_end, x_start, x_end) in &app.rr_cmap { if y >= *y_start && y < *y_end && x >= *x_start && x < *x_end { - // Get the center of the widget to determine left/right side let center_x = (x_start + x_end) / 2; if x < center_x { - // Left side - MINUS button app.rr_ms = (app.rr_ms.saturating_sub(50)).max(50); app.set_msg( &format!("Refresh: {}ms", app.rr_ms), screens::MessageType::Info, ); } else { - // Right side - PLUS button app.rr_ms = (app.rr_ms + 50).min(1000); app.set_msg( &format!("Refresh: {}ms", app.rr_ms), diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index a910b37..1523c6f 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -31,8 +31,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .constraints([ Constraint::Length(3), // title Constraint::Length(sysinfo_height), // system info and crypto - Constraint::Min(20), // menu - Constraint::Length(3), // refresh rate control + Constraint::Min(20), ]) .split(main_layout[0]); @@ -252,28 +251,21 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )); let menu_area = left_layout[2]; - - // Calculate inner area BEFORE moving menu_block + let menu_inner_area = menu_block.inner(menu_area); - + let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); - // ═══ MOUSE CLICK TRACKING FOR MENU ITEMS ═══ - // Store clickable regions for mouse handling - // Each menu item takes 4 lines (title, desc, 2 blank lines), first item has extra blank line at top - let mut current_y = menu_inner_area.y + 1; // Start after the extra blank line before first item - - // Clear previous menu click map + let mut current_y = menu_inner_area.y + 1; + app.menu_click_map.clear(); - + for i in 0..all_items.len() { - let item_height = 4u16; // Each item: title line + desc line + 2 blank lines + let item_height = 4u16; let clickable_y_start = current_y; - let clickable_y_end = current_y + 1; // Title and description lines are clickable - - // Store the clickable region: (y_start, y_end, x_start, x_end, menu_index) - // x_start and x_end define the horizontal bounds within the menu + let clickable_y_end = current_y + 1; + app.menu_click_map.push(( clickable_y_start, clickable_y_end, @@ -281,7 +273,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { menu_inner_area.x + menu_inner_area.width, i, )); - + current_y += item_height; } @@ -291,6 +283,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { Constraint::Length(3), // status msg Constraint::Length(1), // spacer Constraint::Min(10), // pwd stats + Constraint::Length(3), // refresh rate control ]) .split(main_layout[1]); @@ -447,31 +440,18 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { f.render_widget(stats_box, right_layout[2]); } - // Refresh rate control widget let refresh_ms = app.rr_ms; - let refresh_area = left_layout[3]; + let refresh_area = right_layout[3]; let refresh_widget = Paragraph::new(Line::from(vec![ Span::styled("[ ", Style::default().fg(app.theme.gray())), - Span::styled( - "-", - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), + Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), Span::styled(" ] ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{refresh_ms}ms"), - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), + format!("{}ms", refresh_ms), + Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), ), Span::styled(" [ ", Style::default().fg(app.theme.gray())), - Span::styled( - "+", - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), + Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), Span::styled(" ]", Style::default().fg(app.theme.gray())), ])) .alignment(Alignment::Center) @@ -486,8 +466,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )), ); f.render_widget(refresh_widget, refresh_area); - - // Store clickable region for refresh rate control + app.rr_cmap.clear(); app.rr_cmap.push(( refresh_area.y, @@ -495,4 +474,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { refresh_area.x, refresh_area.x + refresh_area.width, )); -} +} \ No newline at end of file From 776bb438402b5f83c0d7aa02189f338e937a1800 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Fri, 27 Feb 2026 23:43:22 +0530 Subject: [PATCH 29/48] FIX: cargo fmt and cargo clippy --- src/ui/widgets/menu.rs | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 1523c6f..05c1ea6 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -251,21 +251,21 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )); let menu_area = left_layout[2]; - + let menu_inner_area = menu_block.inner(menu_area); - + let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); let mut current_y = menu_inner_area.y + 1; - + app.menu_click_map.clear(); - + for i in 0..all_items.len() { let item_height = 4u16; let clickable_y_start = current_y; let clickable_y_end = current_y + 1; - + app.menu_click_map.push(( clickable_y_start, clickable_y_end, @@ -273,7 +273,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { menu_inner_area.x + menu_inner_area.width, i, )); - + current_y += item_height; } @@ -444,14 +444,26 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { let refresh_area = right_layout[3]; let refresh_widget = Paragraph::new(Line::from(vec![ Span::styled("[ ", Style::default().fg(app.theme.gray())), - Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + "-", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" ] ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{}ms", refresh_ms), - Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), + format!("{refresh_ms}ms"), + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), ), Span::styled(" [ ", Style::default().fg(app.theme.gray())), - Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + "+", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" ]", Style::default().fg(app.theme.gray())), ])) .alignment(Alignment::Center) @@ -466,7 +478,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )), ); f.render_widget(refresh_widget, refresh_area); - + app.rr_cmap.clear(); app.rr_cmap.push(( refresh_area.y, @@ -474,4 +486,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { refresh_area.x, refresh_area.x + refresh_area.width, )); -} \ No newline at end of file +} From 125b84bae1186d7ed8f0208c1128d92167ec17f6 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 10:34:53 +0530 Subject: [PATCH 30/48] UPDATE: Added outlier border to Main Menu and shifted refresh counter to border --- src/ui/widgets/menu.rs | 75 +++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 05c1ea6..cab2a2f 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -12,14 +12,16 @@ use ratatui::{ #[allow(clippy::too_many_lines)] pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { - let block = Block::default() - .borders(Borders::NONE) + let outer_block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(app.theme.fg())) .style(Style::default().bg(app.theme.bg0())); - f.render_widget(block, size); + f.render_widget(outer_block, size); let main_layout = Layout::default() .direction(Direction::Horizontal) - .margin(2) + .margin(3) .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) .split(size); @@ -251,21 +253,21 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { )); let menu_area = left_layout[2]; - + let menu_inner_area = menu_block.inner(menu_area); - + let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); let mut current_y = menu_inner_area.y + 1; - + app.menu_click_map.clear(); - + for i in 0..all_items.len() { let item_height = 4u16; let clickable_y_start = current_y; let clickable_y_end = current_y + 1; - + app.menu_click_map.push(( clickable_y_start, clickable_y_end, @@ -273,7 +275,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { menu_inner_area.x + menu_inner_area.width, i, )); - + current_y += item_height; } @@ -283,7 +285,6 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { Constraint::Length(3), // status msg Constraint::Length(1), // spacer Constraint::Min(10), // pwd stats - Constraint::Length(3), // refresh rate control ]) .split(main_layout[1]); @@ -441,44 +442,30 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { } let refresh_ms = app.rr_ms; - let refresh_area = right_layout[3]; + let refresh_text = format!(" [ - ] {}ms [ + ] ", refresh_ms); + let refresh_width = refresh_text.len() as u16; + + let refresh_area = Rect { + x: size.x + size.width - refresh_width - 1, + y: size.y + size.height - 1, + width: refresh_width, + height: 1, + }; + let refresh_widget = Paragraph::new(Line::from(vec![ - Span::styled("[ ", Style::default().fg(app.theme.gray())), - Span::styled( - "-", - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), + Span::styled(" [ ", Style::default().fg(app.theme.gray())), + Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), Span::styled(" ] ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{refresh_ms}ms"), - Style::default() - .fg(app.theme.green()) - .add_modifier(Modifier::BOLD), + format!("{}ms", refresh_ms), + Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), ), Span::styled(" [ ", Style::default().fg(app.theme.gray())), - Span::styled( - "+", - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" ]", Style::default().fg(app.theme.gray())), - ])) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(app.theme.aqua())) - .title(Span::styled( - " Refresh Rate ", - Style::default().fg(app.theme.aqua()), - )), - ); + Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled(" ] ", Style::default().fg(app.theme.gray())), + ])); f.render_widget(refresh_widget, refresh_area); - + app.rr_cmap.clear(); app.rr_cmap.push(( refresh_area.y, @@ -486,4 +473,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { refresh_area.x, refresh_area.x + refresh_area.width, )); -} +} \ No newline at end of file From c683bd078483108e30c2c4181198be7739784c74 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 10:40:23 +0530 Subject: [PATCH 31/48] FIX: Fixed spacing so that all 7 options get space for adjustment --- src/ui/widgets/menu.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index cab2a2f..910703d 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -33,7 +33,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .constraints([ Constraint::Length(3), // title Constraint::Length(sysinfo_height), // system info and crypto - Constraint::Min(20), + Constraint::Min(20), // menu ]) .split(main_layout[0]); @@ -234,7 +234,6 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { Span::styled(*desc, Style::default().fg(app.theme.gray())), ]), Line::from(""), - Line::from(""), ]); ListItem::new(lines) @@ -264,7 +263,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { app.menu_click_map.clear(); for i in 0..all_items.len() { - let item_height = 4u16; + let item_height = 3u16; let clickable_y_start = current_y; let clickable_y_end = current_y + 1; From 9e4091cfe978b16d0700e0abd3dd1d53e7789bce Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 10:56:10 +0530 Subject: [PATCH 32/48] FIX: Added spacing with response --- src/ui/widgets/menu.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index 910703d..dc5ffde 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -22,7 +22,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { let main_layout = Layout::default() .direction(Direction::Horizontal) .margin(3) - .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) + .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) .split(size); let has_vault = app.vault.is_some(); @@ -33,7 +33,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .constraints([ Constraint::Length(3), // title Constraint::Length(sysinfo_height), // system info and crypto - Constraint::Min(20), // menu + Constraint::Min(28), // menu ]) .split(main_layout[0]); @@ -234,6 +234,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { Span::styled(*desc, Style::default().fg(app.theme.gray())), ]), Line::from(""), + Line::from(""), ]); ListItem::new(lines) @@ -263,7 +264,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { app.menu_click_map.clear(); for i in 0..all_items.len() { - let item_height = 3u16; + let item_height = 4u16; let clickable_y_start = current_y; let clickable_y_end = current_y + 1; From e084510047f80f5f6ccc72a8dc3b970f191566ee Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 11:31:19 +0530 Subject: [PATCH 33/48] FIX: Fixed responsiveness and space management globally --- src/ui/widgets/menu.rs | 105 +++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index dc5ffde..af538ca 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -33,7 +33,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .constraints([ Constraint::Length(3), // title Constraint::Length(sysinfo_height), // system info and crypto - Constraint::Min(28), // menu + Constraint::Min(0), // menu ]) .split(main_layout[0]); @@ -194,6 +194,20 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { ("7", "Exit", "Lock vault"), ]; + let menu_area = left_layout[2]; + let avail_h = menu_area.height.saturating_sub(2); + let mlp_item = 2; + let items_count = all_items.len(); + let min_req = (mlp_item * items_count) as u16; + + let spacing = if avail_h >= min_req + (items_count as u16 * 2) + 1 { + 2 + } else if avail_h >= min_req + items_count as u16 + 1 { + 1 + } else { + 0 + }; + let menu_list: Vec = all_items .iter() .enumerate() @@ -214,28 +228,28 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { let mut lines = vec![]; - if i == 0 { + if i == 0 && spacing > 0 { lines.push(Line::from("")); } - lines.extend(vec![ - Line::from(vec![ - Span::styled(prefix, Style::default().fg(app.theme.yellow())), - Span::styled( - format!("[{num}] "), - Style::default() - .fg(app.theme.orange()) - .add_modifier(Modifier::BOLD), - ), - Span::styled(*title, title_style), - ]), - Line::from(vec![ - Span::raw(" "), - Span::styled(*desc, Style::default().fg(app.theme.gray())), - ]), - Line::from(""), - Line::from(""), - ]); + lines.push(Line::from(vec![ + Span::styled(prefix, Style::default().fg(app.theme.yellow())), + Span::styled( + format!("[{num}] "), + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(*title, title_style), + ])); + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled(*desc, Style::default().fg(app.theme.gray())), + ])); + + for _ in 0..spacing { + lines.push(Line::from("")); + } ListItem::new(lines) }) @@ -251,28 +265,29 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .fg(app.theme.green()) .add_modifier(Modifier::BOLD), )); - - let menu_area = left_layout[2]; - let menu_inner_area = menu_block.inner(menu_area); + let menu_ia = menu_block.inner(menu_area); let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); - let mut current_y = menu_inner_area.y + 1; + let mut current_y = menu_ia.y; + if spacing > 0 { + current_y += 1; + } app.menu_click_map.clear(); for i in 0..all_items.len() { - let item_height = 4u16; - let clickable_y_start = current_y; - let clickable_y_end = current_y + 1; + let item_height = (2 + spacing) as u16; + let clicky_start = current_y; + let clicky_end = current_y + 1; app.menu_click_map.push(( - clickable_y_start, - clickable_y_end, - menu_inner_area.x, - menu_inner_area.x + menu_inner_area.width, + clicky_start, + clicky_end, + menu_ia.x, + menu_ia.x + menu_ia.width, i, )); @@ -284,7 +299,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .constraints([ Constraint::Length(3), // status msg Constraint::Length(1), // spacer - Constraint::Min(10), // pwd stats + Constraint::Min(0), // pwd stats ]) .split(main_layout[1]); @@ -441,36 +456,36 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { f.render_widget(stats_box, right_layout[2]); } - let refresh_ms = app.rr_ms; - let refresh_text = format!(" [ - ] {}ms [ + ] ", refresh_ms); - let refresh_width = refresh_text.len() as u16; + let rms = app.rr_ms; + let rtxt = format!(" [ - ] {}ms [ + ] ", rms); + let r_width = rtxt.len() as u16; - let refresh_area = Rect { - x: size.x + size.width - refresh_width - 1, + let r_area = Rect { + x: size.x + size.width - r_width - 1, y: size.y + size.height - 1, - width: refresh_width, + width: r_width, height: 1, }; - let refresh_widget = Paragraph::new(Line::from(vec![ + let r_widget = Paragraph::new(Line::from(vec![ Span::styled(" [ ", Style::default().fg(app.theme.gray())), Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), Span::styled(" ] ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{}ms", refresh_ms), + format!("{}ms", rms), Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), ), Span::styled(" [ ", Style::default().fg(app.theme.gray())), Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), Span::styled(" ] ", Style::default().fg(app.theme.gray())), ])); - f.render_widget(refresh_widget, refresh_area); + f.render_widget(r_widget, r_area); app.rr_cmap.clear(); app.rr_cmap.push(( - refresh_area.y, - refresh_area.y + refresh_area.height, - refresh_area.x, - refresh_area.x + refresh_area.width, + r_area.y, + r_area.y + r_area.height, + r_area.x, + r_area.x + r_area.width, )); } \ No newline at end of file From 36dc430ca93b7c84ed09bf37c4d9794bd5d7befe Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 11:42:08 +0530 Subject: [PATCH 34/48] FIX: cargo fmt and cargo clippy --- src/ui/widgets/menu.rs | 58 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/ui/widgets/menu.rs b/src/ui/widgets/menu.rs index af538ca..bff24f3 100644 --- a/src/ui/widgets/menu.rs +++ b/src/ui/widgets/menu.rs @@ -198,14 +198,12 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { let avail_h = menu_area.height.saturating_sub(2); let mlp_item = 2; let items_count = all_items.len(); - let min_req = (mlp_item * items_count) as u16; - - let spacing = if avail_h >= min_req + (items_count as u16 * 2) + 1 { + let min_req = u16::try_from(mlp_item * items_count).unwrap_or(u16::MAX); + + let spacing = if avail_h > min_req + (u16::try_from(items_count).unwrap_or(0) * 2) { 2 - } else if avail_h >= min_req + items_count as u16 + 1 { - 1 } else { - 0 + i32::from(avail_h > min_req + u16::try_from(items_count).unwrap_or(0)) }; let menu_list: Vec = all_items @@ -246,7 +244,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { Span::raw(" "), Span::styled(*desc, Style::default().fg(app.theme.gray())), ])); - + for _ in 0..spacing { lines.push(Line::from("")); } @@ -265,9 +263,9 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { .fg(app.theme.green()) .add_modifier(Modifier::BOLD), )); - + let menu_ia = menu_block.inner(menu_area); - + let menu = List::new(menu_list).block(menu_block); f.render_widget(menu, menu_area); @@ -275,14 +273,14 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { if spacing > 0 { current_y += 1; } - + app.menu_click_map.clear(); - + for i in 0..all_items.len() { - let item_height = (2 + spacing) as u16; + let item_height = u16::try_from(2 + spacing).unwrap_or(0); let clicky_start = current_y; let clicky_end = current_y + 1; - + app.menu_click_map.push(( clicky_start, clicky_end, @@ -290,7 +288,7 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { menu_ia.x + menu_ia.width, i, )); - + current_y += item_height; } @@ -457,30 +455,42 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { } let rms = app.rr_ms; - let rtxt = format!(" [ - ] {}ms [ + ] ", rms); - let r_width = rtxt.len() as u16; - + let rtxt = format!(" [ - ] {rms}ms [ + ] "); + let r_width = u16::try_from(rtxt.len()).unwrap_or(u16::MAX); + let r_area = Rect { x: size.x + size.width - r_width - 1, y: size.y + size.height - 1, width: r_width, height: 1, }; - + let r_widget = Paragraph::new(Line::from(vec![ Span::styled(" [ ", Style::default().fg(app.theme.gray())), - Span::styled("-", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + "-", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" ] ", Style::default().fg(app.theme.gray())), Span::styled( - format!("{}ms", rms), - Style::default().fg(app.theme.green()).add_modifier(Modifier::BOLD), + format!("{rms}ms"), + Style::default() + .fg(app.theme.green()) + .add_modifier(Modifier::BOLD), ), Span::styled(" [ ", Style::default().fg(app.theme.gray())), - Span::styled("+", Style::default().fg(app.theme.orange()).add_modifier(Modifier::BOLD)), + Span::styled( + "+", + Style::default() + .fg(app.theme.orange()) + .add_modifier(Modifier::BOLD), + ), Span::styled(" ] ", Style::default().fg(app.theme.gray())), ])); f.render_widget(r_widget, r_area); - + app.rr_cmap.clear(); app.rr_cmap.push(( r_area.y, @@ -488,4 +498,4 @@ pub fn draw_main_menu(f: &mut Frame, size: Rect, app: &mut App) { r_area.x, r_area.x + r_area.width, )); -} \ No newline at end of file +} From 8d9aa43cae1977ee040bcac3fd573b5919dcd5d5 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 17:22:15 +0530 Subject: [PATCH 35/48] UPDATE: Working on major updates - Password import - Password Export - Vault import - Vault export - Vault backup - Time based backup --- Cargo.lock | 173 ++++++++++++++++++ Cargo.toml | 1 + src/backup.rs | 489 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 246 +++++++++++++++++++++++-- 4 files changed, 899 insertions(+), 10 deletions(-) create mode 100644 src/backup.rs diff --git a/Cargo.lock b/Cargo.lock index c1801bf..319219e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arboard" version = "3.6.1" @@ -61,6 +70,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytemuck" version = "1.25.0" @@ -104,6 +119,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -126,6 +154,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -380,6 +414,30 @@ dependencies = [ "digest", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "image" version = "0.25.9" @@ -418,6 +476,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.182" @@ -584,6 +652,12 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "option-ext" version = "0.2.0" @@ -620,6 +694,7 @@ dependencies = [ "arboard", "base32", "cc", + "chrono", "crossterm", "dirs", "hex", @@ -1061,6 +1136,51 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + [[package]] name = "weezl" version = "0.1.12" @@ -1089,12 +1209,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 2dd0f09..63deb78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ hex = "0.4" arboard = "3.3" totp-lite = "2.0" base32 = "0.4" +chrono = "0.4" [build-dependencies] cc = "1.0" diff --git a/src/backup.rs b/src/backup.rs new file mode 100644 index 0000000..a2bfba8 --- /dev/null +++ b/src/backup.rs @@ -0,0 +1,489 @@ +use crate::storage; +use std::fs; +use std::path::PathBuf; + +/// Gets the backup directory path: ~/.passlock/backups/ +/// This is where all automatic backups will be stored +pub fn get_backup_dir() -> PathBuf { + let home = dirs::home_dir().expect("Could not find home directory"); + home.join(".passlock").join("backups") +} + +/// Gets the vault-specific backup directory +/// If you have multiple vaults later, each gets its own backup folder +pub fn get_vault_backup_dir(vault_name: &str) -> PathBuf { + get_backup_dir().join(vault_name) +} + +/// Initialize backup directory structure +/// Creates the folders if they don't exist +pub fn init_backup_system() -> Result<(), Box> { + let backup_dir = get_backup_dir(); + + if !backup_dir.exists() { + fs::create_dir_all(&backup_dir)?; + println!("[✔] Created backup directory: {}", backup_dir.display()); + } + + let default_backup_dir = get_vault_backup_dir("default"); + if !default_backup_dir.exists() { + fs::create_dir_all(&default_backup_dir)?; + println!("[✔] Created vault backup directory: {}", default_backup_dir.display()); + } + + Ok(()) +} + +/// Create a timestamped backup of the vault +/// Format: backup_2026-02-28_18-30-45.vault +/// +/// This function: +/// 1. Gets current timestamp +/// 2. Creates a filename with that timestamp +/// 3. Copies the vault file to the backup directory +/// 4. Manages old backups (keeps only last N backups) +pub fn create_backup(vault_name: &str, max_backups: usize) -> Result> { + let home = dirs::home_dir().expect("Could not find home directory"); + let vault_path = home.join(".passlock.vault"); + + if !vault_path.exists() { + return Err("No vault file found to backup".into()); + } + + use chrono::Local; + let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); + let backup_filename = format!("backup_{}.vault", timestamp); + + let backup_dir = get_vault_backup_dir(vault_name); + fs::create_dir_all(&backup_dir)?; + + let backup_path = backup_dir.join(&backup_filename); + + fs::copy(&vault_path, &backup_path)?; + + println!("[✔] Backup created: {}", backup_filename); + + cleanup_old_backups(vault_name, max_backups)?; + + Ok(backup_filename) +} + +/// List all backups for a vault +/// Returns a vector of (filename, file_size, created_time) +pub fn list_backups(vault_name: &str) -> Result, Box> { + let backup_dir = get_vault_backup_dir(vault_name); + + if !backup_dir.exists() { + return Ok(Vec::new()); + } + + let mut backups = Vec::new(); + + for entry in fs::read_dir(backup_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("vault") { + let metadata = fs::metadata(&path)?; + let filename = path.file_name().unwrap().to_string_lossy().to_string(); + let size = metadata.len(); + + use chrono::{DateTime, Local}; + let created = metadata.created()?; + let datetime: DateTime = created.into(); + let created_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); + + backups.push((filename, size, created_str)); + } + } + + backups.sort_by(|a, b| b.0.cmp(&a.0)); + + Ok(backups) +} + +/// Restore vault from a backup +/// +/// This function: +/// 1. Finds the backup file +/// 2. Verifies it can be unlocked with the provided password +/// 3. Copies it back to the main vault location +pub fn restore_backup(vault_name: &str, backup_filename: &str, password: &str) -> Result<(), Box> { + let backup_dir = get_vault_backup_dir(vault_name); + let backup_path = backup_dir.join(backup_filename); + + if !backup_path.exists() { + return Err(format!("Backup file not found: {}", backup_filename).into()); + } + + println!("[...] Verifying backup with password..."); + + let home = dirs::home_dir().expect("Could not find home directory"); + let test_path = home.join(".passlock.vault.test"); + fs::copy(&backup_path, &test_path)?; + + match storage::ld_vt(password) { + Ok(_) => { + fs::remove_file(&test_path)?; + + let vault_path = home.join(".passlock.vault"); + + if vault_path.exists() { + let safety_backup = home.join(".passlock.vault.before_restore"); + fs::copy(&vault_path, &safety_backup)?; + println!("[✔] Current vault backed up to: .passlock.vault.before_restore"); + } + + fs::copy(&backup_path, &vault_path)?; + println!("[✔] Vault restored from backup: {}", backup_filename); + + Ok(()) + } + Err(_) => { + fs::remove_file(&test_path)?; + Err("Incorrect password for this backup".into()) + } + } +} + +/// Clean up old backups, keeping only the most recent N backups +fn cleanup_old_backups(vault_name: &str, max_backups: usize) -> Result<(), Box> { + let backups = list_backups(vault_name)?; + + if backups.len() > max_backups { + let backup_dir = get_vault_backup_dir(vault_name); + let to_delete = &backups[max_backups..]; + + for (filename, _, _) in to_delete { + let backup_path = backup_dir.join(filename); + fs::remove_file(&backup_path)?; + println!("[✔] Removed old backup: {}", filename); + } + } + + Ok(()) +} + +/// Export vault to a specific location (encrypted PassLock format) +/// Useful for manual backups or transferring between systems +pub fn export_vault(password: &str, output_path: &str) -> Result<(), Box> { + let _vault = storage::ld_vt(password)?; + + let home = dirs::home_dir().expect("Could not find home directory"); + let vault_path = home.join(".passlock.vault"); + + if !vault_path.exists() { + return Err("No vault file found to export".into()); + } + + fs::copy(&vault_path, output_path)?; + + println!("[✔] Vault exported to: {}", output_path); + println!("[✔] Format: Encrypted PassLock vault"); + println!("[!] Keep this file secure - it contains all your passwords!"); + + Ok(()) +} + +/// Export vault to CSV format (PLAINTEXT - for importing to other password managers) +/// WARNING: This creates an UNENCRYPTED file with all passwords visible! +pub fn export_to_csv(password: &str, output_path: &str) -> Result<(), Box> { + let vault = storage::ld_vt(password)?; + + let mut csv_content = String::from("name,username,password,url,notes,tags,2fa_secret\n"); + + for entry in &vault.e { + let name = escape_csv(&entry.n); + let username = escape_csv(&entry.u); + let password = escape_csv(&entry.p); + let url = entry.url.as_ref().map_or(String::new(), |u| escape_csv(u)); + let notes = entry.nt.as_ref().map_or(String::new(), |n| escape_csv(n)); + let tags = escape_csv(&entry.tags.join(";")); + let totp = entry.totp_secret.as_ref().map_or(String::new(), |t| escape_csv(t)); + + csv_content.push_str(&format!( + "{},{},{},{},{},{},{}\n", + name, username, password, url, notes, tags, totp + )); + } + + fs::write(output_path, csv_content)?; + + println!("[✔] Vault exported to CSV: {}", output_path); + println!("[!] WARNING: This file is UNENCRYPTED and contains plaintext passwords!"); + println!("[!] Use this for importing to other password managers, then DELETE it!"); + println!("[✔] Total entries exported: {}", vault.e.len()); + + Ok(()) +} + +/// Export vault to JSON format (PLAINTEXT - for importing to other password managers) +/// WARNING: This creates an UNENCRYPTED file with all passwords visible! +pub fn export_to_json(password: &str, output_path: &str) -> Result<(), Box> { + let vault = storage::ld_vt(password)?; + + let mut entries = Vec::new(); + for entry in &vault.e { + let mut json_entry = serde_json::json!({ + "name": entry.n, + "username": entry.u, + "password": entry.p, + "tags": entry.tags, + }); + + if let Some(ref url) = entry.url { + json_entry["url"] = serde_json::json!(url); + } + + if let Some(ref notes) = entry.nt { + json_entry["notes"] = serde_json::json!(notes); + } + + if let Some(ref totp) = entry.totp_secret { + json_entry["totp_secret"] = serde_json::json!(totp); + } + + entries.push(json_entry); + } + + let json_output = serde_json::json!({ + "passlock_version": env!("CARGO_PKG_VERSION"), + "exported_at": chrono::Local::now().to_rfc3339(), + "entries": entries, + }); + + let json_string = serde_json::to_string_pretty(&json_output)?; + fs::write(output_path, json_string)?; + + println!("[✔] Vault exported to JSON: {}", output_path); + println!("[!] WARNING: This file is UNENCRYPTED and contains plaintext passwords!"); + println!("[!] Use this for importing to other password managers, then DELETE it!"); + println!("[✔] Total entries exported: {}", vault.e.len()); + + Ok(()) +} + +/// Helper function to escape CSV fields +fn escape_csv(field: &str) -> String { + if field.contains(',') || field.contains('"') || field.contains('\n') { + format!("\"{}\"", field.replace('"', "\"\"")) + } else { + field.to_string() + } +} + +/// Import from CSV file (from other password managers like LastPass, Bitwarden, etc.) +/// Expected format: name,username,password,url,notes,tags,2fa_secret +pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box> { + use crate::models::Entry; + use std::io::{BufRead, BufReader}; + + let mut vault = storage::ld_vt(password)?; + + let file = fs::File::open(input_path)?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + if let Some(Ok(header)) = lines.next() { + if !header.contains("name") || !header.contains("password") { + return Err("Invalid CSV format - expected header with 'name' and 'password'".into()); + } + } else { + return Err("Empty CSV file".into()); + } + + let mut imported_count = 0; + let mut skipped_count = 0; + + for line_result in lines { + let line = line_result?; + if line.trim().is_empty() { + continue; + } + + let fields = parse_csv_line(&line); + + if fields.len() < 3 { + println!("[!] Skipping invalid line: {}", line); + skipped_count += 1; + continue; + } + + let name = fields[0].clone(); + let username = fields.get(1).cloned().unwrap_or_default(); + let password_val = fields.get(2).cloned().unwrap_or_default(); + let url = fields.get(3).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }); + let notes = fields.get(4).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }); + let tags_str = fields.get(5).cloned().unwrap_or_default(); + let totp_secret = fields.get(6).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }); + + let tags: Vec = tags_str + .split(';') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + let entry = Entry { + id: crate::generate_uuid(), + n: name, + u: username, + p: password_val, + url, + nt: notes, + tags, + totp_secret, + t: crate::get_timestamp(), + last_modified: crate::get_timestamp(), + history: Vec::new(), + }; + + vault.e.push(entry); + imported_count += 1; + } + + storage::svv(&vault, password)?; + + println!("[✔] Import completed!"); + println!("[✔] Imported: {} entries", imported_count); + if skipped_count > 0 { + println!("[!] Skipped: {} invalid entries", skipped_count); + } + println!("[✔] Total entries in vault: {}", vault.e.len()); + + create_backup("default", 10)?; + + Ok(()) +} + +pub fn import_from_json(password: &str, input_path: &str) -> Result<(), Box> { + use crate::models::Entry; + + let mut vault = storage::ld_vt(password)?; + + let json_content = fs::read_to_string(input_path)?; + let json: serde_json::Value = serde_json::from_str(&json_content)?; + + let entries = match json.get("entries") { + Some(serde_json::Value::Array(arr)) => arr, + _ => return Err("Invalid JSON format - expected 'entries' array".into()), + }; + + let mut imported_count = 0; + + for entry_json in entries { + let name = entry_json["name"].as_str().unwrap_or("Untitled").to_string(); + let username = entry_json["username"].as_str().unwrap_or("").to_string(); + let password_val = entry_json["password"].as_str().unwrap_or("").to_string(); + let url = entry_json.get("url").and_then(|v| v.as_str()).map(String::from); + let notes = entry_json.get("notes").and_then(|v| v.as_str()).map(String::from); + let totp_secret = entry_json.get("totp_secret").and_then(|v| v.as_str()).map(String::from); + + let tags = match entry_json.get("tags") { + Some(serde_json::Value::Array(arr)) => { + arr.iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect() + } + _ => Vec::new(), + }; + + let entry = Entry { + id: crate::generate_uuid(), + n: name, + u: username, + p: password_val, + url, + nt: notes, + tags, + totp_secret, + t: crate::get_timestamp(), + last_modified: crate::get_timestamp(), + history: Vec::new(), + }; + + vault.e.push(entry); + imported_count += 1; + } + + storage::svv(&vault, password)?; + + println!("[✔] Import completed!"); + println!("[✔] Imported: {} entries", imported_count); + println!("[✔] Total entries in vault: {}", vault.e.len()); + + create_backup("default", 10)?; + + Ok(()) +} + +/// Simple CSV line parser that handles quoted fields +fn parse_csv_line(line: &str) -> Vec { + let mut fields = Vec::new(); + let mut current_field = String::new(); + let mut in_quotes = false; + let mut chars = line.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + '"' => { + if in_quotes && chars.peek() == Some(&'"') { + current_field.push('"'); + chars.next(); + } else { + in_quotes = !in_quotes; + } + } + ',' if !in_quotes => { + fields.push(current_field.trim().to_string()); + current_field.clear(); + } + _ => { + current_field.push(ch); + } + } + } + + fields.push(current_field.trim().to_string()); + + fields +} + +/// Import vault from a specific location +/// Replaces current vault with the imported one +pub fn import_vault(password: &str, input_path: &str) -> Result<(), Box> { + let import_path = PathBuf::from(input_path); + + if !import_path.exists() { + return Err(format!("Import file not found: {}", input_path).into()); + } + + let home = dirs::home_dir().expect("Could not find home directory"); + let temp_path = home.join(".passlock.vault.import_test"); + fs::copy(&import_path, &temp_path)?; + + println!("[...] Verifying import file with password..."); + + match storage::ld_vt(password) { + Ok(_) => { + fs::remove_file(&temp_path)?; + + let vault_path = home.join(".passlock.vault"); + + if vault_path.exists() { + let safety_backup = home.join(".passlock.vault.before_import"); + fs::copy(&vault_path, &safety_backup)?; + println!("[✔] Current vault backed up to: .passlock.vault.before_import"); + } + + fs::copy(&import_path, &vault_path)?; + println!("[✔] Vault imported from: {}", input_path); + + Ok(()) + } + Err(_) => { + fs::remove_file(&temp_path)?; + Err("Incorrect password for this vault file".into()) + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fa64709..81a64e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod backup; mod crypto; mod models; mod storage; @@ -37,6 +38,8 @@ pub fn get_timestamp() -> u64 { fn main() -> Result<(), Box> { crypto::init_crypto()?; + + backup::init_backup_system()?; let args: Vec = env::args().collect(); @@ -66,6 +69,101 @@ fn main() -> Result<(), Box> { let password = &args[2]; sync_vault(password)?; } + "backup" => { + handle_backup_cmd(&args[2..])?; + } + "export" => { + if args.len() < 4 { + eprintln!("Usage: passlock export "); + eprintln!(); + eprintln!("Export encrypted vault for backup/transfer."); + eprintln!("Works with ANY path on ANY device!"); + eprintln!(); + eprintln!("Examples:"); + eprintln!(" passlock export myPass123 ~/Documents/backup.vault"); + eprintln!(" passlock export myPass123 /media/usb/backup.vault"); + eprintln!(" passlock export myPass123 ~/Dropbox/backup.vault"); + std::process::exit(1); + } + let password = &args[2]; + let output_file = &args[3]; + backup::export_vault(password, output_file)?; + } + "import" => { + if args.len() < 4 { + eprintln!("Usage: passlock import "); + eprintln!(); + eprintln!("Import encrypted vault from backup."); + eprintln!("Works with ANY path on ANY device!"); + eprintln!(); + eprintln!("Examples:"); + eprintln!(" passlock import myPass123 ~/Documents/backup.vault"); + eprintln!(" passlock import myPass123 /media/usb/backup.vault"); + std::process::exit(1); + } + let password = &args[2]; + let input_file = &args[3]; + backup::import_vault(password, input_file)?; + } + "export-csv" => { + if args.len() < 4 { + eprintln!("Usage: passlock export-csv "); + eprintln!(); + eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); + eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); + std::process::exit(1); + } + let password = &args[2]; + let output_file = &args[3]; + backup::export_to_csv(password, output_file)?; + } + "export-json" => { + if args.len() < 4 { + eprintln!("Usage: passlock export-json "); + eprintln!(); + eprintln!("WARNING: Creates UNENCRYPTED JSON with plaintext passwords!"); + eprintln!("Use for importing to other password managers."); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock export-json myPass123 ~/passwords.json"); + std::process::exit(1); + } + let password = &args[2]; + let output_file = &args[3]; + backup::export_to_json(password, output_file)?; + } + "import-csv" => { + if args.len() < 4 { + eprintln!("Usage: passlock import-csv "); + eprintln!(); + eprintln!("Import passwords from CSV file (LastPass, Bitwarden, etc.)"); + eprintln!("Expected format: name,username,password,url,notes,tags,2fa_secret"); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock import-csv myPass123 ~/lastpass-export.csv"); + std::process::exit(1); + } + let password = &args[2]; + let input_file = &args[3]; + backup::import_from_csv(password, input_file)?; + } + "import-json" => { + if args.len() < 4 { + eprintln!("Usage: passlock import-json "); + eprintln!(); + eprintln!("Import passwords from JSON file."); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock import-json myPass123 ~/passwords.json"); + std::process::exit(1); + } + let password = &args[2]; + let input_file = &args[3]; + backup::import_from_json(password, input_file)?; + } "info" => { handle_icmd(&args[2..])?; } @@ -105,6 +203,10 @@ fn create_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault created successfully."); + + println!("[...] Creating initial backup..."); + backup::create_backup("default", 10)?; + Ok(()) } @@ -129,6 +231,98 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); + + println!("[...] Creating backup..."); + backup::create_backup("default", 10)?; + + Ok(()) +} + +fn handle_backup_cmd(args: &[String]) -> Result<(), Box> { + if args.is_empty() { + println!("Backup Commands:"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!(); + println!("Usage: passlock backup "); + println!(); + println!("Subcommands:"); + println!(" create Create manual backup"); + println!(" list List all backups"); + println!(" restore Restore from backup"); + println!(); + println!("Examples:"); + println!(" passlock backup create myPassword123"); + println!(" passlock backup list"); + println!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); + return Ok(()); + } + + match args[0].as_str() { + "create" => { + if args.len() < 2 { + eprintln!("Usage: passlock backup create "); + std::process::exit(1); + } + let password = &args[1]; + + let _vault = storage::ld_vt(password)?; + println!("[✔] Password verified"); + + backup::create_backup("default", 10)?; + println!("[✔] Manual backup created successfully"); + } + "list" => { + let backups = backup::list_backups("default")?; + + if backups.is_empty() { + println!("[!] No backups found"); + println!(); + println!("Create your first backup with:"); + println!(" passlock backup create "); + } else { + println!("Available Backups:"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!(); + println!("{:<40} {:<12} {}", "Filename", "Size", "Created"); + println!("{}", "─".repeat(70)); + + for (filename, size, created) in &backups { + let size_kb = if *size > 1024 { + format!("{} KB", *size / 1024) + } else { + format!("{} bytes", *size) + }; + println!("{:<40} {:<12} {}", filename, size_kb, created); + } + + println!(); + println!("Total backups: {}", backups.len()); + println!(); + println!("Restore a backup with:"); + println!(" passlock backup restore "); + } + } + "restore" => { + if args.len() < 3 { + eprintln!("Usage: passlock backup restore "); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); + std::process::exit(1); + } + let backup_name = &args[1]; + let password = &args[2]; + + backup::restore_backup("default", backup_name, password)?; + println!("[✔] Backup restored successfully"); + } + _ => { + println!("Unknown backup subcommand: {}", args[0]); + println!(); + println!("Available subcommands: create, list, restore"); + } + } + Ok(()) } @@ -170,6 +364,9 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } + + let backups = backup::list_backups("default").unwrap_or_else(|_| Vec::new()); + println!(" Backups: {} available", backups.len()); } else { println!("Vault Status:"); println!(" No vault found"); @@ -195,19 +392,48 @@ fn print_usage() { println!("USAGE:"); println!(" passlock [OPTIONS]"); println!(); - println!("COMMANDS:"); - println!(" create Create a new vault"); - println!(" unlock Unlock and verify vault"); - println!(" sync Sync vault changes"); - println!(" tui Launch TUI interface"); - println!(" info [cpu] Show system information"); - println!(" version Show version"); - println!(" help Show this help"); + println!("VAULT COMMANDS:"); + println!(" create Create a new vault"); + println!(" unlock Unlock and verify vault"); + println!(" sync Sync vault changes"); + println!(" tui Launch TUI interface"); + println!(); + println!("BACKUP COMMANDS (Encrypted):"); + println!(" backup create Create manual backup"); + println!(" backup list List all backups"); + println!(" backup restore Restore from backup"); + println!(); + println!(" export Export vault (encrypted)"); + println!(" import Import vault (encrypted)"); + println!(); + println!("PASSWORD MANAGER MIGRATION (Plaintext - USE WITH CAUTION):"); + println!(" export-csv Export to CSV (for other PMs)"); + println!(" export-json Export to JSON (for other PMs)"); + println!(" import-csv Import from CSV"); + println!(" import-json Import from JSON"); + println!(); + println!("INFO COMMANDS:"); + println!(" info [cpu] Show system information"); + println!(" version Show version"); + println!(" help Show this help"); println!(); println!("EXAMPLES:"); + println!(); + println!(" # Create and use vault"); println!(" passlock create mySecurePassword123"); println!(" passlock tui"); - println!(" passlock info cpu"); + println!(); + println!(" # Backup to any location (works everywhere!)"); + println!(" passlock export myPass123 ~/Documents/backup.vault"); + println!(" passlock export myPass123 /media/usb/backup.vault"); + println!(" passlock export myPass123 ~/Dropbox/backup.vault"); + println!(); + println!(" # Migrate from LastPass/Bitwarden"); + println!(" passlock import-csv myPass123 ~/lastpass-export.csv"); + println!(); + println!(" # Migrate to another password manager"); + println!(" passlock export-csv myPass123 ~/passwords.csv"); + println!(" # (Then import into other PM and DELETE the CSV!)"); println!(); println!("For more info: https://github.com/hachimamma/Passlock"); -} +} \ No newline at end of file From d0d43160ad608216509e08a9984663225eb99ce7 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 17:23:32 +0530 Subject: [PATCH 36/48] FIX: cargo fmt --- src/backup.rs | 353 +++++++++++++++++++++++++++----------------------- src/main.rs | 40 +++--- 2 files changed, 214 insertions(+), 179 deletions(-) diff --git a/src/backup.rs b/src/backup.rs index a2bfba8..07e0ad1 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -19,169 +19,181 @@ pub fn get_vault_backup_dir(vault_name: &str) -> PathBuf { /// Creates the folders if they don't exist pub fn init_backup_system() -> Result<(), Box> { let backup_dir = get_backup_dir(); - + if !backup_dir.exists() { fs::create_dir_all(&backup_dir)?; println!("[✔] Created backup directory: {}", backup_dir.display()); } - + let default_backup_dir = get_vault_backup_dir("default"); if !default_backup_dir.exists() { fs::create_dir_all(&default_backup_dir)?; - println!("[✔] Created vault backup directory: {}", default_backup_dir.display()); + println!( + "[✔] Created vault backup directory: {}", + default_backup_dir.display() + ); } - + Ok(()) } /// Create a timestamped backup of the vault /// Format: backup_2026-02-28_18-30-45.vault -/// +/// /// This function: /// 1. Gets current timestamp /// 2. Creates a filename with that timestamp /// 3. Copies the vault file to the backup directory /// 4. Manages old backups (keeps only last N backups) -pub fn create_backup(vault_name: &str, max_backups: usize) -> Result> { +pub fn create_backup( + vault_name: &str, + max_backups: usize, +) -> Result> { let home = dirs::home_dir().expect("Could not find home directory"); let vault_path = home.join(".passlock.vault"); - + if !vault_path.exists() { return Err("No vault file found to backup".into()); } - + use chrono::Local; let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); - let backup_filename = format!("backup_{}.vault", timestamp); - + let backup_filename = format!("backup_{timestamp}.vault"); + let backup_dir = get_vault_backup_dir(vault_name); fs::create_dir_all(&backup_dir)?; - + let backup_path = backup_dir.join(&backup_filename); - + fs::copy(&vault_path, &backup_path)?; - - println!("[✔] Backup created: {}", backup_filename); - + + println!("[✔] Backup created: {backup_filename}"); + cleanup_old_backups(vault_name, max_backups)?; - + Ok(backup_filename) } /// List all backups for a vault -/// Returns a vector of (filename, file_size, created_time) -pub fn list_backups(vault_name: &str) -> Result, Box> { +/// Returns a vector of (filename, `file_size`, `created_time`) +pub fn list_backups( + vault_name: &str, +) -> Result, Box> { let backup_dir = get_vault_backup_dir(vault_name); - + if !backup_dir.exists() { return Ok(Vec::new()); } - + let mut backups = Vec::new(); - + for entry in fs::read_dir(backup_dir)? { let entry = entry?; let path = entry.path(); - + if path.extension().and_then(|s| s.to_str()) == Some("vault") { let metadata = fs::metadata(&path)?; let filename = path.file_name().unwrap().to_string_lossy().to_string(); let size = metadata.len(); - + use chrono::{DateTime, Local}; let created = metadata.created()?; let datetime: DateTime = created.into(); let created_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); - + backups.push((filename, size, created_str)); } } - + backups.sort_by(|a, b| b.0.cmp(&a.0)); - + Ok(backups) } /// Restore vault from a backup -/// +/// /// This function: /// 1. Finds the backup file /// 2. Verifies it can be unlocked with the provided password /// 3. Copies it back to the main vault location -pub fn restore_backup(vault_name: &str, backup_filename: &str, password: &str) -> Result<(), Box> { +pub fn restore_backup( + vault_name: &str, + backup_filename: &str, + password: &str, +) -> Result<(), Box> { let backup_dir = get_vault_backup_dir(vault_name); let backup_path = backup_dir.join(backup_filename); - + if !backup_path.exists() { - return Err(format!("Backup file not found: {}", backup_filename).into()); + return Err(format!("Backup file not found: {backup_filename}").into()); } - + println!("[...] Verifying backup with password..."); - + let home = dirs::home_dir().expect("Could not find home directory"); let test_path = home.join(".passlock.vault.test"); fs::copy(&backup_path, &test_path)?; - - match storage::ld_vt(password) { - Ok(_) => { - fs::remove_file(&test_path)?; - - let vault_path = home.join(".passlock.vault"); - - if vault_path.exists() { - let safety_backup = home.join(".passlock.vault.before_restore"); - fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: .passlock.vault.before_restore"); - } - - fs::copy(&backup_path, &vault_path)?; - println!("[✔] Vault restored from backup: {}", backup_filename); - - Ok(()) - } - Err(_) => { - fs::remove_file(&test_path)?; - Err("Incorrect password for this backup".into()) + + if storage::ld_vt(password).is_ok() { + fs::remove_file(&test_path)?; + + let vault_path = home.join(".passlock.vault"); + + if vault_path.exists() { + let safety_backup = home.join(".passlock.vault.before_restore"); + fs::copy(&vault_path, &safety_backup)?; + println!("[✔] Current vault backed up to: .passlock.vault.before_restore"); } + + fs::copy(&backup_path, &vault_path)?; + println!("[✔] Vault restored from backup: {backup_filename}"); + + Ok(()) + } else { + fs::remove_file(&test_path)?; + Err("Incorrect password for this backup".into()) } } /// Clean up old backups, keeping only the most recent N backups -fn cleanup_old_backups(vault_name: &str, max_backups: usize) -> Result<(), Box> { +fn cleanup_old_backups( + vault_name: &str, + max_backups: usize, +) -> Result<(), Box> { let backups = list_backups(vault_name)?; - + if backups.len() > max_backups { let backup_dir = get_vault_backup_dir(vault_name); let to_delete = &backups[max_backups..]; - + for (filename, _, _) in to_delete { let backup_path = backup_dir.join(filename); fs::remove_file(&backup_path)?; - println!("[✔] Removed old backup: {}", filename); + println!("[✔] Removed old backup: {filename}"); } } - + Ok(()) } -/// Export vault to a specific location (encrypted PassLock format) +/// Export vault to a specific location (encrypted `PassLock` format) /// Useful for manual backups or transferring between systems pub fn export_vault(password: &str, output_path: &str) -> Result<(), Box> { let _vault = storage::ld_vt(password)?; - + let home = dirs::home_dir().expect("Could not find home directory"); let vault_path = home.join(".passlock.vault"); - + if !vault_path.exists() { return Err("No vault file found to export".into()); } - + fs::copy(&vault_path, output_path)?; - - println!("[✔] Vault exported to: {}", output_path); + + println!("[✔] Vault exported to: {output_path}"); println!("[✔] Format: Encrypted PassLock vault"); println!("[!] Keep this file secure - it contains all your passwords!"); - + Ok(()) } @@ -189,9 +201,9 @@ pub fn export_vault(password: &str, output_path: &str) -> Result<(), Box Result<(), Box> { let vault = storage::ld_vt(password)?; - + let mut csv_content = String::from("name,username,password,url,notes,tags,2fa_secret\n"); - + for entry in &vault.e { let name = escape_csv(&entry.n); let username = escape_csv(&entry.u); @@ -199,21 +211,23 @@ pub fn export_to_csv(password: &str, output_path: &str) -> Result<(), Box Result<(), Box Result<(), Box> { let vault = storage::ld_vt(password)?; - + let mut entries = Vec::new(); for entry in &vault.e { let mut json_entry = serde_json::json!({ @@ -230,36 +244,36 @@ pub fn export_to_json(password: &str, output_path: &str) -> Result<(), Box String { } } -/// Import from CSV file (from other password managers like LastPass, Bitwarden, etc.) -/// Expected format: name,username,password,url,notes,tags,2fa_secret +/// Import from CSV file (from other password managers like `LastPass`, Bitwarden, etc.) +/// Expected format: `name,username,password,url,notes,tags,2fa_secret` pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box> { use crate::models::Entry; use std::io::{BufRead, BufReader}; - + let mut vault = storage::ld_vt(password)?; - + let file = fs::File::open(input_path)?; let reader = BufReader::new(file); let mut lines = reader.lines(); - + if let Some(Ok(header)) = lines.next() { if !header.contains("name") || !header.contains("password") { return Err("Invalid CSV format - expected header with 'name' and 'password'".into()); @@ -291,38 +305,44 @@ pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box = tags_str .split(';') .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) + .map(std::string::ToString::to_string) .collect(); - + let entry = Entry { id: crate::generate_uuid(), n: name, @@ -336,58 +356,72 @@ pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box 0 { - println!("[!] Skipped: {} invalid entries", skipped_count); + println!("[!] Skipped: {skipped_count} invalid entries"); } println!("[✔] Total entries in vault: {}", vault.e.len()); - + create_backup("default", 10)?; - + Ok(()) } -pub fn import_from_json(password: &str, input_path: &str) -> Result<(), Box> { +pub fn import_from_json( + password: &str, + input_path: &str, +) -> Result<(), Box> { use crate::models::Entry; - + let mut vault = storage::ld_vt(password)?; - + let json_content = fs::read_to_string(input_path)?; let json: serde_json::Value = serde_json::from_str(&json_content)?; - + let entries = match json.get("entries") { Some(serde_json::Value::Array(arr)) => arr, _ => return Err("Invalid JSON format - expected 'entries' array".into()), }; - + let mut imported_count = 0; - + for entry_json in entries { - let name = entry_json["name"].as_str().unwrap_or("Untitled").to_string(); + let name = entry_json["name"] + .as_str() + .unwrap_or("Untitled") + .to_string(); let username = entry_json["username"].as_str().unwrap_or("").to_string(); let password_val = entry_json["password"].as_str().unwrap_or("").to_string(); - let url = entry_json.get("url").and_then(|v| v.as_str()).map(String::from); - let notes = entry_json.get("notes").and_then(|v| v.as_str()).map(String::from); - let totp_secret = entry_json.get("totp_secret").and_then(|v| v.as_str()).map(String::from); - + let url = entry_json + .get("url") + .and_then(|v| v.as_str()) + .map(String::from); + let notes = entry_json + .get("notes") + .and_then(|v| v.as_str()) + .map(String::from); + let totp_secret = entry_json + .get("totp_secret") + .and_then(|v| v.as_str()) + .map(String::from); + let tags = match entry_json.get("tags") { - Some(serde_json::Value::Array(arr)) => { - arr.iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect() - } + Some(serde_json::Value::Array(arr)) => arr + .iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect(), _ => Vec::new(), }; - + let entry = Entry { id: crate::generate_uuid(), n: name, @@ -401,19 +435,19 @@ pub fn import_from_json(password: &str, input_path: &str) -> Result<(), Box Vec { let mut current_field = String::new(); let mut in_quotes = false; let mut chars = line.chars().peekable(); - + while let Some(ch) = chars.next() { match ch { '"' => { @@ -443,9 +477,9 @@ fn parse_csv_line(line: &str) -> Vec { } } } - + fields.push(current_field.trim().to_string()); - + fields } @@ -453,37 +487,34 @@ fn parse_csv_line(line: &str) -> Vec { /// Replaces current vault with the imported one pub fn import_vault(password: &str, input_path: &str) -> Result<(), Box> { let import_path = PathBuf::from(input_path); - + if !import_path.exists() { - return Err(format!("Import file not found: {}", input_path).into()); + return Err(format!("Import file not found: {input_path}").into()); } - + let home = dirs::home_dir().expect("Could not find home directory"); let temp_path = home.join(".passlock.vault.import_test"); fs::copy(&import_path, &temp_path)?; - + println!("[...] Verifying import file with password..."); - - match storage::ld_vt(password) { - Ok(_) => { - fs::remove_file(&temp_path)?; - - let vault_path = home.join(".passlock.vault"); - - if vault_path.exists() { - let safety_backup = home.join(".passlock.vault.before_import"); - fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: .passlock.vault.before_import"); - } - - fs::copy(&import_path, &vault_path)?; - println!("[✔] Vault imported from: {}", input_path); - - Ok(()) - } - Err(_) => { - fs::remove_file(&temp_path)?; - Err("Incorrect password for this vault file".into()) + + if storage::ld_vt(password).is_ok() { + fs::remove_file(&temp_path)?; + + let vault_path = home.join(".passlock.vault"); + + if vault_path.exists() { + let safety_backup = home.join(".passlock.vault.before_import"); + fs::copy(&vault_path, &safety_backup)?; + println!("[✔] Current vault backed up to: .passlock.vault.before_import"); } + + fs::copy(&import_path, &vault_path)?; + println!("[✔] Vault imported from: {input_path}"); + + Ok(()) + } else { + fs::remove_file(&temp_path)?; + Err("Incorrect password for this vault file".into()) } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 81a64e1..25eef36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ pub fn get_timestamp() -> u64 { fn main() -> Result<(), Box> { crypto::init_crypto()?; - + backup::init_backup_system()?; let args: Vec = env::args().collect(); @@ -110,7 +110,9 @@ fn main() -> Result<(), Box> { eprintln!("Usage: passlock export-csv "); eprintln!(); eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); - eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); + eprintln!( + "Use for importing to other password managers (LastPass, Bitwarden, etc.)" + ); eprintln!(); eprintln!("Example:"); eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); @@ -203,10 +205,10 @@ fn create_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault created successfully."); - + println!("[...] Creating initial backup..."); backup::create_backup("default", 10)?; - + Ok(()) } @@ -231,10 +233,10 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); - + println!("[...] Creating backup..."); backup::create_backup("default", 10)?; - + Ok(()) } @@ -264,16 +266,16 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> std::process::exit(1); } let password = &args[1]; - + let _vault = storage::ld_vt(password)?; println!("[✔] Password verified"); - + backup::create_backup("default", 10)?; println!("[✔] Manual backup created successfully"); } "list" => { let backups = backup::list_backups("default")?; - + if backups.is_empty() { println!("[!] No backups found"); println!(); @@ -283,18 +285,18 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Available Backups:"); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); - println!("{:<40} {:<12} {}", "Filename", "Size", "Created"); + println!("{:<40} {:<12} Created", "Filename", "Size"); println!("{}", "─".repeat(70)); - + for (filename, size, created) in &backups { let size_kb = if *size > 1024 { format!("{} KB", *size / 1024) } else { format!("{} bytes", *size) }; - println!("{:<40} {:<12} {}", filename, size_kb, created); + println!("{filename:<40} {size_kb:<12} {created}"); } - + println!(); println!("Total backups: {}", backups.len()); println!(); @@ -307,12 +309,14 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> eprintln!("Usage: passlock backup restore "); eprintln!(); eprintln!("Example:"); - eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); + eprintln!( + " passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123" + ); std::process::exit(1); } let backup_name = &args[1]; let password = &args[2]; - + backup::restore_backup("default", backup_name, password)?; println!("[✔] Backup restored successfully"); } @@ -322,7 +326,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Available subcommands: create, list, restore"); } } - + Ok(()) } @@ -364,7 +368,7 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } - + let backups = backup::list_backups("default").unwrap_or_else(|_| Vec::new()); println!(" Backups: {} available", backups.len()); } else { @@ -436,4 +440,4 @@ fn print_usage() { println!(" # (Then import into other PM and DELETE the CSV!)"); println!(); println!("For more info: https://github.com/hachimamma/Passlock"); -} \ No newline at end of file +} From 8593bfdc6970ece446696add98e2433bee14ffe4 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 22:05:16 +0530 Subject: [PATCH 37/48] FIX: moving on with the CLI features --- src/backup.rs | 387 ++++++++++++++++++++++++-------------------------- src/config.rs | 187 ++++++++++++++++++++++++ src/main.rs | 91 +++++++----- 3 files changed, 430 insertions(+), 235 deletions(-) create mode 100644 src/config.rs diff --git a/src/backup.rs b/src/backup.rs index 07e0ad1..41acb6c 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -3,57 +3,65 @@ use std::fs; use std::path::PathBuf; /// Gets the backup directory path: ~/.passlock/backups/ -/// This is where all automatic backups will be stored pub fn get_backup_dir() -> PathBuf { - let home = dirs::home_dir().expect("Could not find home directory"); - home.join(".passlock").join("backups") + crate::config::get_passlock_dir().join("backups") } /// Gets the vault-specific backup directory -/// If you have multiple vaults later, each gets its own backup folder pub fn get_vault_backup_dir(vault_name: &str) -> PathBuf { get_backup_dir().join(vault_name) } /// Initialize backup directory structure -/// Creates the folders if they don't exist pub fn init_backup_system() -> Result<(), Box> { let backup_dir = get_backup_dir(); - + if !backup_dir.exists() { fs::create_dir_all(&backup_dir)?; println!("[✔] Created backup directory: {}", backup_dir.display()); } - - let default_backup_dir = get_vault_backup_dir("default"); - if !default_backup_dir.exists() { - fs::create_dir_all(&default_backup_dir)?; - println!( - "[✔] Created vault backup directory: {}", - default_backup_dir.display() - ); - } - + Ok(()) } /// Create a timestamped backup of the vault -/// Format: backup_2026-02-28_18-30-45.vault -/// -/// This function: -/// 1. Gets current timestamp -/// 2. Creates a filename with that timestamp -/// 3. Copies the vault file to the backup directory -/// 4. Manages old backups (keeps only last N backups) +/// +/// SMART BACKUP: Only creates a new backup if the vault has actually changed +/// since the last backup. This saves disk space! +/// +/// Returns: (backup_filename, was_actually_created) pub fn create_backup( vault_name: &str, max_backups: usize, -) -> Result> { - let home = dirs::home_dir().expect("Could not find home directory"); - let vault_path = home.join(".passlock.vault"); + force: bool, // Set true to force backup even if no changes +) -> Result<(String, bool), Box> { + let vault_path = crate::config::get_vault_path(vault_name); if !vault_path.exists() { - return Err("No vault file found to backup".into()); + return Err(format!("Vault '{}' not found", vault_name).into()); + } + + // SMART BACKUP: Check if vault has changed since last backup + if !force { + let backup_dir = get_vault_backup_dir(vault_name); + if backup_dir.exists() { + let backups = list_backups(vault_name)?; + if !backups.is_empty() { + // Get most recent backup + let latest_backup = &backups[0]; + let latest_backup_path = backup_dir.join(&latest_backup.0); + + // Compare files + let vault_meta = fs::metadata(&vault_path)?; + let backup_meta = fs::metadata(&latest_backup_path)?; + + // Skip if same size (likely unchanged) + if vault_meta.len() == backup_meta.len() { + println!("[✔] Backup skipped (no changes detected)"); + return Ok((latest_backup.0.clone(), false)); + } + } + } } use chrono::Local; @@ -65,17 +73,18 @@ pub fn create_backup( let backup_path = backup_dir.join(&backup_filename); + // Copy vault to backup fs::copy(&vault_path, &backup_path)?; println!("[✔] Backup created: {backup_filename}"); + // Clean up old backups cleanup_old_backups(vault_name, max_backups)?; - Ok(backup_filename) + Ok((backup_filename, true)) } /// List all backups for a vault -/// Returns a vector of (filename, `file_size`, `created_time`) pub fn list_backups( vault_name: &str, ) -> Result, Box> { @@ -105,17 +114,13 @@ pub fn list_backups( } } + // Sort by filename (timestamp) - newest first backups.sort_by(|a, b| b.0.cmp(&a.0)); Ok(backups) } /// Restore vault from a backup -/// -/// This function: -/// 1. Finds the backup file -/// 2. Verifies it can be unlocked with the provided password -/// 3. Copies it back to the main vault location pub fn restore_backup( vault_name: &str, backup_filename: &str, @@ -130,32 +135,38 @@ pub fn restore_backup( println!("[...] Verifying backup with password..."); - let home = dirs::home_dir().expect("Could not find home directory"); - let test_path = home.join(".passlock.vault.test"); - fs::copy(&backup_path, &test_path)?; + let vault_path = crate::config::get_vault_path(vault_name); + let temp_path = crate::config::get_passlock_dir().join(format!("{}.temp", vault_name)); + + // Copy backup to temp location + fs::copy(&backup_path, &temp_path)?; + // Try to decrypt it (validates password) + // TODO: This needs to be updated to use vault_name parameter if storage::ld_vt(password).is_ok() { - fs::remove_file(&test_path)?; - - let vault_path = home.join(".passlock.vault"); + // Password works! Restore it + fs::remove_file(&temp_path)?; + // Safety backup of current vault if vault_path.exists() { - let safety_backup = home.join(".passlock.vault.before_restore"); + let safety_backup = crate::config::get_passlock_dir() + .join(format!("{}.before_restore", vault_name)); fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: .passlock.vault.before_restore"); + println!("[✔] Current vault backed up to: {}.before_restore", vault_name); } + // Restore the backup fs::copy(&backup_path, &vault_path)?; - println!("[✔] Vault restored from backup: {backup_filename}"); + println!("[✔] Vault '{}' restored from: {}", vault_name, backup_filename); Ok(()) } else { - fs::remove_file(&test_path)?; + fs::remove_file(&temp_path)?; Err("Incorrect password for this backup".into()) } } -/// Clean up old backups, keeping only the most recent N backups +/// Clean up old backups, keeping only the most recent N fn cleanup_old_backups( vault_name: &str, max_backups: usize, @@ -176,30 +187,78 @@ fn cleanup_old_backups( Ok(()) } -/// Export vault to a specific location (encrypted `PassLock` format) -/// Useful for manual backups or transferring between systems -pub fn export_vault(password: &str, output_path: &str) -> Result<(), Box> { +/// Export vault to external location (encrypted) +pub fn export_vault( + vault_name: &str, + password: &str, + output_path: &str, +) -> Result<(), Box> { + // Verify password let _vault = storage::ld_vt(password)?; - let home = dirs::home_dir().expect("Could not find home directory"); - let vault_path = home.join(".passlock.vault"); + let vault_path = crate::config::get_vault_path(vault_name); if !vault_path.exists() { - return Err("No vault file found to export".into()); + return Err(format!("Vault '{}' not found", vault_name).into()); } fs::copy(&vault_path, output_path)?; - println!("[✔] Vault exported to: {output_path}"); + println!("[✔] Vault '{}' exported to: {}", vault_name, output_path); println!("[✔] Format: Encrypted PassLock vault"); - println!("[!] Keep this file secure - it contains all your passwords!"); + println!("[!] Keep this file secure!"); Ok(()) } -/// Export vault to CSV format (PLAINTEXT - for importing to other password managers) -/// WARNING: This creates an UNENCRYPTED file with all passwords visible! -pub fn export_to_csv(password: &str, output_path: &str) -> Result<(), Box> { +/// Import vault from external location (encrypted) +pub fn import_vault( + vault_name: &str, + password: &str, + input_path: &str, +) -> Result<(), Box> { + let import_path = PathBuf::from(input_path); + + if !import_path.exists() { + return Err(format!("Import file not found: {input_path}").into()); + } + + let vault_path = crate::config::get_vault_path(vault_name); + let temp_path = crate::config::get_passlock_dir().join(format!("{}.import_test", vault_name)); + + fs::copy(&import_path, &temp_path)?; + + println!("[...] Verifying import file..."); + + // Try to decrypt + if storage::ld_vt(password).is_ok() { + fs::remove_file(&temp_path)?; + + // Safety backup + if vault_path.exists() { + let safety_backup = crate::config::get_passlock_dir() + .join(format!("{}.before_import", vault_name)); + fs::copy(&vault_path, &safety_backup)?; + println!("[✔] Current vault backed up: {}.before_import", vault_name); + } + + // Import + fs::copy(&import_path, &vault_path)?; + println!("[✔] Vault '{}' imported from: {}", vault_name, input_path); + + Ok(()) + } else { + fs::remove_file(&temp_path)?; + Err("Incorrect password for this vault file".into()) + } +} + +/// Export to CSV (PLAINTEXT!) +pub fn export_to_csv( + vault_name: &str, + password: &str, + output_path: &str, +) -> Result<(), Box> { let vault = storage::ld_vt(password)?; let mut csv_content = String::from("name,username,password,url,notes,tags,2fa_secret\n"); @@ -207,33 +266,32 @@ pub fn export_to_csv(password: &str, output_path: &str) -> Result<(), Box Result<(), Box> { +/// Export to JSON (PLAINTEXT!) +pub fn export_to_json( + vault_name: &str, + password: &str, + output_path: &str, +) -> Result<(), Box> { let vault = storage::ld_vt(password)?; let mut entries = Vec::new(); @@ -248,11 +306,9 @@ pub fn export_to_json(password: &str, output_path: &str) -> Result<(), Box Result<(), Box Result<(), Box String { - if field.contains(',') || field.contains('"') || field.contains('\n') { - format!("\"{}\"", field.replace('"', "\"\"")) - } else { - field.to_string() - } -} - -/// Import from CSV file (from other password managers like `LastPass`, Bitwarden, etc.) -/// Expected format: `name,username,password,url,notes,tags,2fa_secret` -pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box> { +/// Import from CSV +pub fn import_from_csv( + vault_name: &str, + password: &str, + input_path: &str, +) -> Result<(), Box> { use crate::models::Entry; use std::io::{BufRead, BufReader}; @@ -298,9 +348,10 @@ pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box Result<(), Box = tags_str - .split(';') - .filter(|s| !s.is_empty()) - .map(std::string::ToString::to_string) - .collect(); - let entry = Entry { id: crate::generate_uuid(), - n: name, - u: username, - p: password_val, - url, - nt: notes, - tags, - totp_secret, + n: fields[0].clone(), + u: fields.get(1).cloned().unwrap_or_default(), + p: fields.get(2).cloned().unwrap_or_default(), + url: fields.get(3).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), + nt: fields.get(4).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), + tags: fields.get(5).map_or(Vec::new(), |s| { + s.split(';').filter(|t| !t.is_empty()).map(String::from).collect() + }), + totp_secret: fields.get(6).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), t: crate::get_timestamp(), last_modified: crate::get_timestamp(), history: Vec::new(), @@ -363,19 +395,24 @@ pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box 0 { - println!("[!] Skipped: {skipped_count} invalid entries"); + println!("[!] Skipped: {} invalid entries", skipped_count); } - println!("[✔] Total entries in vault: {}", vault.e.len()); - create_backup("default", 10)?; + // Auto-backup after import + let config = crate::config::load_config()?; + if config.auto_backup { + create_backup(vault_name, config.max_backups, false)?; + } Ok(()) } +/// Import from JSON pub fn import_from_json( + vault_name: &str, password: &str, input_path: &str, ) -> Result<(), Box> { @@ -388,49 +425,26 @@ pub fn import_from_json( let entries = match json.get("entries") { Some(serde_json::Value::Array(arr)) => arr, - _ => return Err("Invalid JSON format - expected 'entries' array".into()), + _ => return Err("Invalid JSON format".into()), }; let mut imported_count = 0; for entry_json in entries { - let name = entry_json["name"] - .as_str() - .unwrap_or("Untitled") - .to_string(); - let username = entry_json["username"].as_str().unwrap_or("").to_string(); - let password_val = entry_json["password"].as_str().unwrap_or("").to_string(); - let url = entry_json - .get("url") - .and_then(|v| v.as_str()) - .map(String::from); - let notes = entry_json - .get("notes") - .and_then(|v| v.as_str()) - .map(String::from); - let totp_secret = entry_json - .get("totp_secret") - .and_then(|v| v.as_str()) - .map(String::from); - - let tags = match entry_json.get("tags") { - Some(serde_json::Value::Array(arr)) => arr - .iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect(), - _ => Vec::new(), - }; - let entry = Entry { id: crate::generate_uuid(), - n: name, - u: username, - p: password_val, - url, - nt: notes, - tags, - totp_secret, + n: entry_json["name"].as_str().unwrap_or("Untitled").to_string(), + u: entry_json["username"].as_str().unwrap_or("").to_string(), + p: entry_json["password"].as_str().unwrap_or("").to_string(), + url: entry_json.get("url").and_then(|v| v.as_str()).map(String::from), + nt: entry_json.get("notes").and_then(|v| v.as_str()).map(String::from), + totp_secret: entry_json.get("totp_secret").and_then(|v| v.as_str()).map(String::from), + tags: match entry_json.get("tags") { + Some(serde_json::Value::Array(arr)) => { + arr.iter().filter_map(|v| v.as_str()).map(String::from).collect() + } + _ => Vec::new(), + }, t: crate::get_timestamp(), last_modified: crate::get_timestamp(), history: Vec::new(), @@ -442,16 +456,26 @@ pub fn import_from_json( storage::svv(&vault, password)?; - println!("[✔] Import completed!"); - println!("[✔] Imported: {imported_count} entries"); - println!("[✔] Total entries in vault: {}", vault.e.len()); + println!("[✔] Import completed to vault '{}'", vault_name); + println!("[✔] Imported: {} entries", imported_count); - create_backup("default", 10)?; + // Auto-backup after import + let config = crate::config::load_config()?; + if config.auto_backup { + create_backup(vault_name, config.max_backups, false)?; + } Ok(()) } -/// Simple CSV line parser that handles quoted fields +fn escape_csv(field: &str) -> String { + if field.contains(',') || field.contains('"') || field.contains('\n') { + format!("\"{}\"", field.replace('"', "\"\"")) + } else { + field.to_string() + } +} + fn parse_csv_line(line: &str) -> Vec { let mut fields = Vec::new(); let mut current_field = String::new(); @@ -479,42 +503,5 @@ fn parse_csv_line(line: &str) -> Vec { } fields.push(current_field.trim().to_string()); - fields -} - -/// Import vault from a specific location -/// Replaces current vault with the imported one -pub fn import_vault(password: &str, input_path: &str) -> Result<(), Box> { - let import_path = PathBuf::from(input_path); - - if !import_path.exists() { - return Err(format!("Import file not found: {input_path}").into()); - } - - let home = dirs::home_dir().expect("Could not find home directory"); - let temp_path = home.join(".passlock.vault.import_test"); - fs::copy(&import_path, &temp_path)?; - - println!("[...] Verifying import file with password..."); - - if storage::ld_vt(password).is_ok() { - fs::remove_file(&temp_path)?; - - let vault_path = home.join(".passlock.vault"); - - if vault_path.exists() { - let safety_backup = home.join(".passlock.vault.before_import"); - fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: .passlock.vault.before_import"); - } - - fs::copy(&import_path, &vault_path)?; - println!("[✔] Vault imported from: {input_path}"); - - Ok(()) - } else { - fs::remove_file(&temp_path)?; - Err("Incorrect password for this vault file".into()) - } -} +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c9a3090 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,187 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +/// Configuration for PassLock +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Config { + /// Currently active vault name + pub active_vault: String, + /// Auto-backup on every save (if false, only manual backups) + pub auto_backup: bool, + /// Maximum number of backups to keep per vault + pub max_backups: usize, + /// Clipboard timeout in seconds + pub clipboard_timeout: u64, + /// UI refresh rate in milliseconds + pub refresh_rate: u64, +} + +impl Default for Config { + fn default() -> Self { + Self { + active_vault: String::from("personal"), + auto_backup: true, + max_backups: 10, + clipboard_timeout: 30, + refresh_rate: 100, + } + } +} + +/// Get the PassLock root directory: ~/.passlock/ +pub fn get_passlock_dir() -> PathBuf { + let home = dirs::home_dir().expect("Could not find home directory"); + home.join(".passlock") +} + +/// Get the vaults directory: ~/.passlock/vaults/ +pub fn get_vaults_dir() -> PathBuf { + get_passlock_dir().join("vaults") +} + +/// Get the config file path: ~/.passlock/config.json +pub fn get_config_path() -> PathBuf { + get_passlock_dir().join("config.json") +} + +/// Get the path to a specific vault file +pub fn get_vault_path(vault_name: &str) -> PathBuf { + get_vaults_dir().join(format!("{}.vault", vault_name)) +} + +/// Initialize the PassLock directory structure +pub fn init_passlock_dirs() -> Result<(), Box> { + let passlock_dir = get_passlock_dir(); + let vaults_dir = get_vaults_dir(); + + // Create main directory + if !passlock_dir.exists() { + fs::create_dir_all(&passlock_dir)?; + println!("[✔] Created PassLock directory: {}", passlock_dir.display()); + } + + // Create vaults directory + if !vaults_dir.exists() { + fs::create_dir_all(&vaults_dir)?; + println!("[✔] Created vaults directory: {}", vaults_dir.display()); + } + + // Check for old vault file and migrate it + let home = dirs::home_dir().expect("Could not find home directory"); + let old_vault = home.join(".passlock.vault"); + if old_vault.exists() { + let new_vault = get_vault_path("personal"); + if !new_vault.exists() { + fs::copy(&old_vault, &new_vault)?; + println!("[✔] Migrated old vault to: {}", new_vault.display()); + println!("[!] You can delete the old vault: {}", old_vault.display()); + } + } + + Ok(()) +} + +/// Load configuration from file +pub fn load_config() -> Result> { + let config_path = get_config_path(); + + if config_path.exists() { + let config_str = fs::read_to_string(config_path)?; + let config: Config = serde_json::from_str(&config_str)?; + Ok(config) + } else { + // Create default config + let config = Config::default(); + save_config(&config)?; + Ok(config) + } +} + +/// Save configuration to file +pub fn save_config(config: &Config) -> Result<(), Box> { + let config_path = get_config_path(); + let config_str = serde_json::to_string_pretty(config)?; + fs::write(config_path, config_str)?; + Ok(()) +} + +/// List all available vaults +pub fn list_vaults() -> Result, Box> { + let vaults_dir = get_vaults_dir(); + + if !vaults_dir.exists() { + return Ok(Vec::new()); + } + + let mut vaults = Vec::new(); + + for entry in fs::read_dir(vaults_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("vault") { + if let Some(name) = path.file_stem().and_then(|s| s.to_str()) { + vaults.push(name.to_string()); + } + } + } + + vaults.sort(); + Ok(vaults) +} + +/// Check if a vault exists +pub fn vault_exists(vault_name: &str) -> bool { + get_vault_path(vault_name).exists() +} + +/// Delete a vault and its backups +pub fn delete_vault(vault_name: &str) -> Result<(), Box> { + let vault_path = get_vault_path(vault_name); + + if !vault_path.exists() { + return Err(format!("Vault '{}' does not exist", vault_name).into()); + } + + // Delete vault file + fs::remove_file(&vault_path)?; + + // Delete backups + let backup_dir = crate::backup::get_vault_backup_dir(vault_name); + if backup_dir.exists() { + fs::remove_dir_all(&backup_dir)?; + } + + println!("[✔] Deleted vault: {}", vault_name); + println!("[✔] Deleted backups for: {}", vault_name); + + Ok(()) +} + +/// Rename a vault +pub fn rename_vault(old_name: &str, new_name: &str) -> Result<(), Box> { + let old_path = get_vault_path(old_name); + let new_path = get_vault_path(new_name); + + if !old_path.exists() { + return Err(format!("Vault '{}' does not exist", old_name).into()); + } + + if new_path.exists() { + return Err(format!("Vault '{}' already exists", new_name).into()); + } + + fs::rename(&old_path, &new_path)?; + + // Rename backup directory + let old_backup_dir = crate::backup::get_vault_backup_dir(old_name); + let new_backup_dir = crate::backup::get_vault_backup_dir(new_name); + if old_backup_dir.exists() { + fs::rename(&old_backup_dir, &new_backup_dir)?; + } + + println!("[✔] Renamed vault: {} → {}", old_name, new_name); + + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 25eef36..fe8cdba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod backup; +mod config; mod crypto; mod models; mod storage; @@ -38,8 +39,14 @@ pub fn get_timestamp() -> u64 { fn main() -> Result<(), Box> { crypto::init_crypto()?; - + + // Initialize PassLock directory structure and config + config::init_passlock_dirs()?; backup::init_backup_system()?; + + // Load config to get active vault name + let cfg = config::load_config().unwrap_or_default(); + let active_vault = cfg.active_vault.clone(); let args: Vec = env::args().collect(); @@ -69,9 +76,11 @@ fn main() -> Result<(), Box> { let password = &args[2]; sync_vault(password)?; } + // NEW: Backup commands "backup" => { handle_backup_cmd(&args[2..])?; } + // NEW: Export vault to external file (ENCRYPTED - for backup) "export" => { if args.len() < 4 { eprintln!("Usage: passlock export "); @@ -87,8 +96,9 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let output_file = &args[3]; - backup::export_vault(password, output_file)?; + backup::export_vault(&active_vault, password, output_file)?; } + // NEW: Import vault from external file (ENCRYPTED - for restore) "import" => { if args.len() < 4 { eprintln!("Usage: passlock import "); @@ -103,16 +113,15 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let input_file = &args[3]; - backup::import_vault(password, input_file)?; + backup::import_vault(&active_vault, password, input_file)?; } + // NEW: Export to CSV (PLAINTEXT - for other password managers) "export-csv" => { if args.len() < 4 { eprintln!("Usage: passlock export-csv "); eprintln!(); eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); - eprintln!( - "Use for importing to other password managers (LastPass, Bitwarden, etc.)" - ); + eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); eprintln!(); eprintln!("Example:"); eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); @@ -120,8 +129,9 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let output_file = &args[3]; - backup::export_to_csv(password, output_file)?; + backup::export_to_csv(&active_vault, password, output_file)?; } + // NEW: Export to JSON (PLAINTEXT - for other password managers) "export-json" => { if args.len() < 4 { eprintln!("Usage: passlock export-json "); @@ -135,8 +145,9 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let output_file = &args[3]; - backup::export_to_json(password, output_file)?; + backup::export_to_json(&active_vault, password, output_file)?; } + // NEW: Import from CSV (PLAINTEXT - from other password managers) "import-csv" => { if args.len() < 4 { eprintln!("Usage: passlock import-csv "); @@ -150,8 +161,9 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let input_file = &args[3]; - backup::import_from_csv(password, input_file)?; + backup::import_from_csv(&active_vault, password, input_file)?; } + // NEW: Import from JSON (PLAINTEXT - from other password managers) "import-json" => { if args.len() < 4 { eprintln!("Usage: passlock import-json "); @@ -164,7 +176,7 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let input_file = &args[3]; - backup::import_from_json(password, input_file)?; + backup::import_from_json(&active_vault, password, input_file)?; } "info" => { handle_icmd(&args[2..])?; @@ -205,10 +217,11 @@ fn create_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault created successfully."); - + + // Create initial backup (force=true for first backup) println!("[...] Creating initial backup..."); - backup::create_backup("default", 10)?; - + backup::create_backup("personal", 10, true)?; + Ok(()) } @@ -233,13 +246,16 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); - + + // Automatic backup after sync (smart backup) println!("[...] Creating backup..."); - backup::create_backup("default", 10)?; - + let cfg = config::load_config().unwrap_or_default(); + backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; + Ok(()) } +/// Handle backup subcommands fn handle_backup_cmd(args: &[String]) -> Result<(), Box> { if args.is_empty() { println!("Backup Commands:"); @@ -266,28 +282,32 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> std::process::exit(1); } let password = &args[1]; - + + // Verify password before backup let _vault = storage::ld_vt(password)?; println!("[✔] Password verified"); - - backup::create_backup("default", 10)?; + + // Create manual backup (force=true) + let cfg = config::load_config().unwrap_or_default(); + backup::create_backup(&cfg.active_vault, cfg.max_backups, true)?; println!("[✔] Manual backup created successfully"); } "list" => { - let backups = backup::list_backups("default")?; - + let cfg = config::load_config().unwrap_or_default(); + let backups = backup::list_backups(&cfg.active_vault)?; + if backups.is_empty() { - println!("[!] No backups found"); + println!("[!] No backups found for vault '{}'", cfg.active_vault); println!(); println!("Create your first backup with:"); println!(" passlock backup create "); } else { - println!("Available Backups:"); + println!("Available Backups for vault '{}':", cfg.active_vault); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); println!("{:<40} {:<12} Created", "Filename", "Size"); println!("{}", "─".repeat(70)); - + for (filename, size, created) in &backups { let size_kb = if *size > 1024 { format!("{} KB", *size / 1024) @@ -296,7 +316,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> }; println!("{filename:<40} {size_kb:<12} {created}"); } - + println!(); println!("Total backups: {}", backups.len()); println!(); @@ -309,15 +329,14 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> eprintln!("Usage: passlock backup restore "); eprintln!(); eprintln!("Example:"); - eprintln!( - " passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123" - ); + eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); std::process::exit(1); } let backup_name = &args[1]; let password = &args[2]; - - backup::restore_backup("default", backup_name, password)?; + + let cfg = config::load_config().unwrap_or_default(); + backup::restore_backup(&cfg.active_vault, backup_name, password)?; println!("[✔] Backup restored successfully"); } _ => { @@ -326,7 +345,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Available subcommands: create, list, restore"); } } - + Ok(()) } @@ -368,9 +387,11 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } - - let backups = backup::list_backups("default").unwrap_or_else(|_| Vec::new()); - println!(" Backups: {} available", backups.len()); + + // Show backup info + let cfg = config::load_config().unwrap_or_default(); + let backups = backup::list_backups(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); + println!(" Backups: {} available (vault: {})", backups.len(), cfg.active_vault); } else { println!("Vault Status:"); println!(" No vault found"); @@ -440,4 +461,4 @@ fn print_usage() { println!(" # (Then import into other PM and DELETE the CSV!)"); println!(); println!("For more info: https://github.com/hachimamma/Passlock"); -} +} \ No newline at end of file From 942566f2a44d4d194114f33619c3ab49c22f5228 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sat, 28 Feb 2026 22:19:44 +0530 Subject: [PATCH 38/48] REMOVE: Removed backup.rs, reverted to stable commit --- src/backup.rs | 520 -------------------------------------------------- src/main.rs | 250 +----------------------- 2 files changed, 10 insertions(+), 760 deletions(-) delete mode 100644 src/backup.rs diff --git a/src/backup.rs b/src/backup.rs deleted file mode 100644 index 07e0ad1..0000000 --- a/src/backup.rs +++ /dev/null @@ -1,520 +0,0 @@ -use crate::storage; -use std::fs; -use std::path::PathBuf; - -/// Gets the backup directory path: ~/.passlock/backups/ -/// This is where all automatic backups will be stored -pub fn get_backup_dir() -> PathBuf { - let home = dirs::home_dir().expect("Could not find home directory"); - home.join(".passlock").join("backups") -} - -/// Gets the vault-specific backup directory -/// If you have multiple vaults later, each gets its own backup folder -pub fn get_vault_backup_dir(vault_name: &str) -> PathBuf { - get_backup_dir().join(vault_name) -} - -/// Initialize backup directory structure -/// Creates the folders if they don't exist -pub fn init_backup_system() -> Result<(), Box> { - let backup_dir = get_backup_dir(); - - if !backup_dir.exists() { - fs::create_dir_all(&backup_dir)?; - println!("[✔] Created backup directory: {}", backup_dir.display()); - } - - let default_backup_dir = get_vault_backup_dir("default"); - if !default_backup_dir.exists() { - fs::create_dir_all(&default_backup_dir)?; - println!( - "[✔] Created vault backup directory: {}", - default_backup_dir.display() - ); - } - - Ok(()) -} - -/// Create a timestamped backup of the vault -/// Format: backup_2026-02-28_18-30-45.vault -/// -/// This function: -/// 1. Gets current timestamp -/// 2. Creates a filename with that timestamp -/// 3. Copies the vault file to the backup directory -/// 4. Manages old backups (keeps only last N backups) -pub fn create_backup( - vault_name: &str, - max_backups: usize, -) -> Result> { - let home = dirs::home_dir().expect("Could not find home directory"); - let vault_path = home.join(".passlock.vault"); - - if !vault_path.exists() { - return Err("No vault file found to backup".into()); - } - - use chrono::Local; - let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); - let backup_filename = format!("backup_{timestamp}.vault"); - - let backup_dir = get_vault_backup_dir(vault_name); - fs::create_dir_all(&backup_dir)?; - - let backup_path = backup_dir.join(&backup_filename); - - fs::copy(&vault_path, &backup_path)?; - - println!("[✔] Backup created: {backup_filename}"); - - cleanup_old_backups(vault_name, max_backups)?; - - Ok(backup_filename) -} - -/// List all backups for a vault -/// Returns a vector of (filename, `file_size`, `created_time`) -pub fn list_backups( - vault_name: &str, -) -> Result, Box> { - let backup_dir = get_vault_backup_dir(vault_name); - - if !backup_dir.exists() { - return Ok(Vec::new()); - } - - let mut backups = Vec::new(); - - for entry in fs::read_dir(backup_dir)? { - let entry = entry?; - let path = entry.path(); - - if path.extension().and_then(|s| s.to_str()) == Some("vault") { - let metadata = fs::metadata(&path)?; - let filename = path.file_name().unwrap().to_string_lossy().to_string(); - let size = metadata.len(); - - use chrono::{DateTime, Local}; - let created = metadata.created()?; - let datetime: DateTime = created.into(); - let created_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); - - backups.push((filename, size, created_str)); - } - } - - backups.sort_by(|a, b| b.0.cmp(&a.0)); - - Ok(backups) -} - -/// Restore vault from a backup -/// -/// This function: -/// 1. Finds the backup file -/// 2. Verifies it can be unlocked with the provided password -/// 3. Copies it back to the main vault location -pub fn restore_backup( - vault_name: &str, - backup_filename: &str, - password: &str, -) -> Result<(), Box> { - let backup_dir = get_vault_backup_dir(vault_name); - let backup_path = backup_dir.join(backup_filename); - - if !backup_path.exists() { - return Err(format!("Backup file not found: {backup_filename}").into()); - } - - println!("[...] Verifying backup with password..."); - - let home = dirs::home_dir().expect("Could not find home directory"); - let test_path = home.join(".passlock.vault.test"); - fs::copy(&backup_path, &test_path)?; - - if storage::ld_vt(password).is_ok() { - fs::remove_file(&test_path)?; - - let vault_path = home.join(".passlock.vault"); - - if vault_path.exists() { - let safety_backup = home.join(".passlock.vault.before_restore"); - fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: .passlock.vault.before_restore"); - } - - fs::copy(&backup_path, &vault_path)?; - println!("[✔] Vault restored from backup: {backup_filename}"); - - Ok(()) - } else { - fs::remove_file(&test_path)?; - Err("Incorrect password for this backup".into()) - } -} - -/// Clean up old backups, keeping only the most recent N backups -fn cleanup_old_backups( - vault_name: &str, - max_backups: usize, -) -> Result<(), Box> { - let backups = list_backups(vault_name)?; - - if backups.len() > max_backups { - let backup_dir = get_vault_backup_dir(vault_name); - let to_delete = &backups[max_backups..]; - - for (filename, _, _) in to_delete { - let backup_path = backup_dir.join(filename); - fs::remove_file(&backup_path)?; - println!("[✔] Removed old backup: {filename}"); - } - } - - Ok(()) -} - -/// Export vault to a specific location (encrypted `PassLock` format) -/// Useful for manual backups or transferring between systems -pub fn export_vault(password: &str, output_path: &str) -> Result<(), Box> { - let _vault = storage::ld_vt(password)?; - - let home = dirs::home_dir().expect("Could not find home directory"); - let vault_path = home.join(".passlock.vault"); - - if !vault_path.exists() { - return Err("No vault file found to export".into()); - } - - fs::copy(&vault_path, output_path)?; - - println!("[✔] Vault exported to: {output_path}"); - println!("[✔] Format: Encrypted PassLock vault"); - println!("[!] Keep this file secure - it contains all your passwords!"); - - Ok(()) -} - -/// Export vault to CSV format (PLAINTEXT - for importing to other password managers) -/// WARNING: This creates an UNENCRYPTED file with all passwords visible! -pub fn export_to_csv(password: &str, output_path: &str) -> Result<(), Box> { - let vault = storage::ld_vt(password)?; - - let mut csv_content = String::from("name,username,password,url,notes,tags,2fa_secret\n"); - - for entry in &vault.e { - let name = escape_csv(&entry.n); - let username = escape_csv(&entry.u); - let password = escape_csv(&entry.p); - let url = entry.url.as_ref().map_or(String::new(), |u| escape_csv(u)); - let notes = entry.nt.as_ref().map_or(String::new(), |n| escape_csv(n)); - let tags = escape_csv(&entry.tags.join(";")); - let totp = entry - .totp_secret - .as_ref() - .map_or(String::new(), |t| escape_csv(t)); - - csv_content.push_str(&format!( - "{name},{username},{password},{url},{notes},{tags},{totp}\n" - )); - } - - fs::write(output_path, csv_content)?; - - println!("[✔] Vault exported to CSV: {output_path}"); - println!("[!] WARNING: This file is UNENCRYPTED and contains plaintext passwords!"); - println!("[!] Use this for importing to other password managers, then DELETE it!"); - println!("[✔] Total entries exported: {}", vault.e.len()); - - Ok(()) -} - -/// Export vault to JSON format (PLAINTEXT - for importing to other password managers) -/// WARNING: This creates an UNENCRYPTED file with all passwords visible! -pub fn export_to_json(password: &str, output_path: &str) -> Result<(), Box> { - let vault = storage::ld_vt(password)?; - - let mut entries = Vec::new(); - for entry in &vault.e { - let mut json_entry = serde_json::json!({ - "name": entry.n, - "username": entry.u, - "password": entry.p, - "tags": entry.tags, - }); - - if let Some(ref url) = entry.url { - json_entry["url"] = serde_json::json!(url); - } - - if let Some(ref notes) = entry.nt { - json_entry["notes"] = serde_json::json!(notes); - } - - if let Some(ref totp) = entry.totp_secret { - json_entry["totp_secret"] = serde_json::json!(totp); - } - - entries.push(json_entry); - } - - let json_output = serde_json::json!({ - "passlock_version": env!("CARGO_PKG_VERSION"), - "exported_at": chrono::Local::now().to_rfc3339(), - "entries": entries, - }); - - let json_string = serde_json::to_string_pretty(&json_output)?; - fs::write(output_path, json_string)?; - - println!("[✔] Vault exported to JSON: {output_path}"); - println!("[!] WARNING: This file is UNENCRYPTED and contains plaintext passwords!"); - println!("[!] Use this for importing to other password managers, then DELETE it!"); - println!("[✔] Total entries exported: {}", vault.e.len()); - - Ok(()) -} - -/// Helper function to escape CSV fields -fn escape_csv(field: &str) -> String { - if field.contains(',') || field.contains('"') || field.contains('\n') { - format!("\"{}\"", field.replace('"', "\"\"")) - } else { - field.to_string() - } -} - -/// Import from CSV file (from other password managers like `LastPass`, Bitwarden, etc.) -/// Expected format: `name,username,password,url,notes,tags,2fa_secret` -pub fn import_from_csv(password: &str, input_path: &str) -> Result<(), Box> { - use crate::models::Entry; - use std::io::{BufRead, BufReader}; - - let mut vault = storage::ld_vt(password)?; - - let file = fs::File::open(input_path)?; - let reader = BufReader::new(file); - let mut lines = reader.lines(); - - if let Some(Ok(header)) = lines.next() { - if !header.contains("name") || !header.contains("password") { - return Err("Invalid CSV format - expected header with 'name' and 'password'".into()); - } - } else { - return Err("Empty CSV file".into()); - } - - let mut imported_count = 0; - let mut skipped_count = 0; - - for line_result in lines { - let line = line_result?; - if line.trim().is_empty() { - continue; - } - - let fields = parse_csv_line(&line); - - if fields.len() < 3 { - println!("[!] Skipping invalid line: {line}"); - skipped_count += 1; - continue; - } - - let name = fields[0].clone(); - let username = fields.get(1).cloned().unwrap_or_default(); - let password_val = fields.get(2).cloned().unwrap_or_default(); - let url = fields - .get(3) - .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }); - let notes = fields - .get(4) - .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }); - let tags_str = fields.get(5).cloned().unwrap_or_default(); - let totp_secret = fields - .get(6) - .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }); - - let tags: Vec = tags_str - .split(';') - .filter(|s| !s.is_empty()) - .map(std::string::ToString::to_string) - .collect(); - - let entry = Entry { - id: crate::generate_uuid(), - n: name, - u: username, - p: password_val, - url, - nt: notes, - tags, - totp_secret, - t: crate::get_timestamp(), - last_modified: crate::get_timestamp(), - history: Vec::new(), - }; - - vault.e.push(entry); - imported_count += 1; - } - - storage::svv(&vault, password)?; - - println!("[✔] Import completed!"); - println!("[✔] Imported: {imported_count} entries"); - if skipped_count > 0 { - println!("[!] Skipped: {skipped_count} invalid entries"); - } - println!("[✔] Total entries in vault: {}", vault.e.len()); - - create_backup("default", 10)?; - - Ok(()) -} - -pub fn import_from_json( - password: &str, - input_path: &str, -) -> Result<(), Box> { - use crate::models::Entry; - - let mut vault = storage::ld_vt(password)?; - - let json_content = fs::read_to_string(input_path)?; - let json: serde_json::Value = serde_json::from_str(&json_content)?; - - let entries = match json.get("entries") { - Some(serde_json::Value::Array(arr)) => arr, - _ => return Err("Invalid JSON format - expected 'entries' array".into()), - }; - - let mut imported_count = 0; - - for entry_json in entries { - let name = entry_json["name"] - .as_str() - .unwrap_or("Untitled") - .to_string(); - let username = entry_json["username"].as_str().unwrap_or("").to_string(); - let password_val = entry_json["password"].as_str().unwrap_or("").to_string(); - let url = entry_json - .get("url") - .and_then(|v| v.as_str()) - .map(String::from); - let notes = entry_json - .get("notes") - .and_then(|v| v.as_str()) - .map(String::from); - let totp_secret = entry_json - .get("totp_secret") - .and_then(|v| v.as_str()) - .map(String::from); - - let tags = match entry_json.get("tags") { - Some(serde_json::Value::Array(arr)) => arr - .iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect(), - _ => Vec::new(), - }; - - let entry = Entry { - id: crate::generate_uuid(), - n: name, - u: username, - p: password_val, - url, - nt: notes, - tags, - totp_secret, - t: crate::get_timestamp(), - last_modified: crate::get_timestamp(), - history: Vec::new(), - }; - - vault.e.push(entry); - imported_count += 1; - } - - storage::svv(&vault, password)?; - - println!("[✔] Import completed!"); - println!("[✔] Imported: {imported_count} entries"); - println!("[✔] Total entries in vault: {}", vault.e.len()); - - create_backup("default", 10)?; - - Ok(()) -} - -/// Simple CSV line parser that handles quoted fields -fn parse_csv_line(line: &str) -> Vec { - let mut fields = Vec::new(); - let mut current_field = String::new(); - let mut in_quotes = false; - let mut chars = line.chars().peekable(); - - while let Some(ch) = chars.next() { - match ch { - '"' => { - if in_quotes && chars.peek() == Some(&'"') { - current_field.push('"'); - chars.next(); - } else { - in_quotes = !in_quotes; - } - } - ',' if !in_quotes => { - fields.push(current_field.trim().to_string()); - current_field.clear(); - } - _ => { - current_field.push(ch); - } - } - } - - fields.push(current_field.trim().to_string()); - - fields -} - -/// Import vault from a specific location -/// Replaces current vault with the imported one -pub fn import_vault(password: &str, input_path: &str) -> Result<(), Box> { - let import_path = PathBuf::from(input_path); - - if !import_path.exists() { - return Err(format!("Import file not found: {input_path}").into()); - } - - let home = dirs::home_dir().expect("Could not find home directory"); - let temp_path = home.join(".passlock.vault.import_test"); - fs::copy(&import_path, &temp_path)?; - - println!("[...] Verifying import file with password..."); - - if storage::ld_vt(password).is_ok() { - fs::remove_file(&temp_path)?; - - let vault_path = home.join(".passlock.vault"); - - if vault_path.exists() { - let safety_backup = home.join(".passlock.vault.before_import"); - fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: .passlock.vault.before_import"); - } - - fs::copy(&import_path, &vault_path)?; - println!("[✔] Vault imported from: {input_path}"); - - Ok(()) - } else { - fs::remove_file(&temp_path)?; - Err("Incorrect password for this vault file".into()) - } -} diff --git a/src/main.rs b/src/main.rs index 25eef36..3bf881c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod backup; mod crypto; mod models; mod storage; @@ -39,8 +38,6 @@ pub fn get_timestamp() -> u64 { fn main() -> Result<(), Box> { crypto::init_crypto()?; - backup::init_backup_system()?; - let args: Vec = env::args().collect(); if args.len() > 1 { @@ -69,103 +66,6 @@ fn main() -> Result<(), Box> { let password = &args[2]; sync_vault(password)?; } - "backup" => { - handle_backup_cmd(&args[2..])?; - } - "export" => { - if args.len() < 4 { - eprintln!("Usage: passlock export "); - eprintln!(); - eprintln!("Export encrypted vault for backup/transfer."); - eprintln!("Works with ANY path on ANY device!"); - eprintln!(); - eprintln!("Examples:"); - eprintln!(" passlock export myPass123 ~/Documents/backup.vault"); - eprintln!(" passlock export myPass123 /media/usb/backup.vault"); - eprintln!(" passlock export myPass123 ~/Dropbox/backup.vault"); - std::process::exit(1); - } - let password = &args[2]; - let output_file = &args[3]; - backup::export_vault(password, output_file)?; - } - "import" => { - if args.len() < 4 { - eprintln!("Usage: passlock import "); - eprintln!(); - eprintln!("Import encrypted vault from backup."); - eprintln!("Works with ANY path on ANY device!"); - eprintln!(); - eprintln!("Examples:"); - eprintln!(" passlock import myPass123 ~/Documents/backup.vault"); - eprintln!(" passlock import myPass123 /media/usb/backup.vault"); - std::process::exit(1); - } - let password = &args[2]; - let input_file = &args[3]; - backup::import_vault(password, input_file)?; - } - "export-csv" => { - if args.len() < 4 { - eprintln!("Usage: passlock export-csv "); - eprintln!(); - eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); - eprintln!( - "Use for importing to other password managers (LastPass, Bitwarden, etc.)" - ); - eprintln!(); - eprintln!("Example:"); - eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); - std::process::exit(1); - } - let password = &args[2]; - let output_file = &args[3]; - backup::export_to_csv(password, output_file)?; - } - "export-json" => { - if args.len() < 4 { - eprintln!("Usage: passlock export-json "); - eprintln!(); - eprintln!("WARNING: Creates UNENCRYPTED JSON with plaintext passwords!"); - eprintln!("Use for importing to other password managers."); - eprintln!(); - eprintln!("Example:"); - eprintln!(" passlock export-json myPass123 ~/passwords.json"); - std::process::exit(1); - } - let password = &args[2]; - let output_file = &args[3]; - backup::export_to_json(password, output_file)?; - } - "import-csv" => { - if args.len() < 4 { - eprintln!("Usage: passlock import-csv "); - eprintln!(); - eprintln!("Import passwords from CSV file (LastPass, Bitwarden, etc.)"); - eprintln!("Expected format: name,username,password,url,notes,tags,2fa_secret"); - eprintln!(); - eprintln!("Example:"); - eprintln!(" passlock import-csv myPass123 ~/lastpass-export.csv"); - std::process::exit(1); - } - let password = &args[2]; - let input_file = &args[3]; - backup::import_from_csv(password, input_file)?; - } - "import-json" => { - if args.len() < 4 { - eprintln!("Usage: passlock import-json "); - eprintln!(); - eprintln!("Import passwords from JSON file."); - eprintln!(); - eprintln!("Example:"); - eprintln!(" passlock import-json myPass123 ~/passwords.json"); - std::process::exit(1); - } - let password = &args[2]; - let input_file = &args[3]; - backup::import_from_json(password, input_file)?; - } "info" => { handle_icmd(&args[2..])?; } @@ -205,10 +105,6 @@ fn create_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault created successfully."); - - println!("[...] Creating initial backup..."); - backup::create_backup("default", 10)?; - Ok(()) } @@ -233,100 +129,6 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); - - println!("[...] Creating backup..."); - backup::create_backup("default", 10)?; - - Ok(()) -} - -fn handle_backup_cmd(args: &[String]) -> Result<(), Box> { - if args.is_empty() { - println!("Backup Commands:"); - println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - println!(); - println!("Usage: passlock backup "); - println!(); - println!("Subcommands:"); - println!(" create Create manual backup"); - println!(" list List all backups"); - println!(" restore Restore from backup"); - println!(); - println!("Examples:"); - println!(" passlock backup create myPassword123"); - println!(" passlock backup list"); - println!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); - return Ok(()); - } - - match args[0].as_str() { - "create" => { - if args.len() < 2 { - eprintln!("Usage: passlock backup create "); - std::process::exit(1); - } - let password = &args[1]; - - let _vault = storage::ld_vt(password)?; - println!("[✔] Password verified"); - - backup::create_backup("default", 10)?; - println!("[✔] Manual backup created successfully"); - } - "list" => { - let backups = backup::list_backups("default")?; - - if backups.is_empty() { - println!("[!] No backups found"); - println!(); - println!("Create your first backup with:"); - println!(" passlock backup create "); - } else { - println!("Available Backups:"); - println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - println!(); - println!("{:<40} {:<12} Created", "Filename", "Size"); - println!("{}", "─".repeat(70)); - - for (filename, size, created) in &backups { - let size_kb = if *size > 1024 { - format!("{} KB", *size / 1024) - } else { - format!("{} bytes", *size) - }; - println!("{filename:<40} {size_kb:<12} {created}"); - } - - println!(); - println!("Total backups: {}", backups.len()); - println!(); - println!("Restore a backup with:"); - println!(" passlock backup restore "); - } - } - "restore" => { - if args.len() < 3 { - eprintln!("Usage: passlock backup restore "); - eprintln!(); - eprintln!("Example:"); - eprintln!( - " passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123" - ); - std::process::exit(1); - } - let backup_name = &args[1]; - let password = &args[2]; - - backup::restore_backup("default", backup_name, password)?; - println!("[✔] Backup restored successfully"); - } - _ => { - println!("Unknown backup subcommand: {}", args[0]); - println!(); - println!("Available subcommands: create, list, restore"); - } - } - Ok(()) } @@ -368,9 +170,6 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } - - let backups = backup::list_backups("default").unwrap_or_else(|_| Vec::new()); - println!(" Backups: {} available", backups.len()); } else { println!("Vault Status:"); println!(" No vault found"); @@ -396,48 +195,19 @@ fn print_usage() { println!("USAGE:"); println!(" passlock [OPTIONS]"); println!(); - println!("VAULT COMMANDS:"); - println!(" create Create a new vault"); - println!(" unlock Unlock and verify vault"); - println!(" sync Sync vault changes"); - println!(" tui Launch TUI interface"); - println!(); - println!("BACKUP COMMANDS (Encrypted):"); - println!(" backup create Create manual backup"); - println!(" backup list List all backups"); - println!(" backup restore Restore from backup"); - println!(); - println!(" export Export vault (encrypted)"); - println!(" import Import vault (encrypted)"); - println!(); - println!("PASSWORD MANAGER MIGRATION (Plaintext - USE WITH CAUTION):"); - println!(" export-csv Export to CSV (for other PMs)"); - println!(" export-json Export to JSON (for other PMs)"); - println!(" import-csv Import from CSV"); - println!(" import-json Import from JSON"); - println!(); - println!("INFO COMMANDS:"); - println!(" info [cpu] Show system information"); - println!(" version Show version"); - println!(" help Show this help"); + println!("COMMANDS:"); + println!(" create Create a new vault"); + println!(" unlock Unlock and verify vault"); + println!(" sync Sync vault changes"); + println!(" tui Launch TUI interface"); + println!(" info [cpu] Show system information"); + println!(" version Show version"); + println!(" help Show this help"); println!(); println!("EXAMPLES:"); - println!(); - println!(" # Create and use vault"); println!(" passlock create mySecurePassword123"); println!(" passlock tui"); - println!(); - println!(" # Backup to any location (works everywhere!)"); - println!(" passlock export myPass123 ~/Documents/backup.vault"); - println!(" passlock export myPass123 /media/usb/backup.vault"); - println!(" passlock export myPass123 ~/Dropbox/backup.vault"); - println!(); - println!(" # Migrate from LastPass/Bitwarden"); - println!(" passlock import-csv myPass123 ~/lastpass-export.csv"); - println!(); - println!(" # Migrate to another password manager"); - println!(" passlock export-csv myPass123 ~/passwords.csv"); - println!(" # (Then import into other PM and DELETE the CSV!)"); + println!(" passlock info cpu"); println!(); println!("For more info: https://github.com/hachimamma/Passlock"); -} +} \ No newline at end of file From 4c7e03fd0815166040f78d1bbef7b6350bf4cee5 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 10:37:01 +0530 Subject: [PATCH 39/48] UPDATE: Working on major updates - Completely implemented Credential import - Completely implemented Credential Export - Completely implemented Vault implemented import - Completely implemented Vault export - Completely implemented Vault backup - Implemented Time-based backup --- src/backup.rs | 132 ++++++++++-------------- src/config.rs | 30 +----- src/main.rs | 270 +++++++++++++++++++++++++++++++++++++++---------- src/storage.rs | 96 +++++++++++++----- 4 files changed, 346 insertions(+), 182 deletions(-) diff --git a/src/backup.rs b/src/backup.rs index 41acb6c..1b584da 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -2,19 +2,19 @@ use crate::storage; use std::fs; use std::path::PathBuf; -/// Gets the backup directory path: ~/.passlock/backups/ -pub fn get_backup_dir() -> PathBuf { +/// Gets the backup directory path +pub fn gback_dir() -> PathBuf { crate::config::get_passlock_dir().join("backups") } -/// Gets the vault-specific backup directory -pub fn get_vault_backup_dir(vault_name: &str) -> PathBuf { - get_backup_dir().join(vault_name) +/// Gets the vault specific backup directory +pub fn gvback_dir(vault_name: &str) -> PathBuf { + gback_dir().join(vault_name) } /// Initialize backup directory structure -pub fn init_backup_system() -> Result<(), Box> { - let backup_dir = get_backup_dir(); +pub fn init_bsys() -> Result<(), Box> { + let backup_dir = gback_dir(); if !backup_dir.exists() { fs::create_dir_all(&backup_dir)?; @@ -26,39 +26,35 @@ pub fn init_backup_system() -> Result<(), Box> { /// Create a timestamped backup of the vault /// -/// SMART BACKUP: Only creates a new backup if the vault has actually changed -/// since the last backup. This saves disk space! +/// Only creates a new backup if the vault has actually changed +/// since the last backup /// -/// Returns: (backup_filename, was_actually_created) +/// Returns: (back_fn, was_actually_created) pub fn create_backup( vault_name: &str, max_backups: usize, - force: bool, // Set true to force backup even if no changes + force: bool, ) -> Result<(String, bool), Box> { let vault_path = crate::config::get_vault_path(vault_name); if !vault_path.exists() { return Err(format!("Vault '{}' not found", vault_name).into()); } - - // SMART BACKUP: Check if vault has changed since last backup + if !force { - let backup_dir = get_vault_backup_dir(vault_name); + let backup_dir = gvback_dir(vault_name); if backup_dir.exists() { - let backups = list_backups(vault_name)?; + let backups = ls_backs(vault_name)?; if !backups.is_empty() { - // Get most recent backup - let latest_backup = &backups[0]; - let latest_backup_path = backup_dir.join(&latest_backup.0); + let last_back = &backups[0]; + let lb_path = backup_dir.join(&last_back.0); - // Compare files let vault_meta = fs::metadata(&vault_path)?; - let backup_meta = fs::metadata(&latest_backup_path)?; + let backup_meta = fs::metadata(&lb_path)?; - // Skip if same size (likely unchanged) if vault_meta.len() == backup_meta.len() { println!("[✔] Backup skipped (no changes detected)"); - return Ok((latest_backup.0.clone(), false)); + return Ok((last_back.0.clone(), false)); } } } @@ -66,29 +62,27 @@ pub fn create_backup( use chrono::Local; let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); - let backup_filename = format!("backup_{timestamp}.vault"); + let back_fn = format!("backup_{timestamp}.vault"); - let backup_dir = get_vault_backup_dir(vault_name); + let backup_dir = gvback_dir(vault_name); fs::create_dir_all(&backup_dir)?; - let backup_path = backup_dir.join(&backup_filename); + let backup_path = backup_dir.join(&back_fn); - // Copy vault to backup fs::copy(&vault_path, &backup_path)?; - println!("[✔] Backup created: {backup_filename}"); + println!("[✔] Backup created: {back_fn}"); - // Clean up old backups - cleanup_old_backups(vault_name, max_backups)?; + clean_obs(vault_name, max_backups)?; - Ok((backup_filename, true)) + Ok((back_fn, true)) } /// List all backups for a vault -pub fn list_backups( +pub fn ls_backs( vault_name: &str, ) -> Result, Box> { - let backup_dir = get_vault_backup_dir(vault_name); + let backup_dir = gvback_dir(vault_name); if !backup_dir.exists() { return Ok(Vec::new()); @@ -114,23 +108,22 @@ pub fn list_backups( } } - // Sort by filename (timestamp) - newest first backups.sort_by(|a, b| b.0.cmp(&a.0)); Ok(backups) } -/// Restore vault from a backup +/// Restore vault from a back pub fn restore_backup( vault_name: &str, - backup_filename: &str, + back_fn: &str, password: &str, ) -> Result<(), Box> { - let backup_dir = get_vault_backup_dir(vault_name); - let backup_path = backup_dir.join(backup_filename); + let backup_dir = gvback_dir(vault_name); + let backup_path = backup_dir.join(back_fn); if !backup_path.exists() { - return Err(format!("Backup file not found: {backup_filename}").into()); + return Err(format!("Backup file not found: {back_fn}").into()); } println!("[...] Verifying backup with password..."); @@ -138,16 +131,11 @@ pub fn restore_backup( let vault_path = crate::config::get_vault_path(vault_name); let temp_path = crate::config::get_passlock_dir().join(format!("{}.temp", vault_name)); - // Copy backup to temp location fs::copy(&backup_path, &temp_path)?; - // Try to decrypt it (validates password) - // TODO: This needs to be updated to use vault_name parameter if storage::ld_vt(password).is_ok() { - // Password works! Restore it fs::remove_file(&temp_path)?; - // Safety backup of current vault if vault_path.exists() { let safety_backup = crate::config::get_passlock_dir() .join(format!("{}.before_restore", vault_name)); @@ -155,9 +143,8 @@ pub fn restore_backup( println!("[✔] Current vault backed up to: {}.before_restore", vault_name); } - // Restore the backup fs::copy(&backup_path, &vault_path)?; - println!("[✔] Vault '{}' restored from: {}", vault_name, backup_filename); + println!("[✔] Vault '{}' restored from: {}", vault_name, back_fn); Ok(()) } else { @@ -166,15 +153,15 @@ pub fn restore_backup( } } -/// Clean up old backups, keeping only the most recent N -fn cleanup_old_backups( +/// Clean up old backups, keeping recent N backs +fn clean_obs( vault_name: &str, max_backups: usize, ) -> Result<(), Box> { - let backups = list_backups(vault_name)?; + let backups = ls_backs(vault_name)?; if backups.len() > max_backups { - let backup_dir = get_vault_backup_dir(vault_name); + let backup_dir = gvback_dir(vault_name); let to_delete = &backups[max_backups..]; for (filename, _, _) in to_delete { @@ -187,13 +174,12 @@ fn cleanup_old_backups( Ok(()) } -/// Export vault to external location (encrypted) +/// Export vault to external location pub fn export_vault( vault_name: &str, password: &str, output_path: &str, ) -> Result<(), Box> { - // Verify password let _vault = storage::ld_vt(password)?; let vault_path = crate::config::get_vault_path(vault_name); @@ -206,12 +192,12 @@ pub fn export_vault( println!("[✔] Vault '{}' exported to: {}", vault_name, output_path); println!("[✔] Format: Encrypted PassLock vault"); - println!("[!] Keep this file secure!"); + println!("[!] Keep this file secure."); Ok(()) } -/// Import vault from external location (encrypted) +/// Import vault from external location pub fn import_vault( vault_name: &str, password: &str, @@ -230,11 +216,9 @@ pub fn import_vault( println!("[...] Verifying import file..."); - // Try to decrypt if storage::ld_vt(password).is_ok() { fs::remove_file(&temp_path)?; - // Safety backup if vault_path.exists() { let safety_backup = crate::config::get_passlock_dir() .join(format!("{}.before_import", vault_name)); @@ -242,7 +226,6 @@ pub fn import_vault( println!("[✔] Current vault backed up: {}.before_import", vault_name); } - // Import fs::copy(&import_path, &vault_path)?; println!("[✔] Vault '{}' imported from: {}", vault_name, input_path); @@ -253,8 +236,8 @@ pub fn import_vault( } } -/// Export to CSV (PLAINTEXT!) -pub fn export_to_csv( +/// Export to CSV +pub fn export_csv( vault_name: &str, password: &str, output_path: &str, @@ -264,30 +247,30 @@ pub fn export_to_csv( let mut csv_content = String::from("name,username,password,url,notes,tags,2fa_secret\n"); for entry in &vault.e { - let name = escape_csv(&entry.n); - let username = escape_csv(&entry.u); - let password_val = escape_csv(&entry.p); - let url = entry.url.as_ref().map_or(String::new(), |u| escape_csv(u)); - let notes = entry.nt.as_ref().map_or(String::new(), |n| escape_csv(n)); - let tags = escape_csv(&entry.tags.join(";")); - let totp = entry.totp_secret.as_ref().map_or(String::new(), |t| escape_csv(t)); + let name = esc_csv(&entry.n); + let username = esc_csv(&entry.u); + let password_val = esc_csv(&entry.p); + let url = entry.url.as_ref().map_or(String::new(), |u| esc_csv(u)); + let notes = entry.nt.as_ref().map_or(String::new(), |n| esc_csv(n)); + let tags = esc_csv(&entry.tags.join(";")); + let totp = entry.totp_secret.as_ref().map_or(String::new(), |t| esc_csv(t)); csv_content.push_str(&format!( - "{name},{username},{password_val},{url},{notes},{tags},{totp}\n" + "{name},{username},{password_val},{url},{totp},{tags},{notes}\n" )); } fs::write(output_path, csv_content)?; println!("[✔] Vault '{}' exported to CSV: {}", vault_name, output_path); - println!("[!] WARNING: PLAINTEXT FILE - DELETE AFTER USE!"); + println!("[!] WARNING: This is Plaintext, delete after use."); println!("[✔] {} entries exported", vault.e.len()); Ok(()) } -/// Export to JSON (PLAINTEXT!) -pub fn export_to_json( +/// Export to JSON +pub fn export_json( vault_name: &str, password: &str, output_path: &str, @@ -327,14 +310,14 @@ pub fn export_to_json( fs::write(output_path, json_string)?; println!("[✔] Vault '{}' exported to JSON: {}", vault_name, output_path); - println!("[!] WARNING: PLAINTEXT FILE - DELETE AFTER USE!"); + println!("[!] WARNING: This is Plaintext, delete after use."); println!("[✔] {} entries exported", vault.e.len()); Ok(()) } /// Import from CSV -pub fn import_from_csv( +pub fn import_csv( vault_name: &str, password: &str, input_path: &str, @@ -348,7 +331,6 @@ pub fn import_from_csv( let reader = BufReader::new(file); let mut lines = reader.lines(); - // Skip header if let Some(Ok(header)) = lines.next() { if !header.contains("name") || !header.contains("password") { return Err("Invalid CSV format".into()); @@ -401,7 +383,6 @@ pub fn import_from_csv( println!("[!] Skipped: {} invalid entries", skipped_count); } - // Auto-backup after import let config = crate::config::load_config()?; if config.auto_backup { create_backup(vault_name, config.max_backups, false)?; @@ -411,7 +392,7 @@ pub fn import_from_csv( } /// Import from JSON -pub fn import_from_json( +pub fn import_json( vault_name: &str, password: &str, input_path: &str, @@ -459,7 +440,6 @@ pub fn import_from_json( println!("[✔] Import completed to vault '{}'", vault_name); println!("[✔] Imported: {} entries", imported_count); - // Auto-backup after import let config = crate::config::load_config()?; if config.auto_backup { create_backup(vault_name, config.max_backups, false)?; @@ -468,7 +448,7 @@ pub fn import_from_json( Ok(()) } -fn escape_csv(field: &str) -> String { +fn esc_csv(field: &str) -> String { if field.contains(',') || field.contains('"') || field.contains('\n') { format!("\"{}\"", field.replace('"', "\"\"")) } else { diff --git a/src/config.rs b/src/config.rs index c9a3090..03e0f68 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,18 +2,12 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; -/// Configuration for PassLock #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Config { - /// Currently active vault name pub active_vault: String, - /// Auto-backup on every save (if false, only manual backups) pub auto_backup: bool, - /// Maximum number of backups to keep per vault pub max_backups: usize, - /// Clipboard timeout in seconds pub clipboard_timeout: u64, - /// UI refresh rate in milliseconds pub refresh_rate: u64, } @@ -29,45 +23,37 @@ impl Default for Config { } } -/// Get the PassLock root directory: ~/.passlock/ pub fn get_passlock_dir() -> PathBuf { let home = dirs::home_dir().expect("Could not find home directory"); home.join(".passlock") } -/// Get the vaults directory: ~/.passlock/vaults/ pub fn get_vaults_dir() -> PathBuf { get_passlock_dir().join("vaults") } -/// Get the config file path: ~/.passlock/config.json pub fn get_config_path() -> PathBuf { get_passlock_dir().join("config.json") } -/// Get the path to a specific vault file pub fn get_vault_path(vault_name: &str) -> PathBuf { get_vaults_dir().join(format!("{}.vault", vault_name)) } -/// Initialize the PassLock directory structure pub fn init_passlock_dirs() -> Result<(), Box> { let passlock_dir = get_passlock_dir(); let vaults_dir = get_vaults_dir(); - // Create main directory if !passlock_dir.exists() { fs::create_dir_all(&passlock_dir)?; println!("[✔] Created PassLock directory: {}", passlock_dir.display()); } - // Create vaults directory if !vaults_dir.exists() { fs::create_dir_all(&vaults_dir)?; println!("[✔] Created vaults directory: {}", vaults_dir.display()); } - // Check for old vault file and migrate it let home = dirs::home_dir().expect("Could not find home directory"); let old_vault = home.join(".passlock.vault"); if old_vault.exists() { @@ -82,7 +68,6 @@ pub fn init_passlock_dirs() -> Result<(), Box> { Ok(()) } -/// Load configuration from file pub fn load_config() -> Result> { let config_path = get_config_path(); @@ -91,14 +76,12 @@ pub fn load_config() -> Result> { let config: Config = serde_json::from_str(&config_str)?; Ok(config) } else { - // Create default config let config = Config::default(); save_config(&config)?; Ok(config) } } -/// Save configuration to file pub fn save_config(config: &Config) -> Result<(), Box> { let config_path = get_config_path(); let config_str = serde_json::to_string_pretty(config)?; @@ -106,7 +89,6 @@ pub fn save_config(config: &Config) -> Result<(), Box> { Ok(()) } -/// List all available vaults pub fn list_vaults() -> Result, Box> { let vaults_dir = get_vaults_dir(); @@ -131,12 +113,10 @@ pub fn list_vaults() -> Result, Box> { Ok(vaults) } -/// Check if a vault exists pub fn vault_exists(vault_name: &str) -> bool { get_vault_path(vault_name).exists() } -/// Delete a vault and its backups pub fn delete_vault(vault_name: &str) -> Result<(), Box> { let vault_path = get_vault_path(vault_name); @@ -144,11 +124,9 @@ pub fn delete_vault(vault_name: &str) -> Result<(), Box> return Err(format!("Vault '{}' does not exist", vault_name).into()); } - // Delete vault file fs::remove_file(&vault_path)?; - // Delete backups - let backup_dir = crate::backup::get_vault_backup_dir(vault_name); + let backup_dir = crate::backup::gvback_dir(vault_name); if backup_dir.exists() { fs::remove_dir_all(&backup_dir)?; } @@ -159,7 +137,6 @@ pub fn delete_vault(vault_name: &str) -> Result<(), Box> Ok(()) } -/// Rename a vault pub fn rename_vault(old_name: &str, new_name: &str) -> Result<(), Box> { let old_path = get_vault_path(old_name); let new_path = get_vault_path(new_name); @@ -174,9 +151,8 @@ pub fn rename_vault(old_name: &str, new_name: &str) -> Result<(), Box u64 { fn main() -> Result<(), Box> { crypto::init_crypto()?; - // Initialize PassLock directory structure and config config::init_passlock_dirs()?; - backup::init_backup_system()?; + backup::init_bsys()?; - // Load config to get active vault name let cfg = config::load_config().unwrap_or_default(); let active_vault = cfg.active_vault.clone(); @@ -76,17 +74,18 @@ fn main() -> Result<(), Box> { let password = &args[2]; sync_vault(password)?; } - // NEW: Backup commands "backup" => { handle_backup_cmd(&args[2..])?; } - // NEW: Export vault to external file (ENCRYPTED - for backup) + "vault" => { + handle_vault_cmd(&args[2..])?; + } "export" => { if args.len() < 4 { - eprintln!("Usage: passlock export "); + eprintln!("Usage: passlock export "); eprintln!(); eprintln!("Export encrypted vault for backup/transfer."); - eprintln!("Works with ANY path on ANY device!"); + eprintln!("Works with any path, also across connected devices."); eprintln!(); eprintln!("Examples:"); eprintln!(" passlock export myPass123 ~/Documents/backup.vault"); @@ -98,13 +97,12 @@ fn main() -> Result<(), Box> { let output_file = &args[3]; backup::export_vault(&active_vault, password, output_file)?; } - // NEW: Import vault from external file (ENCRYPTED - for restore) "import" => { if args.len() < 4 { - eprintln!("Usage: passlock import "); + eprintln!("Usage: passlock import "); eprintln!(); eprintln!("Import encrypted vault from backup."); - eprintln!("Works with ANY path on ANY device!"); + eprintln!("Works with any path, also across connected devices."); eprintln!(); eprintln!("Examples:"); eprintln!(" passlock import myPass123 ~/Documents/backup.vault"); @@ -115,12 +113,11 @@ fn main() -> Result<(), Box> { let input_file = &args[3]; backup::import_vault(&active_vault, password, input_file)?; } - // NEW: Export to CSV (PLAINTEXT - for other password managers) "export-csv" => { if args.len() < 4 { - eprintln!("Usage: passlock export-csv "); + eprintln!("Usage: passlock export-csv "); eprintln!(); - eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); + eprintln!("WARNING: Creates unencrypted CSV file."); eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); eprintln!(); eprintln!("Example:"); @@ -129,14 +126,13 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let output_file = &args[3]; - backup::export_to_csv(&active_vault, password, output_file)?; + backup::export_csv(&active_vault, password, output_file)?; } - // NEW: Export to JSON (PLAINTEXT - for other password managers) "export-json" => { if args.len() < 4 { - eprintln!("Usage: passlock export-json "); + eprintln!("Usage: passlock export-json "); eprintln!(); - eprintln!("WARNING: Creates UNENCRYPTED JSON with plaintext passwords!"); + eprintln!("WARNING: Creates unencrypted JSON file."); eprintln!("Use for importing to other password managers."); eprintln!(); eprintln!("Example:"); @@ -145,12 +141,11 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let output_file = &args[3]; - backup::export_to_json(&active_vault, password, output_file)?; + backup::export_json(&active_vault, password, output_file)?; } - // NEW: Import from CSV (PLAINTEXT - from other password managers) "import-csv" => { if args.len() < 4 { - eprintln!("Usage: passlock import-csv "); + eprintln!("Usage: passlock import-csv "); eprintln!(); eprintln!("Import passwords from CSV file (LastPass, Bitwarden, etc.)"); eprintln!("Expected format: name,username,password,url,notes,tags,2fa_secret"); @@ -161,12 +156,11 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let input_file = &args[3]; - backup::import_from_csv(&active_vault, password, input_file)?; + backup::import_csv(&active_vault, password, input_file)?; } - // NEW: Import from JSON (PLAINTEXT - from other password managers) "import-json" => { if args.len() < 4 { - eprintln!("Usage: passlock import-json "); + eprintln!("Usage: passlock import-json "); eprintln!(); eprintln!("Import passwords from JSON file."); eprintln!(); @@ -176,7 +170,7 @@ fn main() -> Result<(), Box> { } let password = &args[2]; let input_file = &args[3]; - backup::import_from_json(&active_vault, password, input_file)?; + backup::import_json(&active_vault, password, input_file)?; } "info" => { handle_icmd(&args[2..])?; @@ -218,7 +212,6 @@ fn create_vault(password: &str) -> Result<(), Box> { println!("[✔] Vault created successfully."); - // Create initial backup (force=true for first backup) println!("[...] Creating initial backup..."); backup::create_backup("personal", 10, true)?; @@ -247,7 +240,6 @@ fn sync_vault(password: &str) -> Result<(), Box> { println!("[✔] Vault synced successfully."); - // Automatic backup after sync (smart backup) println!("[...] Creating backup..."); let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; @@ -283,18 +275,16 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> } let password = &args[1]; - // Verify password before backup let _vault = storage::ld_vt(password)?; println!("[✔] Password verified"); - // Create manual backup (force=true) let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, true)?; println!("[✔] Manual backup created successfully"); } "list" => { let cfg = config::load_config().unwrap_or_default(); - let backups = backup::list_backups(&cfg.active_vault)?; + let backups = backup::ls_backs(&cfg.active_vault)?; if backups.is_empty() { println!("[!] No backups found for vault '{}'", cfg.active_vault); @@ -321,12 +311,12 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Total backups: {}", backups.len()); println!(); println!("Restore a backup with:"); - println!(" passlock backup restore "); + println!(" passlock backup restore "); } } "restore" => { if args.len() < 3 { - eprintln!("Usage: passlock backup restore "); + eprintln!("Usage: passlock backup restore "); eprintln!(); eprintln!("Example:"); eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); @@ -349,6 +339,195 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> Ok(()) } +/// Handle vault management subcommands +fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { + if args.is_empty() { + println!("Vault Management Commands:"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!(); + println!("Usage: passlock vault "); + println!(); + println!("Subcommands:"); + println!(" create Create new vault"); + println!(" list List all vaults"); + println!(" use Set active vault"); + println!(" info Show current vault info"); + println!(" delete Delete vault"); + println!(" rename Rename vault"); + println!(); + println!("Examples:"); + println!(" passlock vault create personal myPass123"); + println!(" passlock vault create work workPass456"); + println!(" passlock vault list"); + println!(" passlock vault use work"); + return Ok(()); + } + + match args[0].as_str() { + "create" => { + if args.len() < 3 { + eprintln!("Usage: passlock vault create "); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock vault create personal myPassword123"); + std::process::exit(1); + } + let vault_name = &args[1]; + let password = &args[2]; + + if config::vault_exists(vault_name) { + return Err(format!("Vault '{}' already exists", vault_name).into()); + } + + let salt = crypto::gen_salt(); + let vault = Vault::new(salt); + + storage::save_vault_to(vault_name, &vault, password)?; + + println!("[✔] Vault '{}' created successfully", vault_name); + + println!("[...] Creating initial backup..."); + backup::create_backup(vault_name, 10, true)?; + + let mut cfg = config::load_config()?; + cfg.active_vault = vault_name.to_string(); + config::save_config(&cfg)?; + println!("[✔] Vault '{}' set as active", vault_name); + } + "list" => { + let vaults = config::list_vaults()?; + let cfg = config::load_config()?; + + if vaults.is_empty() { + println!("[!] No vaults found"); + println!(); + println!("Create your first vault with:"); + println!(" passlock vault create "); + } else { + println!("Available Vaults:"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!(); + for vault_name in vaults { + let marker = if vault_name == cfg.active_vault { " (active)" } else { "" }; + println!(" • {}{}", vault_name, marker); + } + println!(); + println!("Switch vault with: passlock vault use "); + } + } + "use" => { + if args.len() < 2 { + eprintln!("Usage: passlock vault use "); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock vault use work"); + std::process::exit(1); + } + let vault_name = &args[1]; + + if !config::vault_exists(vault_name) { + return Err(format!("Vault '{}' does not exist", vault_name).into()); + } + + let mut cfg = config::load_config()?; + cfg.active_vault = vault_name.to_string(); + config::save_config(&cfg)?; + + println!("[✔] Active vault set to: {}", vault_name); + } + "info" => { + let cfg = config::load_config()?; + let vaults = config::list_vaults()?; + + println!("Vault Information:"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!(); + println!("Active vault: {}", cfg.active_vault); + println!("Total vaults: {}", vaults.len()); + println!(); + + if config::vault_exists(&cfg.active_vault) { + let vault_path = config::get_vault_path(&cfg.active_vault); + if let Ok(metadata) = std::fs::metadata(&vault_path) { + let size_kb = metadata.len() / 1024; + if size_kb > 0 { + println!("Vault size: {} KB", size_kb); + } else { + println!("Vault size: {} bytes", metadata.len()); + } + } + + let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); + println!("Backups: {} available", backups.len()); + } else { + println!("Active vault '{}' not found!", cfg.active_vault); + } + + println!(); + println!("Configuration:"); + println!(" Auto-backup: {}", cfg.auto_backup); + println!(" Max backups: {}", cfg.max_backups); + println!(" Clipboard timeout: {}s", cfg.clipboard_timeout); + println!(" Refresh rate: {}ms", cfg.refresh_rate); + } + "delete" => { + if args.len() < 2 { + eprintln!("Usage: passlock vault delete "); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock vault delete old-vault"); + std::process::exit(1); + } + let vault_name = &args[1]; + + let cfg = config::load_config()?; + if vault_name == &cfg.active_vault { + return Err("Cannot delete active vault! Switch to another vault first.".into()); + } + + println!("WARNING: This will delete vault '{}' and all its backups", vault_name); + println!("Are you sure? (y/n)"); + + use std::io::{self, BufRead}; + let stdin = io::stdin(); + let mut line = String::new(); + stdin.lock().read_line(&mut line)?; + + if line.trim().to_lowercase() == "y" { + config::delete_vault(vault_name)?; + } else { + println!("[!] Deletion cancelled"); + } + } + "rename" => { + if args.len() < 3 { + eprintln!("Usage: passlock vault rename "); + eprintln!(); + eprintln!("Example:"); + eprintln!(" passlock vault rename work company"); + std::process::exit(1); + } + let old_name = &args[1]; + let new_name = &args[2]; + + config::rename_vault(old_name, new_name)?; + + let mut cfg = config::load_config()?; + if cfg.active_vault == *old_name { + cfg.active_vault = new_name.to_string(); + config::save_config(&cfg)?; + } + } + _ => { + println!("Unknown vault subcommand: {}", args[0]); + println!(); + println!("Available subcommands: create, list, use, info, delete, rename"); + } + } + + Ok(()) +} + #[allow(clippy::unnecessary_wraps)] fn handle_icmd(args: &[String]) -> Result<(), Box> { if args.is_empty() || args[0] == "cpu" { @@ -388,9 +567,8 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } - // Show backup info let cfg = config::load_config().unwrap_or_default(); - let backups = backup::list_backups(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); + let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); println!(" Backups: {} available (vault: {})", backups.len(), cfg.active_vault); } else { println!("Vault Status:"); @@ -423,6 +601,14 @@ fn print_usage() { println!(" sync Sync vault changes"); println!(" tui Launch TUI interface"); println!(); + println!("VAULT MANAGEMENT:"); + println!(" vault create Create new vault"); + println!(" vault list List all vaults"); + println!(" vault use Set active vault"); + println!(" vault info Show vault info"); + println!(" vault delete Delete vault"); + println!(" vault rename Rename vault"); + println!(); println!("BACKUP COMMANDS (Encrypted):"); println!(" backup create Create manual backup"); println!(" backup list List all backups"); @@ -442,23 +628,5 @@ fn print_usage() { println!(" version Show version"); println!(" help Show this help"); println!(); - println!("EXAMPLES:"); - println!(); - println!(" # Create and use vault"); - println!(" passlock create mySecurePassword123"); - println!(" passlock tui"); - println!(); - println!(" # Backup to any location (works everywhere!)"); - println!(" passlock export myPass123 ~/Documents/backup.vault"); - println!(" passlock export myPass123 /media/usb/backup.vault"); - println!(" passlock export myPass123 ~/Dropbox/backup.vault"); - println!(); - println!(" # Migrate from LastPass/Bitwarden"); - println!(" passlock import-csv myPass123 ~/lastpass-export.csv"); - println!(); - println!(" # Migrate to another password manager"); - println!(" passlock export-csv myPass123 ~/passwords.csv"); - println!(" # (Then import into other PM and DELETE the CSV!)"); - println!(); println!("For more info: https://github.com/hachimamma/Passlock"); } \ No newline at end of file diff --git a/src/storage.rs b/src/storage.rs index 8d918b3..e107d14 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -3,57 +3,97 @@ use crate::models::Vault; use std::fs; use std::path::PathBuf; -fn vt_p() -> PathBuf { - let home = dirs::home_dir().expect("no home"); - home.join(".passlock.vault") -} - -fn tmp_p() -> PathBuf { - let home = dirs::home_dir().expect("no home"); - home.join(".passlock.temp") +/// Get vault path for a specific vault register +fn vt_p_named(vault_name: &str) -> PathBuf { + crate::config::get_vault_path(vault_name) } -pub fn svv(v: &Vault, pwd: &str) -> Result<(), String> { +/// Save vault to a specific vault register +pub fn svv_named(vault_name: &str, v: &Vault, pwd: &str) -> Result<(), String> { + let vault_path = vt_p_named(vault_name); + + if let Some(parent) = vault_path.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let j = serde_json::to_string(v).map_err(|e| e.to_string())?; let j_bytes = j.as_bytes(); - let enc_d = crypto::enc(j_bytes, pwd, &v.s)?; - let salt_bytes = hex::decode(&v.s).map_err(|_| "Invalid salt")?; - let mut final_data = Vec::new(); - final_data.extend_from_slice(&salt_bytes); - final_data.extend_from_slice(&enc_d); - - fs::write(vt_p(), final_data).map_err(|e| e.to_string())?; - + let mut __final_data__ = Vec::new(); + __final_data__.extend_from_slice(&salt_bytes); + __final_data__.extend_from_slice(&enc_d); + fs::write(vault_path, __final_data__).map_err(|e| e.to_string())?; + let tmp_j = serde_json::to_string(v).map_err(|e| e.to_string())?; fs::write(tmp_p(), tmp_j).map_err(|e| e.to_string())?; - + Ok(()) } -pub fn ld_vt(pwd: &str) -> Result { - let data = fs::read(vt_p()).map_err(|_| "vault not found")?; - +/// Load vault from a specific vault register +pub fn ld_vt_named(vault_name: &str, pwd: &str) -> Result { + let vault_path = vt_p_named(vault_name); + + if !vault_path.exists() { + return Err(format!("Vault '{}' not found", vault_name)); + } + + let data = fs::read(vault_path).map_err(|_| "vault not found")?; if data.len() < 16 { return Err("corrupt vault".to_string()); } - let salt_bytes = &data[0..16]; let enc_data = &data[16..]; let salt = hex::encode(salt_bytes); - let dec_data = crypto::dec(enc_data, pwd, &salt)?; let dec_str = String::from_utf8(dec_data).map_err(|_| "invalid data")?; - let v: Vault = serde_json::from_str(&dec_str).map_err(|e| e.to_string())?; - + let tmp_j = serde_json::to_string(&v).map_err(|e| e.to_string())?; fs::write(tmp_p(), tmp_j).map_err(|e| e.to_string())?; - + Ok(v) } -pub fn vt_exi() -> bool { - vt_p().exists() +/// Check for a specific vault +pub fn vt_exi_named(vault_name: &str) -> bool { + vt_p_named(vault_name).exists() +} + +/// Helper for vault creation +pub fn save_vault_to(vault_name: &str, v: &Vault, pwd: &str) -> Result<(), String> { + svv_named(vault_name, v, pwd) +} + +// backward compatible + +/// Get vault path +fn _vt_p() -> PathBuf { + let cfg = crate::config::load_config().unwrap_or_default(); + vt_p_named(&cfg.active_vault) +} + +/// Get temp file path +fn tmp_p() -> PathBuf { + let home = dirs::home_dir().expect("no home"); + home.join(".passlock.temp") +} + +/// Save vault +pub fn svv(v: &Vault, pwd: &str) -> Result<(), String> { + let cfg = crate::config::load_config().unwrap_or_default(); + svv_named(&cfg.active_vault, v, pwd) } + +/// Load vault +pub fn ld_vt(pwd: &str) -> Result { + let cfg = crate::config::load_config().unwrap_or_default(); + ld_vt_named(&cfg.active_vault, pwd) +} + +/// Check if vault exists +pub fn vt_exi() -> bool { + let cfg = crate::config::load_config().unwrap_or_default(); + vt_exi_named(&cfg.active_vault) +} \ No newline at end of file From e9ad97bac1db3e57b1e571fed38958d642d92905 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 10:52:07 +0530 Subject: [PATCH 40/48] FIX: cargo fmt and cargo clippy --- src/backup.rs | 197 +++++++++++++++++++++++-------------------------- src/config.rs | 58 +++++++-------- src/main.rs | 129 ++++++++++++++++++-------------- src/storage.rs | 20 ++--- 4 files changed, 205 insertions(+), 199 deletions(-) diff --git a/src/backup.rs b/src/backup.rs index 1b584da..dc77175 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -1,7 +1,14 @@ +use crate::models::Entry; use crate::storage; +use chrono::{DateTime, Local}; +use std::fmt::Write; use std::fs; use std::path::PathBuf; +// Type alias for complex return type +type BackupInfo = Vec<(String, u64, String)>; +type Result = std::result::Result>; + /// Gets the backup directory path pub fn gback_dir() -> PathBuf { crate::config::get_passlock_dir().join("backups") @@ -13,32 +20,28 @@ pub fn gvback_dir(vault_name: &str) -> PathBuf { } /// Initialize backup directory structure -pub fn init_bsys() -> Result<(), Box> { +pub fn init_bsys() -> Result<()> { let backup_dir = gback_dir(); - + if !backup_dir.exists() { fs::create_dir_all(&backup_dir)?; println!("[✔] Created backup directory: {}", backup_dir.display()); } - + Ok(()) } /// Create a timestamped backup of the vault -/// +/// /// Only creates a new backup if the vault has actually changed /// since the last backup -/// -/// Returns: (back_fn, was_actually_created) -pub fn create_backup( - vault_name: &str, - max_backups: usize, - force: bool, -) -> Result<(String, bool), Box> { +/// +/// Returns: (`back_fn`, `was_actually_created`) +pub fn create_backup(vault_name: &str, max_backups: usize, force: bool) -> Result<(String, bool)> { let vault_path = crate::config::get_vault_path(vault_name); if !vault_path.exists() { - return Err(format!("Vault '{}' not found", vault_name).into()); + return Err(format!("Vault '{vault_name}' not found").into()); } if !force { @@ -48,10 +51,10 @@ pub fn create_backup( if !backups.is_empty() { let last_back = &backups[0]; let lb_path = backup_dir.join(&last_back.0); - + let vault_meta = fs::metadata(&vault_path)?; let backup_meta = fs::metadata(&lb_path)?; - + if vault_meta.len() == backup_meta.len() { println!("[✔] Backup skipped (no changes detected)"); return Ok((last_back.0.clone(), false)); @@ -60,7 +63,6 @@ pub fn create_backup( } } - use chrono::Local; let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); let back_fn = format!("backup_{timestamp}.vault"); @@ -79,9 +81,7 @@ pub fn create_backup( } /// List all backups for a vault -pub fn ls_backs( - vault_name: &str, -) -> Result, Box> { +pub fn ls_backs(vault_name: &str) -> Result { let backup_dir = gvback_dir(vault_name); if !backup_dir.exists() { @@ -99,7 +99,6 @@ pub fn ls_backs( let filename = path.file_name().unwrap().to_string_lossy().to_string(); let size = metadata.len(); - use chrono::{DateTime, Local}; let created = metadata.created()?; let datetime: DateTime = created.into(); let created_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); @@ -114,11 +113,7 @@ pub fn ls_backs( } /// Restore vault from a back -pub fn restore_backup( - vault_name: &str, - back_fn: &str, - password: &str, -) -> Result<(), Box> { +pub fn restore_backup(vault_name: &str, back_fn: &str, password: &str) -> Result<()> { let backup_dir = gvback_dir(vault_name); let backup_path = backup_dir.join(back_fn); @@ -129,22 +124,22 @@ pub fn restore_backup( println!("[...] Verifying backup with password..."); let vault_path = crate::config::get_vault_path(vault_name); - let temp_path = crate::config::get_passlock_dir().join(format!("{}.temp", vault_name)); - + let temp_path = crate::config::get_passlock_dir().join(format!("{vault_name}.temp")); + fs::copy(&backup_path, &temp_path)?; if storage::ld_vt(password).is_ok() { fs::remove_file(&temp_path)?; if vault_path.exists() { - let safety_backup = crate::config::get_passlock_dir() - .join(format!("{}.before_restore", vault_name)); + let safety_backup = + crate::config::get_passlock_dir().join(format!("{vault_name}.before_restore")); fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up to: {}.before_restore", vault_name); + println!("[✔] Current vault backed up to: {vault_name}.before_restore"); } fs::copy(&backup_path, &vault_path)?; - println!("[✔] Vault '{}' restored from: {}", vault_name, back_fn); + println!("[✔] Vault '{vault_name}' restored from: {back_fn}"); Ok(()) } else { @@ -154,10 +149,7 @@ pub fn restore_backup( } /// Clean up old backups, keeping recent N backs -fn clean_obs( - vault_name: &str, - max_backups: usize, -) -> Result<(), Box> { +fn clean_obs(vault_name: &str, max_backups: usize) -> Result<()> { let backups = ls_backs(vault_name)?; if backups.len() > max_backups { @@ -175,22 +167,18 @@ fn clean_obs( } /// Export vault to external location -pub fn export_vault( - vault_name: &str, - password: &str, - output_path: &str, -) -> Result<(), Box> { +pub fn export_vault(vault_name: &str, password: &str, output_path: &str) -> Result<()> { let _vault = storage::ld_vt(password)?; let vault_path = crate::config::get_vault_path(vault_name); if !vault_path.exists() { - return Err(format!("Vault '{}' not found", vault_name).into()); + return Err(format!("Vault '{vault_name}' not found").into()); } fs::copy(&vault_path, output_path)?; - println!("[✔] Vault '{}' exported to: {}", vault_name, output_path); + println!("[✔] Vault '{vault_name}' exported to: {output_path}"); println!("[✔] Format: Encrypted PassLock vault"); println!("[!] Keep this file secure."); @@ -198,11 +186,7 @@ pub fn export_vault( } /// Import vault from external location -pub fn import_vault( - vault_name: &str, - password: &str, - input_path: &str, -) -> Result<(), Box> { +pub fn import_vault(vault_name: &str, password: &str, input_path: &str) -> Result<()> { let import_path = PathBuf::from(input_path); if !import_path.exists() { @@ -210,8 +194,8 @@ pub fn import_vault( } let vault_path = crate::config::get_vault_path(vault_name); - let temp_path = crate::config::get_passlock_dir().join(format!("{}.import_test", vault_name)); - + let temp_path = crate::config::get_passlock_dir().join(format!("{vault_name}.import_test")); + fs::copy(&import_path, &temp_path)?; println!("[...] Verifying import file..."); @@ -220,14 +204,14 @@ pub fn import_vault( fs::remove_file(&temp_path)?; if vault_path.exists() { - let safety_backup = crate::config::get_passlock_dir() - .join(format!("{}.before_import", vault_name)); + let safety_backup = + crate::config::get_passlock_dir().join(format!("{vault_name}.before_import")); fs::copy(&vault_path, &safety_backup)?; - println!("[✔] Current vault backed up: {}.before_import", vault_name); + println!("[✔] Current vault backed up: {vault_name}.before_import"); } fs::copy(&import_path, &vault_path)?; - println!("[✔] Vault '{}' imported from: {}", vault_name, input_path); + println!("[✔] Vault '{vault_name}' imported from: {input_path}"); Ok(()) } else { @@ -237,11 +221,7 @@ pub fn import_vault( } /// Export to CSV -pub fn export_csv( - vault_name: &str, - password: &str, - output_path: &str, -) -> Result<(), Box> { +pub fn export_csv(vault_name: &str, password: &str, output_path: &str) -> Result<()> { let vault = storage::ld_vt(password)?; let mut csv_content = String::from("name,username,password,url,notes,tags,2fa_secret\n"); @@ -253,16 +233,20 @@ pub fn export_csv( let url = entry.url.as_ref().map_or(String::new(), |u| esc_csv(u)); let notes = entry.nt.as_ref().map_or(String::new(), |n| esc_csv(n)); let tags = esc_csv(&entry.tags.join(";")); - let totp = entry.totp_secret.as_ref().map_or(String::new(), |t| esc_csv(t)); - - csv_content.push_str(&format!( - "{name},{username},{password_val},{url},{totp},{tags},{notes}\n" - )); + let totp = entry + .totp_secret + .as_ref() + .map_or(String::new(), |t| esc_csv(t)); + + writeln!( + csv_content, + "{name},{username},{password_val},{url},{totp},{tags},{notes}" + )?; } fs::write(output_path, csv_content)?; - println!("[✔] Vault '{}' exported to CSV: {}", vault_name, output_path); + println!("[✔] Vault '{vault_name}' exported to CSV: {output_path}"); println!("[!] WARNING: This is Plaintext, delete after use."); println!("[✔] {} entries exported", vault.e.len()); @@ -270,11 +254,7 @@ pub fn export_csv( } /// Export to JSON -pub fn export_json( - vault_name: &str, - password: &str, - output_path: &str, -) -> Result<(), Box> { +pub fn export_json(vault_name: &str, password: &str, output_path: &str) -> Result<()> { let vault = storage::ld_vt(password)?; let mut entries = Vec::new(); @@ -302,14 +282,14 @@ pub fn export_json( let json_output = serde_json::json!({ "passlock_version": env!("CARGO_PKG_VERSION"), "vault_name": vault_name, - "exported_at": chrono::Local::now().to_rfc3339(), + "exported_at": Local::now().to_rfc3339(), "entries": entries, }); let json_string = serde_json::to_string_pretty(&json_output)?; fs::write(output_path, json_string)?; - println!("[✔] Vault '{}' exported to JSON: {}", vault_name, output_path); + println!("[✔] Vault '{vault_name}' exported to JSON: {output_path}"); println!("[!] WARNING: This is Plaintext, delete after use."); println!("[✔] {} entries exported", vault.e.len()); @@ -317,12 +297,7 @@ pub fn export_json( } /// Import from CSV -pub fn import_csv( - vault_name: &str, - password: &str, - input_path: &str, -) -> Result<(), Box> { - use crate::models::Entry; +pub fn import_csv(vault_name: &str, password: &str, input_path: &str) -> Result<()> { use std::io::{BufRead, BufReader}; let mut vault = storage::ld_vt(password)?; @@ -360,12 +335,21 @@ pub fn import_csv( n: fields[0].clone(), u: fields.get(1).cloned().unwrap_or_default(), p: fields.get(2).cloned().unwrap_or_default(), - url: fields.get(3).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), - nt: fields.get(4).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), + url: fields + .get(3) + .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), + nt: fields + .get(4) + .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), tags: fields.get(5).map_or(Vec::new(), |s| { - s.split(';').filter(|t| !t.is_empty()).map(String::from).collect() + s.split(';') + .filter(|t| !t.is_empty()) + .map(String::from) + .collect() }), - totp_secret: fields.get(6).and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), + totp_secret: fields + .get(6) + .and_then(|s| if s.is_empty() { None } else { Some(s.clone()) }), t: crate::get_timestamp(), last_modified: crate::get_timestamp(), history: Vec::new(), @@ -377,10 +361,10 @@ pub fn import_csv( storage::svv(&vault, password)?; - println!("[✔] Import completed to vault '{}'", vault_name); - println!("[✔] Imported: {} entries", imported_count); + println!("[✔] Import completed to vault '{vault_name}'"); + println!("[✔] Imported: {imported_count} entries"); if skipped_count > 0 { - println!("[!] Skipped: {} invalid entries", skipped_count); + println!("[!] Skipped: {skipped_count} invalid entries"); } let config = crate::config::load_config()?; @@ -392,21 +376,14 @@ pub fn import_csv( } /// Import from JSON -pub fn import_json( - vault_name: &str, - password: &str, - input_path: &str, -) -> Result<(), Box> { - use crate::models::Entry; - +pub fn import_json(vault_name: &str, password: &str, input_path: &str) -> Result<()> { let mut vault = storage::ld_vt(password)?; let json_content = fs::read_to_string(input_path)?; let json: serde_json::Value = serde_json::from_str(&json_content)?; - let entries = match json.get("entries") { - Some(serde_json::Value::Array(arr)) => arr, - _ => return Err("Invalid JSON format".into()), + let Some(serde_json::Value::Array(entries)) = json.get("entries") else { + return Err("Invalid JSON format".into()); }; let mut imported_count = 0; @@ -414,16 +391,30 @@ pub fn import_json( for entry_json in entries { let entry = Entry { id: crate::generate_uuid(), - n: entry_json["name"].as_str().unwrap_or("Untitled").to_string(), + n: entry_json["name"] + .as_str() + .unwrap_or("Untitled") + .to_string(), u: entry_json["username"].as_str().unwrap_or("").to_string(), p: entry_json["password"].as_str().unwrap_or("").to_string(), - url: entry_json.get("url").and_then(|v| v.as_str()).map(String::from), - nt: entry_json.get("notes").and_then(|v| v.as_str()).map(String::from), - totp_secret: entry_json.get("totp_secret").and_then(|v| v.as_str()).map(String::from), + url: entry_json + .get("url") + .and_then(|v| v.as_str()) + .map(String::from), + nt: entry_json + .get("notes") + .and_then(|v| v.as_str()) + .map(String::from), + totp_secret: entry_json + .get("totp_secret") + .and_then(|v| v.as_str()) + .map(String::from), tags: match entry_json.get("tags") { - Some(serde_json::Value::Array(arr)) => { - arr.iter().filter_map(|v| v.as_str()).map(String::from).collect() - } + Some(serde_json::Value::Array(arr)) => arr + .iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect(), _ => Vec::new(), }, t: crate::get_timestamp(), @@ -437,8 +428,8 @@ pub fn import_json( storage::svv(&vault, password)?; - println!("[✔] Import completed to vault '{}'", vault_name); - println!("[✔] Imported: {} entries", imported_count); + println!("[✔] Import completed to vault '{vault_name}'"); + println!("[✔] Imported: {imported_count} entries"); let config = crate::config::load_config()?; if config.auto_backup { @@ -484,4 +475,4 @@ fn parse_csv_line(line: &str) -> Vec { fields.push(current_field.trim().to_string()); fields -} \ No newline at end of file +} diff --git a/src/config.rs b/src/config.rs index 03e0f68..cbec77f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,23 +37,23 @@ pub fn get_config_path() -> PathBuf { } pub fn get_vault_path(vault_name: &str) -> PathBuf { - get_vaults_dir().join(format!("{}.vault", vault_name)) + get_vaults_dir().join(format!("{vault_name}.vault")) } pub fn init_passlock_dirs() -> Result<(), Box> { let passlock_dir = get_passlock_dir(); let vaults_dir = get_vaults_dir(); - + if !passlock_dir.exists() { fs::create_dir_all(&passlock_dir)?; println!("[✔] Created PassLock directory: {}", passlock_dir.display()); } - + if !vaults_dir.exists() { fs::create_dir_all(&vaults_dir)?; println!("[✔] Created vaults directory: {}", vaults_dir.display()); } - + let home = dirs::home_dir().expect("Could not find home directory"); let old_vault = home.join(".passlock.vault"); if old_vault.exists() { @@ -64,13 +64,13 @@ pub fn init_passlock_dirs() -> Result<(), Box> { println!("[!] You can delete the old vault: {}", old_vault.display()); } } - + Ok(()) } pub fn load_config() -> Result> { let config_path = get_config_path(); - + if config_path.exists() { let config_str = fs::read_to_string(config_path)?; let config: Config = serde_json::from_str(&config_str)?; @@ -91,24 +91,24 @@ pub fn save_config(config: &Config) -> Result<(), Box> { pub fn list_vaults() -> Result, Box> { let vaults_dir = get_vaults_dir(); - + if !vaults_dir.exists() { return Ok(Vec::new()); } - + let mut vaults = Vec::new(); - + for entry in fs::read_dir(vaults_dir)? { let entry = entry?; let path = entry.path(); - + if path.extension().and_then(|s| s.to_str()) == Some("vault") { if let Some(name) = path.file_stem().and_then(|s| s.to_str()) { vaults.push(name.to_string()); } } } - + vaults.sort(); Ok(vaults) } @@ -119,45 +119,45 @@ pub fn vault_exists(vault_name: &str) -> bool { pub fn delete_vault(vault_name: &str) -> Result<(), Box> { let vault_path = get_vault_path(vault_name); - + if !vault_path.exists() { - return Err(format!("Vault '{}' does not exist", vault_name).into()); + return Err(format!("Vault '{vault_name}' does not exist").into()); } - + fs::remove_file(&vault_path)?; - + let backup_dir = crate::backup::gvback_dir(vault_name); if backup_dir.exists() { fs::remove_dir_all(&backup_dir)?; } - - println!("[✔] Deleted vault: {}", vault_name); - println!("[✔] Deleted backups for: {}", vault_name); - + + println!("[✔] Deleted vault: {vault_name}"); + println!("[✔] Deleted backups for: {vault_name}"); + Ok(()) } pub fn rename_vault(old_name: &str, new_name: &str) -> Result<(), Box> { let old_path = get_vault_path(old_name); let new_path = get_vault_path(new_name); - + if !old_path.exists() { - return Err(format!("Vault '{}' does not exist", old_name).into()); + return Err(format!("Vault '{old_name}' does not exist").into()); } - + if new_path.exists() { - return Err(format!("Vault '{}' already exists", new_name).into()); + return Err(format!("Vault '{new_name}' already exists").into()); } - + fs::rename(&old_path, &new_path)?; - + let old_backup_dir = crate::backup::gvback_dir(old_name); let new_backup_dir = crate::backup::gvback_dir(new_name); if old_backup_dir.exists() { fs::rename(&old_backup_dir, &new_backup_dir)?; } - - println!("[✔] Renamed vault: {} → {}", old_name, new_name); - + + println!("[✔] Renamed vault: {old_name} → {new_name}"); + Ok(()) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 6b4b614..2fbf2f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod vault_ffi; use models::Vault; use std::env; +use std::io::{self, BufRead}; // Moved import to top /// Generates a UUID string. /// @@ -37,12 +38,13 @@ pub fn get_timestamp() -> u64 { .as_secs() } +#[allow(clippy::too_many_lines)] fn main() -> Result<(), Box> { crypto::init_crypto()?; - + config::init_passlock_dirs()?; backup::init_bsys()?; - + let cfg = config::load_config().unwrap_or_default(); let active_vault = cfg.active_vault.clone(); @@ -118,7 +120,9 @@ fn main() -> Result<(), Box> { eprintln!("Usage: passlock export-csv "); eprintln!(); eprintln!("WARNING: Creates unencrypted CSV file."); - eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); + eprintln!( + "Use for importing to other password managers (LastPass, Bitwarden, etc.)" + ); eprintln!(); eprintln!("Example:"); eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); @@ -211,10 +215,10 @@ fn create_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault created successfully."); - + println!("[...] Creating initial backup..."); backup::create_backup("personal", 10, true)?; - + Ok(()) } @@ -239,11 +243,11 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); - + println!("[...] Creating backup..."); let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; - + Ok(()) } @@ -274,10 +278,10 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> std::process::exit(1); } let password = &args[1]; - + let _vault = storage::ld_vt(password)?; println!("[✔] Password verified"); - + let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, true)?; println!("[✔] Manual backup created successfully"); @@ -285,7 +289,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> "list" => { let cfg = config::load_config().unwrap_or_default(); let backups = backup::ls_backs(&cfg.active_vault)?; - + if backups.is_empty() { println!("[!] No backups found for vault '{}'", cfg.active_vault); println!(); @@ -297,7 +301,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!(); println!("{:<40} {:<12} Created", "Filename", "Size"); println!("{}", "─".repeat(70)); - + for (filename, size, created) in &backups { let size_kb = if *size > 1024 { format!("{} KB", *size / 1024) @@ -306,7 +310,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> }; println!("{filename:<40} {size_kb:<12} {created}"); } - + println!(); println!("Total backups: {}", backups.len()); println!(); @@ -319,12 +323,14 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> eprintln!("Usage: passlock backup restore "); eprintln!(); eprintln!("Example:"); - eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); + eprintln!( + " passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123" + ); std::process::exit(1); } let backup_name = &args[1]; let password = &args[2]; - + let cfg = config::load_config().unwrap_or_default(); backup::restore_backup(&cfg.active_vault, backup_name, password)?; println!("[✔] Backup restored successfully"); @@ -335,11 +341,12 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Available subcommands: create, list, restore"); } } - + Ok(()) } /// Handle vault management subcommands +#[allow(clippy::too_many_lines)] fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { if args.is_empty() { println!("Vault Management Commands:"); @@ -374,30 +381,30 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let vault_name = &args[1]; let password = &args[2]; - + if config::vault_exists(vault_name) { - return Err(format!("Vault '{}' already exists", vault_name).into()); + return Err(format!("Vault '{vault_name}' already exists").into()); } - + let salt = crypto::gen_salt(); let vault = Vault::new(salt); - + storage::save_vault_to(vault_name, &vault, password)?; - - println!("[✔] Vault '{}' created successfully", vault_name); - + + println!("[✔] Vault '{vault_name}' created successfully"); + println!("[...] Creating initial backup..."); backup::create_backup(vault_name, 10, true)?; - + let mut cfg = config::load_config()?; - cfg.active_vault = vault_name.to_string(); + cfg.active_vault.clone_from(vault_name); // FIXED: using clone_from config::save_config(&cfg)?; - println!("[✔] Vault '{}' set as active", vault_name); + println!("[✔] Vault '{vault_name}' set as active"); } "list" => { let vaults = config::list_vaults()?; let cfg = config::load_config()?; - + if vaults.is_empty() { println!("[!] No vaults found"); println!(); @@ -408,8 +415,12 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); for vault_name in vaults { - let marker = if vault_name == cfg.active_vault { " (active)" } else { "" }; - println!(" • {}{}", vault_name, marker); + let marker = if vault_name == cfg.active_vault { + " (active)" + } else { + "" + }; + println!(" • {vault_name}{marker}"); } println!(); println!("Switch vault with: passlock vault use "); @@ -424,45 +435,45 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { std::process::exit(1); } let vault_name = &args[1]; - + if !config::vault_exists(vault_name) { - return Err(format!("Vault '{}' does not exist", vault_name).into()); + return Err(format!("Vault '{vault_name}' does not exist").into()); } - + let mut cfg = config::load_config()?; - cfg.active_vault = vault_name.to_string(); + cfg.active_vault.clone_from(vault_name); // FIXED: using clone_from config::save_config(&cfg)?; - - println!("[✔] Active vault set to: {}", vault_name); + + println!("[✔] Active vault set to: {vault_name}"); } "info" => { let cfg = config::load_config()?; let vaults = config::list_vaults()?; - + println!("Vault Information:"); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); println!("Active vault: {}", cfg.active_vault); println!("Total vaults: {}", vaults.len()); println!(); - + if config::vault_exists(&cfg.active_vault) { let vault_path = config::get_vault_path(&cfg.active_vault); if let Ok(metadata) = std::fs::metadata(&vault_path) { let size_kb = metadata.len() / 1024; if size_kb > 0 { - println!("Vault size: {} KB", size_kb); + println!("Vault size: {size_kb} KB"); } else { println!("Vault size: {} bytes", metadata.len()); } } - + let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); println!("Backups: {} available", backups.len()); } else { println!("Active vault '{}' not found!", cfg.active_vault); } - + println!(); println!("Configuration:"); println!(" Auto-backup: {}", cfg.auto_backup); @@ -479,20 +490,20 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { std::process::exit(1); } let vault_name = &args[1]; - + let cfg = config::load_config()?; if vault_name == &cfg.active_vault { return Err("Cannot delete active vault! Switch to another vault first.".into()); } - - println!("WARNING: This will delete vault '{}' and all its backups", vault_name); + + println!("WARNING: This will delete vault '{vault_name}' and all its backups"); println!("Are you sure? (y/n)"); - - use std::io::{self, BufRead}; + + // Removed inner use statement - now using import from top let stdin = io::stdin(); let mut line = String::new(); stdin.lock().read_line(&mut line)?; - + if line.trim().to_lowercase() == "y" { config::delete_vault(vault_name)?; } else { @@ -509,12 +520,12 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let old_name = &args[1]; let new_name = &args[2]; - + config::rename_vault(old_name, new_name)?; - + let mut cfg = config::load_config()?; if cfg.active_vault == *old_name { - cfg.active_vault = new_name.to_string(); + cfg.active_vault.clone_from(new_name); // FIXED: using clone_from config::save_config(&cfg)?; } } @@ -524,7 +535,7 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("Available subcommands: create, list, use, info, delete, rename"); } } - + Ok(()) } @@ -566,10 +577,14 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } - + let cfg = config::load_config().unwrap_or_default(); let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); - println!(" Backups: {} available (vault: {})", backups.len(), cfg.active_vault); + println!( + " Backups: {} available (vault: {})", + backups.len(), + cfg.active_vault + ); } else { println!("Vault Status:"); println!(" No vault found"); @@ -602,22 +617,22 @@ fn print_usage() { println!(" tui Launch TUI interface"); println!(); println!("VAULT MANAGEMENT:"); - println!(" vault create Create new vault"); + println!(" vault create Create new vault"); println!(" vault list List all vaults"); - println!(" vault use Set active vault"); + println!(" vault use Set active vault"); println!(" vault info Show vault info"); - println!(" vault delete Delete vault"); + println!(" vault delete Delete vault"); println!(" vault rename Rename vault"); println!(); println!("BACKUP COMMANDS (Encrypted):"); println!(" backup create Create manual backup"); println!(" backup list List all backups"); - println!(" backup restore Restore from backup"); + println!(" backup restore Restore from backup"); println!(); println!(" export Export vault (encrypted)"); println!(" import Import vault (encrypted)"); println!(); - println!("PASSWORD MANAGER MIGRATION (Plaintext - USE WITH CAUTION):"); + println!("PASSWORD MANAGER MIGRATION:"); println!(" export-csv Export to CSV (for other PMs)"); println!(" export-json Export to JSON (for other PMs)"); println!(" import-csv Import from CSV"); @@ -629,4 +644,4 @@ fn print_usage() { println!(" help Show this help"); println!(); println!("For more info: https://github.com/hachimamma/Passlock"); -} \ No newline at end of file +} diff --git a/src/storage.rs b/src/storage.rs index e107d14..aac2586 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -11,11 +11,11 @@ fn vt_p_named(vault_name: &str) -> PathBuf { /// Save vault to a specific vault register pub fn svv_named(vault_name: &str, v: &Vault, pwd: &str) -> Result<(), String> { let vault_path = vt_p_named(vault_name); - + if let Some(parent) = vault_path.parent() { fs::create_dir_all(parent).map_err(|e| e.to_string())?; } - + let j = serde_json::to_string(v).map_err(|e| e.to_string())?; let j_bytes = j.as_bytes(); let enc_d = crypto::enc(j_bytes, pwd, &v.s)?; @@ -24,21 +24,21 @@ pub fn svv_named(vault_name: &str, v: &Vault, pwd: &str) -> Result<(), String> { __final_data__.extend_from_slice(&salt_bytes); __final_data__.extend_from_slice(&enc_d); fs::write(vault_path, __final_data__).map_err(|e| e.to_string())?; - + let tmp_j = serde_json::to_string(v).map_err(|e| e.to_string())?; fs::write(tmp_p(), tmp_j).map_err(|e| e.to_string())?; - + Ok(()) } /// Load vault from a specific vault register pub fn ld_vt_named(vault_name: &str, pwd: &str) -> Result { let vault_path = vt_p_named(vault_name); - + if !vault_path.exists() { - return Err(format!("Vault '{}' not found", vault_name)); + return Err(format!("Vault '{vault_name}' not found")); } - + let data = fs::read(vault_path).map_err(|_| "vault not found")?; if data.len() < 16 { return Err("corrupt vault".to_string()); @@ -49,10 +49,10 @@ pub fn ld_vt_named(vault_name: &str, pwd: &str) -> Result { let dec_data = crypto::dec(enc_data, pwd, &salt)?; let dec_str = String::from_utf8(dec_data).map_err(|_| "invalid data")?; let v: Vault = serde_json::from_str(&dec_str).map_err(|e| e.to_string())?; - + let tmp_j = serde_json::to_string(&v).map_err(|e| e.to_string())?; fs::write(tmp_p(), tmp_j).map_err(|e| e.to_string())?; - + Ok(v) } @@ -96,4 +96,4 @@ pub fn ld_vt(pwd: &str) -> Result { pub fn vt_exi() -> bool { let cfg = crate::config::load_config().unwrap_or_default(); vt_exi_named(&cfg.active_vault) -} \ No newline at end of file +} From cc8996483865bed9acf58ca22a992085b4258239 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 10:55:23 +0530 Subject: [PATCH 41/48] EDIT: Edited src/main.rs and src/backup.rs --- src/backup.rs | 1 - src/main.rs | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/backup.rs b/src/backup.rs index dc77175..b881e15 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -5,7 +5,6 @@ use std::fmt::Write; use std::fs; use std::path::PathBuf; -// Type alias for complex return type type BackupInfo = Vec<(String, u64, String)>; type Result = std::result::Result>; diff --git a/src/main.rs b/src/main.rs index 2fbf2f4..815494b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ mod vault_ffi; use models::Vault; use std::env; -use std::io::{self, BufRead}; // Moved import to top +use std::io::{self, BufRead}; /// Generates a UUID string. /// @@ -397,7 +397,7 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { backup::create_backup(vault_name, 10, true)?; let mut cfg = config::load_config()?; - cfg.active_vault.clone_from(vault_name); // FIXED: using clone_from + cfg.active_vault.clone_from(vault_name); config::save_config(&cfg)?; println!("[✔] Vault '{vault_name}' set as active"); } @@ -441,7 +441,7 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let mut cfg = config::load_config()?; - cfg.active_vault.clone_from(vault_name); // FIXED: using clone_from + cfg.active_vault.clone_from(vault_name); config::save_config(&cfg)?; println!("[✔] Active vault set to: {vault_name}"); @@ -499,7 +499,6 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("WARNING: This will delete vault '{vault_name}' and all its backups"); println!("Are you sure? (y/n)"); - // Removed inner use statement - now using import from top let stdin = io::stdin(); let mut line = String::new(); stdin.lock().read_line(&mut line)?; @@ -525,7 +524,7 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { let mut cfg = config::load_config()?; if cfg.active_vault == *old_name { - cfg.active_vault.clone_from(new_name); // FIXED: using clone_from + cfg.active_vault.clone_from(new_name); config::save_config(&cfg)?; } } From 73bc6cdf86a7ddb73e22dd2e919b8f77c4496395 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 18:45:23 +0530 Subject: [PATCH 42/48] FIX + UPDATE: Fixed backup algorithm && addest latest flag --- src/main.rs | 231 ++++++++++++++++++++++++++------------------------ src/ui/mod.rs | 6 ++ 2 files changed, 125 insertions(+), 112 deletions(-) diff --git a/src/main.rs b/src/main.rs index 815494b..18770d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ mod vault_ffi; use models::Vault; use std::env; -use std::io::{self, BufRead}; /// Generates a UUID string. /// @@ -38,13 +37,12 @@ pub fn get_timestamp() -> u64 { .as_secs() } -#[allow(clippy::too_many_lines)] fn main() -> Result<(), Box> { crypto::init_crypto()?; - + config::init_passlock_dirs()?; backup::init_bsys()?; - + let cfg = config::load_config().unwrap_or_default(); let active_vault = cfg.active_vault.clone(); @@ -52,14 +50,6 @@ fn main() -> Result<(), Box> { if args.len() > 1 { match args[1].as_str() { - "create" => { - if args.len() < 3 { - eprintln!("Usage: passlock create "); - std::process::exit(1); - } - let password = &args[2]; - create_vault(password)?; - } "unlock" => { if args.len() < 3 { eprintln!("Usage: passlock unlock "); @@ -87,7 +77,7 @@ fn main() -> Result<(), Box> { eprintln!("Usage: passlock export "); eprintln!(); eprintln!("Export encrypted vault for backup/transfer."); - eprintln!("Works with any path, also across connected devices."); + eprintln!("Works with ANY path on ANY device!"); eprintln!(); eprintln!("Examples:"); eprintln!(" passlock export myPass123 ~/Documents/backup.vault"); @@ -104,7 +94,7 @@ fn main() -> Result<(), Box> { eprintln!("Usage: passlock import "); eprintln!(); eprintln!("Import encrypted vault from backup."); - eprintln!("Works with any path, also across connected devices."); + eprintln!("Works with ANY path on ANY device!"); eprintln!(); eprintln!("Examples:"); eprintln!(" passlock import myPass123 ~/Documents/backup.vault"); @@ -117,12 +107,10 @@ fn main() -> Result<(), Box> { } "export-csv" => { if args.len() < 4 { - eprintln!("Usage: passlock export-csv "); + eprintln!("Usage: passlock export_csv "); eprintln!(); - eprintln!("WARNING: Creates unencrypted CSV file."); - eprintln!( - "Use for importing to other password managers (LastPass, Bitwarden, etc.)" - ); + eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); + eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); eprintln!(); eprintln!("Example:"); eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); @@ -134,9 +122,9 @@ fn main() -> Result<(), Box> { } "export-json" => { if args.len() < 4 { - eprintln!("Usage: passlock export-json "); + eprintln!("Usage: passlock export_json "); eprintln!(); - eprintln!("WARNING: Creates unencrypted JSON file."); + eprintln!("WARNING: Creates UNENCRYPTED JSON with plaintext passwords!"); eprintln!("Use for importing to other password managers."); eprintln!(); eprintln!("Example:"); @@ -164,7 +152,7 @@ fn main() -> Result<(), Box> { } "import-json" => { if args.len() < 4 { - eprintln!("Usage: passlock import-json "); + eprintln!("Usage: passlock import_json "); eprintln!(); eprintln!("Import passwords from JSON file."); eprintln!(); @@ -204,24 +192,6 @@ fn main() -> Result<(), Box> { Ok(()) } -fn create_vault(password: &str) -> Result<(), Box> { - if storage::vt_exi() { - return Err("Vault already exists".into()); - } - - let salt = crypto::gen_salt(); - let vault = Vault::new(salt); - - storage::svv(&vault, password)?; - - println!("[✔] Vault created successfully."); - - println!("[...] Creating initial backup..."); - backup::create_backup("personal", 10, true)?; - - Ok(()) -} - fn unlock_vault(password: &str) -> Result<(), Box> { let vault = storage::ld_vt(password)?; println!("[✔] Vault unlocked successfully."); @@ -243,11 +213,35 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); - + println!("[...] Creating backup..."); let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; + + Ok(()) +} +/// Automatically backups vault after TUI closes +/// Called by TUI when exiting +/// Only creates backup if vault was modified +pub fn auto_back() -> Result<(), Box> { + let cfg = config::load_config().unwrap_or_default(); + + if !cfg.auto_backup { + return Ok(()); + } + + if !config::vault_exists(&cfg.active_vault) { + return Ok(()); + } + + println!("auto backup in progress"); + let (backup_name, was_created) = backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; + + if was_created { + println!("auto backup completed {}", backup_name); + } + Ok(()) } @@ -262,11 +256,12 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Subcommands:"); println!(" create Create manual backup"); println!(" list List all backups"); - println!(" restore Restore from backup"); + println!(" restore Restore from backup"); println!(); println!("Examples:"); println!(" passlock backup create myPassword123"); println!(" passlock backup list"); + println!(" passlock backup restore latest myPassword123"); println!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); return Ok(()); } @@ -278,10 +273,10 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> std::process::exit(1); } let password = &args[1]; - + let _vault = storage::ld_vt(password)?; println!("[✔] Password verified"); - + let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, true)?; println!("[✔] Manual backup created successfully"); @@ -289,7 +284,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> "list" => { let cfg = config::load_config().unwrap_or_default(); let backups = backup::ls_backs(&cfg.active_vault)?; - + if backups.is_empty() { println!("[!] No backups found for vault '{}'", cfg.active_vault); println!(); @@ -301,7 +296,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!(); println!("{:<40} {:<12} Created", "Filename", "Size"); println!("{}", "─".repeat(70)); - + for (filename, size, created) in &backups { let size_kb = if *size > 1024 { format!("{} KB", *size / 1024) @@ -310,7 +305,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> }; println!("{filename:<40} {size_kb:<12} {created}"); } - + println!(); println!("Total backups: {}", backups.len()); println!(); @@ -319,21 +314,44 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> } } "restore" => { - if args.len() < 3 { - eprintln!("Usage: passlock backup restore "); + if args.len() < 2 { + eprintln!("Usage: passlock backup restore "); eprintln!(); - eprintln!("Example:"); - eprintln!( - " passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123" - ); + eprintln!("Examples:"); + eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); + eprintln!(" passlock backup restore latest myPassword123"); std::process::exit(1); } - let backup_name = &args[1]; - let password = &args[2]; - + let cfg = config::load_config().unwrap_or_default(); - backup::restore_backup(&cfg.active_vault, backup_name, password)?; - println!("[✔] Backup restored successfully"); + + if args[1] == "latest" { + if args.len() < 3 { + eprintln!("Usage: passlock backup restore latest "); + std::process::exit(1); + } + let password = &args[2]; + + let backups = backup::ls_backs(&cfg.active_vault)?; + if backups.is_empty() { + return Err("No backups found for this vault".into()); + } + + let latest_backup = &backups[0].0; + println!("[i] Restoring latest backup: {}", latest_backup); + backup::restore_backup(&cfg.active_vault, latest_backup, password)?; + println!("[✔] Vault restored to latest backup successfully"); + } else { + if args.len() < 3 { + eprintln!("Usage: passlock backup restore "); + std::process::exit(1); + } + let backup_name = &args[1]; + let password = &args[2]; + + backup::restore_backup(&cfg.active_vault, backup_name, password)?; + println!("[✔] Backup restored successfully"); + } } _ => { println!("Unknown backup subcommand: {}", args[0]); @@ -341,12 +359,11 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Available subcommands: create, list, restore"); } } - + Ok(()) } /// Handle vault management subcommands -#[allow(clippy::too_many_lines)] fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { if args.is_empty() { println!("Vault Management Commands:"); @@ -381,30 +398,28 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let vault_name = &args[1]; let password = &args[2]; - + if config::vault_exists(vault_name) { - return Err(format!("Vault '{vault_name}' already exists").into()); + return Err(format!("Vault '{}' already exists", vault_name).into()); } let salt = crypto::gen_salt(); let vault = Vault::new(salt); - + storage::save_vault_to(vault_name, &vault, password)?; - - println!("[✔] Vault '{vault_name}' created successfully"); - - println!("[...] Creating initial backup..."); - backup::create_backup(vault_name, 10, true)?; - + + println!("[✔] Vault '{}' created successfully", vault_name); + println!("[i] Backups will be created automatically as you add passwords"); + let mut cfg = config::load_config()?; - cfg.active_vault.clone_from(vault_name); + cfg.active_vault = vault_name.to_string(); config::save_config(&cfg)?; - println!("[✔] Vault '{vault_name}' set as active"); + println!("[✔] Vault '{}' set as active", vault_name); } "list" => { let vaults = config::list_vaults()?; let cfg = config::load_config()?; - + if vaults.is_empty() { println!("[!] No vaults found"); println!(); @@ -415,12 +430,8 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); for vault_name in vaults { - let marker = if vault_name == cfg.active_vault { - " (active)" - } else { - "" - }; - println!(" • {vault_name}{marker}"); + let marker = if vault_name == cfg.active_vault { " (active)" } else { "" }; + println!(" • {}{}", vault_name, marker); } println!(); println!("Switch vault with: passlock vault use "); @@ -435,45 +446,45 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { std::process::exit(1); } let vault_name = &args[1]; - + if !config::vault_exists(vault_name) { - return Err(format!("Vault '{vault_name}' does not exist").into()); + return Err(format!("Vault '{}' does not exist", vault_name).into()); } - + let mut cfg = config::load_config()?; - cfg.active_vault.clone_from(vault_name); + cfg.active_vault = vault_name.to_string(); config::save_config(&cfg)?; - - println!("[✔] Active vault set to: {vault_name}"); + + println!("[✔] Active vault set to: {}", vault_name); } "info" => { let cfg = config::load_config()?; let vaults = config::list_vaults()?; - + println!("Vault Information:"); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); println!("Active vault: {}", cfg.active_vault); println!("Total vaults: {}", vaults.len()); println!(); - + if config::vault_exists(&cfg.active_vault) { let vault_path = config::get_vault_path(&cfg.active_vault); if let Ok(metadata) = std::fs::metadata(&vault_path) { let size_kb = metadata.len() / 1024; if size_kb > 0 { - println!("Vault size: {size_kb} KB"); + println!("Vault size: {} KB", size_kb); } else { println!("Vault size: {} bytes", metadata.len()); } } - + let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); println!("Backups: {} available", backups.len()); } else { println!("Active vault '{}' not found!", cfg.active_vault); } - + println!(); println!("Configuration:"); println!(" Auto-backup: {}", cfg.auto_backup); @@ -490,19 +501,20 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { std::process::exit(1); } let vault_name = &args[1]; - + let cfg = config::load_config()?; if vault_name == &cfg.active_vault { return Err("Cannot delete active vault! Switch to another vault first.".into()); } - - println!("WARNING: This will delete vault '{vault_name}' and all its backups"); + + println!("WARNING: This will delete vault '{}' and all its backups.", vault_name); println!("Are you sure? (y/n)"); - + + use std::io::{self, BufRead}; let stdin = io::stdin(); let mut line = String::new(); stdin.lock().read_line(&mut line)?; - + if line.trim().to_lowercase() == "y" { config::delete_vault(vault_name)?; } else { @@ -519,12 +531,12 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let old_name = &args[1]; let new_name = &args[2]; - + config::rename_vault(old_name, new_name)?; - + let mut cfg = config::load_config()?; if cfg.active_vault == *old_name { - cfg.active_vault.clone_from(new_name); + cfg.active_vault = new_name.to_string(); config::save_config(&cfg)?; } } @@ -534,7 +546,7 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("Available subcommands: create, list, use, info, delete, rename"); } } - + Ok(()) } @@ -576,14 +588,10 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } - + let cfg = config::load_config().unwrap_or_default(); let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); - println!( - " Backups: {} available (vault: {})", - backups.len(), - cfg.active_vault - ); + println!(" Backups: {} available (vault: {})", backups.len(), cfg.active_vault); } else { println!("Vault Status:"); println!(" No vault found"); @@ -610,28 +618,27 @@ fn print_usage() { println!(" passlock [OPTIONS]"); println!(); println!("VAULT COMMANDS:"); - println!(" create Create a new vault"); println!(" unlock Unlock and verify vault"); println!(" sync Sync vault changes"); println!(" tui Launch TUI interface"); println!(); println!("VAULT MANAGEMENT:"); - println!(" vault create Create new vault"); + println!(" vault create Create new vault"); println!(" vault list List all vaults"); - println!(" vault use Set active vault"); + println!(" vault use Set active vault"); println!(" vault info Show vault info"); - println!(" vault delete Delete vault"); + println!(" vault delete Delete vault"); println!(" vault rename Rename vault"); println!(); println!("BACKUP COMMANDS (Encrypted):"); println!(" backup create Create manual backup"); println!(" backup list List all backups"); - println!(" backup restore Restore from backup"); + println!(" backup restore Restore from backup"); println!(); println!(" export Export vault (encrypted)"); println!(" import Import vault (encrypted)"); println!(); - println!("PASSWORD MANAGER MIGRATION:"); + println!("PASSWORD MANAGER MIGRATION (Plaintext - USE WITH CAUTION):"); println!(" export-csv Export to CSV (for other PMs)"); println!(" export-json Export to JSON (for other PMs)"); println!(" import-csv Import from CSV"); @@ -642,5 +649,5 @@ fn print_usage() { println!(" version Show version"); println!(" help Show this help"); println!(); - println!("For more info: https://github.com/hachimamma/Passlock"); -} + println!("For more info: https://github.com/hachimamma/Passlock/blob/main/README.md"); +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 65dda66..f2fc886 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -417,6 +417,9 @@ fn run_app( Screen::UnlockVault => handle_uvi(app, key_event.code), Screen::MainMenu => { if handle_mmi(app, key_event.code) { + if let Err(e) = crate::auto_back() { + eprintln!("[!] Auto-backup failed: {}", e); + } return Ok(()); } } @@ -451,6 +454,9 @@ fn run_app( Screen::ThemeSelector => handle_theme_selector(app, key_event.code), Screen::OptionsMenu => { if handle_options_menu(app, key_event.code) { + if let Err(e) = crate::auto_back() { + eprintln!("[!] Auto-backup failed: {}", e); + } return Ok(()); } } From cecf242ccd7c65eac40acbe60b7cc5112d2e8371 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 18:50:44 +0530 Subject: [PATCH 43/48] FIX: Removed deprecated function --- src/main.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index de59d95..5060ef1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -192,20 +192,6 @@ fn main() -> Result<(), Box> { Ok(()) } -fn create_vault(password: &str) -> Result<(), Box> { - if storage::vt_exi() { - return Err("Vault already exists".into()); - } - - let salt = crypto::gen_salt(); - let vault = Vault::new(salt); - - storage::svv(&vault, password)?; - - println!("[✔] Vault created successfully."); - Ok(()) -} - fn unlock_vault(password: &str) -> Result<(), Box> { let vault = storage::ld_vt(password)?; println!("[✔] Vault unlocked successfully."); From 4569613b6d6a6e86497225e00843c679b8aedd24 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 19:06:11 +0530 Subject: [PATCH 44/48] FIX: cargo fmt and cargo clippy --- src/main.rs | 151 +++++++++++++++++++++++++++----------------------- src/ui/mod.rs | 4 +- 2 files changed, 85 insertions(+), 70 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5060ef1..52027c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,12 +37,13 @@ pub fn get_timestamp() -> u64 { .as_secs() } +#[allow(clippy::too_many_lines)] fn main() -> Result<(), Box> { crypto::init_crypto()?; - + config::init_passlock_dirs()?; backup::init_bsys()?; - + let cfg = config::load_config().unwrap_or_default(); let active_vault = cfg.active_vault.clone(); @@ -99,6 +100,7 @@ fn main() -> Result<(), Box> { eprintln!("Examples:"); eprintln!(" passlock import myPass123 ~/Documents/backup.vault"); eprintln!(" passlock import myPass123 /media/usb/backup.vault"); + eprintln!(" passlock import myPass123 ~/Dropbox/backup.vault"); std::process::exit(1); } let password = &args[2]; @@ -110,7 +112,9 @@ fn main() -> Result<(), Box> { eprintln!("Usage: passlock export_csv "); eprintln!(); eprintln!("WARNING: Creates UNENCRYPTED CSV with plaintext passwords!"); - eprintln!("Use for importing to other password managers (LastPass, Bitwarden, etc.)"); + eprintln!( + "Use for importing to other password managers (LastPass, Bitwarden, etc.)" + ); eprintln!(); eprintln!("Example:"); eprintln!(" passlock export-csv myPass123 ~/passwords.csv"); @@ -213,39 +217,47 @@ fn sync_vault(password: &str) -> Result<(), Box> { storage::svv(&vault, password)?; println!("[✔] Vault synced successfully."); - + println!("[...] Creating backup..."); let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; - + Ok(()) } /// Automatically backups vault after TUI closes /// Called by TUI when exiting /// Only creates backup if vault was modified +/// +/// # Errors +/// Returns an error if: +/// * Backup creation fails +/// * Configuration cannot be loaded +/// * Vault cannot be accessed pub fn auto_back() -> Result<(), Box> { let cfg = config::load_config().unwrap_or_default(); - + if !cfg.auto_backup { return Ok(()); } - + if !config::vault_exists(&cfg.active_vault) { return Ok(()); } - + println!("auto backup in progress"); - let (backup_name, was_created) = backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; - + let (backup_name, was_created) = + backup::create_backup(&cfg.active_vault, cfg.max_backups, false)?; + if was_created { - println!("auto backup completed {}", backup_name); + println!("auto backup completed {backup_name}"); } - + Ok(()) } /// Handle backup subcommands +#[allow(clippy::too_many_lines)] fn handle_backup_cmd(args: &[String]) -> Result<(), Box> { if args.is_empty() { println!("Backup Commands:"); @@ -273,10 +285,10 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> std::process::exit(1); } let password = &args[1]; - + let _vault = storage::ld_vt(password)?; println!("[✔] Password verified"); - + let cfg = config::load_config().unwrap_or_default(); backup::create_backup(&cfg.active_vault, cfg.max_backups, true)?; println!("[✔] Manual backup created successfully"); @@ -284,7 +296,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> "list" => { let cfg = config::load_config().unwrap_or_default(); let backups = backup::ls_backs(&cfg.active_vault)?; - + if backups.is_empty() { println!("[!] No backups found for vault '{}'", cfg.active_vault); println!(); @@ -296,7 +308,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!(); println!("{:<40} {:<12} Created", "Filename", "Size"); println!("{}", "─".repeat(70)); - + for (filename, size, created) in &backups { let size_kb = if *size > 1024 { format!("{} KB", *size / 1024) @@ -305,7 +317,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> }; println!("{filename:<40} {size_kb:<12} {created}"); } - + println!(); println!("Total backups: {}", backups.len()); println!(); @@ -318,27 +330,29 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> eprintln!("Usage: passlock backup restore "); eprintln!(); eprintln!("Examples:"); - eprintln!(" passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123"); + eprintln!( + " passlock backup restore backup_2026-02-28_10-30-45.vault myPassword123" + ); eprintln!(" passlock backup restore latest myPassword123"); std::process::exit(1); } - + let cfg = config::load_config().unwrap_or_default(); - + if args[1] == "latest" { if args.len() < 3 { eprintln!("Usage: passlock backup restore latest "); std::process::exit(1); } let password = &args[2]; - + let backups = backup::ls_backs(&cfg.active_vault)?; if backups.is_empty() { return Err("No backups found for this vault".into()); } - + let latest_backup = &backups[0].0; - println!("[i] Restoring latest backup: {}", latest_backup); + println!("[i] Restoring latest backup: {latest_backup}"); backup::restore_backup(&cfg.active_vault, latest_backup, password)?; println!("[✔] Vault restored to latest backup successfully"); } else { @@ -348,7 +362,7 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> } let backup_name = &args[1]; let password = &args[2]; - + backup::restore_backup(&cfg.active_vault, backup_name, password)?; println!("[✔] Backup restored successfully"); } @@ -359,11 +373,12 @@ fn handle_backup_cmd(args: &[String]) -> Result<(), Box> println!("Available subcommands: create, list, restore"); } } - + Ok(()) } /// Handle vault management subcommands +#[allow(clippy::too_many_lines)] fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { if args.is_empty() { println!("Vault Management Commands:"); @@ -398,28 +413,28 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let vault_name = &args[1]; let password = &args[2]; - + if config::vault_exists(vault_name) { - return Err(format!("Vault '{}' already exists", vault_name).into()); + return Err(format!("Vault '{vault_name}' already exists").into()); } let salt = crypto::gen_salt(); let vault = Vault::new(salt); - + storage::save_vault_to(vault_name, &vault, password)?; - - println!("[✔] Vault '{}' created successfully", vault_name); + + println!("[✔] Vault '{vault_name}' created successfully"); println!("[i] Backups will be created automatically as you add passwords"); - + let mut cfg = config::load_config()?; - cfg.active_vault = vault_name.to_string(); + cfg.active_vault.clone_from(vault_name); config::save_config(&cfg)?; - println!("[✔] Vault '{}' set as active", vault_name); + println!("[✔] Vault '{vault_name}' set as active"); } "list" => { let vaults = config::list_vaults()?; let cfg = config::load_config()?; - + if vaults.is_empty() { println!("[!] No vaults found"); println!(); @@ -430,8 +445,12 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); for vault_name in vaults { - let marker = if vault_name == cfg.active_vault { " (active)" } else { "" }; - println!(" • {}{}", vault_name, marker); + let marker = if vault_name == cfg.active_vault { + " (active)" + } else { + "" + }; + println!(" • {vault_name}{marker}"); } println!(); println!("Switch vault with: passlock vault use "); @@ -446,45 +465,45 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { std::process::exit(1); } let vault_name = &args[1]; - + if !config::vault_exists(vault_name) { - return Err(format!("Vault '{}' does not exist", vault_name).into()); + return Err(format!("Vault '{vault_name}' does not exist").into()); } - + let mut cfg = config::load_config()?; - cfg.active_vault = vault_name.to_string(); + cfg.active_vault.clone_from(vault_name); config::save_config(&cfg)?; - - println!("[✔] Active vault set to: {}", vault_name); + + println!("[✔] Active vault set to: {vault_name}"); } "info" => { let cfg = config::load_config()?; let vaults = config::list_vaults()?; - + println!("Vault Information:"); println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); println!(); println!("Active vault: {}", cfg.active_vault); println!("Total vaults: {}", vaults.len()); println!(); - + if config::vault_exists(&cfg.active_vault) { let vault_path = config::get_vault_path(&cfg.active_vault); if let Ok(metadata) = std::fs::metadata(&vault_path) { let size_kb = metadata.len() / 1024; if size_kb > 0 { - println!("Vault size: {} KB", size_kb); + println!("Vault size: {size_kb} KB"); } else { println!("Vault size: {} bytes", metadata.len()); } } - + let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); println!("Backups: {} available", backups.len()); } else { println!("Active vault '{}' not found!", cfg.active_vault); } - + println!(); println!("Configuration:"); println!(" Auto-backup: {}", cfg.auto_backup); @@ -501,20 +520,18 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { std::process::exit(1); } let vault_name = &args[1]; - + let cfg = config::load_config()?; if vault_name == &cfg.active_vault { return Err("Cannot delete active vault! Switch to another vault first.".into()); } - - println!("WARNING: This will delete vault '{}' and all its backups.", vault_name); + + println!("WARNING: This will delete vault '{vault_name}' and all its backups."); println!("Are you sure? (y/n)"); - - use std::io::{self, BufRead}; - let stdin = io::stdin(); + let mut line = String::new(); - stdin.lock().read_line(&mut line)?; - + std::io::stdin().read_line(&mut line)?; + if line.trim().to_lowercase() == "y" { config::delete_vault(vault_name)?; } else { @@ -531,12 +548,12 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { } let old_name = &args[1]; let new_name = &args[2]; - + config::rename_vault(old_name, new_name)?; - + let mut cfg = config::load_config()?; if cfg.active_vault == *old_name { - cfg.active_vault = new_name.to_string(); + cfg.active_vault.clone_from(new_name); config::save_config(&cfg)?; } } @@ -546,7 +563,7 @@ fn handle_vault_cmd(args: &[String]) -> Result<(), Box> { println!("Available subcommands: create, list, use, info, delete, rename"); } } - + Ok(()) } @@ -588,10 +605,14 @@ fn handle_icmd(args: &[String]) -> Result<(), Box> { } } } - + let cfg = config::load_config().unwrap_or_default(); let backups = backup::ls_backs(&cfg.active_vault).unwrap_or_else(|_| Vec::new()); - println!(" Backups: {} available (vault: {})", backups.len(), cfg.active_vault); + println!( + " Backups: {} available (vault: {})", + backups.len(), + cfg.active_vault + ); } else { println!("Vault Status:"); println!(" No vault found"); @@ -658,10 +679,4 @@ fn print_usage() { println!(" help Show this help"); println!(); println!("For more info: https://github.com/hachimamma/Passlock/blob/main/README.md"); - println!("EXAMPLES:"); - println!(" passlock create mySecurePassword123"); - println!(" passlock tui"); - println!(" passlock info cpu"); - println!(); - println!("For more info: https://github.com/hachimamma/Passlock"); -} \ No newline at end of file +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f2fc886..bd4eefe 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -418,7 +418,7 @@ fn run_app( Screen::MainMenu => { if handle_mmi(app, key_event.code) { if let Err(e) = crate::auto_back() { - eprintln!("[!] Auto-backup failed: {}", e); + eprintln!("[!] Auto-backup failed: {e}"); } return Ok(()); } @@ -455,7 +455,7 @@ fn run_app( Screen::OptionsMenu => { if handle_options_menu(app, key_event.code) { if let Err(e) = crate::auto_back() { - eprintln!("[!] Auto-backup failed: {}", e); + eprintln!("[!] Auto-backup failed: {e}"); } return Ok(()); } From 23a7542f972ef9e819a077fa22c36d70cc28f056 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 19:12:44 +0530 Subject: [PATCH 45/48] FIX: Fixed integrations.yml CI --- .github/workflows/integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0c3e464..ba98dd4 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -64,7 +64,7 @@ jobs: run: | rm -f ~/.passlock.vault ~/.passlock.temp - ./target/release/passlock create mySecurePass123 + ./target/release/passlock vault create myVault mySecurePass123 ./target/release/passlock unlock mySecurePass123 echo '{"e":[],"s":"'$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom)'"}' > ~/.passlock.temp @@ -205,7 +205,7 @@ jobs: cargo build --release cargo test - ./target/release/passlock create password123 + ./target/release/passlock vault create myVault password123 ./target/release/passlock unlock password123 echo "Cross-platform build successful on ${{ matrix.os }}" \ No newline at end of file From 310e83eb595ddca785c18bc903b55441ba15c7f8 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 19:15:19 +0530 Subject: [PATCH 46/48] FIX: Fixed integrations.yml --- .github/workflows/integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index ba98dd4..8dcb997 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -42,7 +42,7 @@ jobs: - name: Test CLI create command run: | - ./target/release/passlock create testpassword123 + ./target/release/passlock vault create myVault mySecurePass123 if [ ! -f ~/.passlock.vault ]; then echo "Vault file not created" @@ -52,7 +52,7 @@ jobs: - name: Test CLI unlock command run: | - ./target/release/passlock unlock testpassword123 + ./target/release/passlock unlock mySecurePass123 if [ ! -f ~/.passlock.temp ]; then echo "Temp file not created" From 3709dae575577b8fdf3c0e190a7eab583ba97621 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 19:19:34 +0530 Subject: [PATCH 47/48] FIX: Fixed integrations.yml --- .github/workflows/integration.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 8dcb997..efec488 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -44,10 +44,18 @@ jobs: run: | ./target/release/passlock vault create myVault mySecurePass123 - if [ ! -f ~/.passlock.vault ]; then - echo "Vault file not created" + # Check for vault in the new location + if [ ! -f ~/.passlock/vaults/myVault.vault ]; then + echo "Vault file not created at ~/.passlock/vaults/myVault.vault" exit 1 fi + + # Check config file exists + if [ ! -f ~/.passlock/config.toml ]; then + echo "Config file not created at ~/.passlock/config.toml" + exit 1 + fi + echo "Vault created successfully" - name: Test CLI unlock command @@ -62,7 +70,7 @@ jobs: - name: Test full encryption/decryption cycle run: | - rm -f ~/.passlock.vault ~/.passlock.temp + rm -f ~/.passlock/vaults/myVault.vault ~/.passlock.temp ~/.passlock/config.toml ./target/release/passlock vault create myVault mySecurePass123 ./target/release/passlock unlock mySecurePass123 @@ -206,6 +214,13 @@ jobs: cargo test ./target/release/passlock vault create myVault password123 + + # Check for vault in the new location + if [ ! -f ~/.passlock/vaults/myVault.vault ]; then + echo "Vault file not created at ~/.passlock/vaults/myVault.vault" + exit 1 + fi + ./target/release/passlock unlock password123 echo "Cross-platform build successful on ${{ matrix.os }}" \ No newline at end of file From 729a96a513e054e93a266203d0606e3c1e80e833 Mon Sep 17 00:00:00 2001 From: hachimamma Date: Sun, 1 Mar 2026 19:21:45 +0530 Subject: [PATCH 48/48] FIX: Fixed integrations.yml CI --- .github/workflows/integration.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index efec488..5a48bd7 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -51,8 +51,8 @@ jobs: fi # Check config file exists - if [ ! -f ~/.passlock/config.toml ]; then - echo "Config file not created at ~/.passlock/config.toml" + if [ ! -f ~/.passlock/config.json ]; then + echo "Config file not created at ~/.passlock/config.json" exit 1 fi @@ -70,7 +70,7 @@ jobs: - name: Test full encryption/decryption cycle run: | - rm -f ~/.passlock/vaults/myVault.vault ~/.passlock.temp ~/.passlock/config.toml + rm -f ~/.passlock/vaults/myVault.vault ~/.passlock.temp ~/.passlock/config.json ./target/release/passlock vault create myVault mySecurePass123 ./target/release/passlock unlock mySecurePass123