chamber_ui/app.rs
1use crate::app;
2use crate::vault_selector::{VaultAction, VaultSelector, VaultSelectorMode};
3use async_trait::async_trait;
4use chamber_import_export::{ExportFormat, export_items, import_items};
5use chamber_password_gen::PasswordConfig;
6use chamber_vault::{AutoLockCallback, AutoLockConfig, AutoLockService, Item, ItemKind, NewItem, Vault, VaultManager};
7use color_eyre::Result;
8use color_eyre::eyre::eyre;
9use ratatui::prelude::Style;
10use ratatui::style::Color;
11use std::path::PathBuf;
12use std::sync::Arc;
13use tui_textarea::TextArea;
14#[derive(Clone, Copy, PartialEq, Eq)]
15pub enum Screen {
16 Unlock,
17 Main,
18 AddItem,
19 ViewItem,
20 EditItem,
21 ChangeMaster,
22 GeneratePassword,
23 ImportExport,
24 VaultSelector,
25}
26
27#[derive(Clone, Copy, PartialEq, Eq)]
28pub enum UnlockField {
29 Master,
30 Confirm,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq)]
34pub enum ChangeKeyField {
35 Current,
36 New,
37 Confirm,
38}
39
40#[derive(Clone, Copy, PartialEq, Eq)]
41pub enum AddItemField {
42 Name,
43 Kind,
44 Value,
45}
46
47#[derive(Clone, Copy, PartialEq, Eq)]
48pub enum PasswordGenField {
49 Length,
50 Options,
51 Generate,
52}
53
54#[derive(Clone, Copy, PartialEq, Eq)]
55pub enum ImportExportField {
56 Path,
57 Format,
58 Action,
59}
60
61#[derive(Clone, Copy, PartialEq, Eq)]
62pub enum ImportExportMode {
63 Export,
64 Import,
65}
66
67#[allow(dead_code)]
68#[derive(Clone, Copy, PartialEq, Eq)]
69pub enum ViewMode {
70 All,
71 Passwords,
72 Environment,
73 Notes,
74}
75
76impl ViewMode {
77 pub const fn as_str(self) -> &'static str {
78 match self {
79 ViewMode::All => "Items",
80 ViewMode::Passwords => "Passwords",
81 ViewMode::Environment => "Environment",
82 ViewMode::Notes => "Notes",
83 }
84 }
85}
86
87#[derive(Clone, Copy, PartialEq, Eq)]
88pub enum StatusType {
89 Info,
90 Success,
91 Warning,
92 Error,
93}
94
95#[derive(Debug, Clone)]
96pub struct ItemCounts {
97 pub total: usize,
98 pub passwords: usize,
99 pub env_vars: usize,
100 pub notes: usize,
101 pub api_keys: usize,
102 pub ssh_keys: usize,
103 pub certificates: usize,
104 pub databases: usize,
105 pub credit_cards: usize,
106 pub secure_notes: usize,
107 pub identities: usize,
108 pub servers: usize,
109 pub wifi_passwords: usize,
110 pub licenses: usize,
111 pub bank_accounts: usize,
112 pub documents: usize,
113 pub recovery_codes: usize,
114 pub oauth_tokens: usize,
115}
116
117#[derive(Debug, Clone)]
118pub struct CountdownInfo {
119 pub enabled: bool,
120 pub minutes_left: i64,
121 pub seconds_left: i64,
122}
123
124pub struct TuiAutoLockCallback {
125 // Callback to lock the TUI app
126}
127
128#[async_trait]
129impl AutoLockCallback for TuiAutoLockCallback {
130 async fn on_auto_lock(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
131 // Lock the vault in TUI mode
132 // This could set a flag that the main loop checks
133 Ok(())
134 }
135}
136
137#[allow(clippy::struct_excessive_bools)]
138pub struct App {
139 pub vault: Vault,
140 pub vault_manager: VaultManager,
141 pub vault_selector: VaultSelector,
142
143 pub screen: Screen,
144 pub master_input: String,
145 pub master_confirm_input: String,
146 pub master_mode_is_setup: bool,
147 pub unlock_focus: UnlockField,
148 pub error: Option<String>,
149
150 pub items: Vec<Item>,
151 pub selected: usize,
152 pub view_mode: ViewMode,
153 pub filtered_items: Vec<Item>,
154 pub search_query: String,
155 pub search_mode: bool,
156
157 pub add_name: String,
158 pub add_kind_idx: usize,
159 pub add_value: String,
160 pub add_value_scroll: usize,
161 pub status_message: Option<String>,
162 pub status_type: StatusType,
163 pub scroll_offset: usize,
164 pub add_value_textarea: TextArea<'static>,
165 pub auto_lock_service: Option<Arc<AutoLockService>>,
166 pub auto_locked: bool,
167 pub countdown_info: Option<CountdownInfo>,
168
169 // Change passes key dialog fields
170 pub ck_current: String,
171 pub ck_new: String,
172 pub ck_confirm: String,
173 pub ck_focus: ChangeKeyField,
174 pub add_focus: AddItemField,
175 pub view_item: Option<Item>,
176 pub view_show_value: bool,
177 pub edit_item: Option<Item>,
178 pub edit_value: String,
179
180 // Password generation fields
181 pub gen_focus: PasswordGenField,
182 pub gen_length_str: String,
183 pub gen_config: PasswordConfig,
184 pub generated_password: Option<String>,
185
186 // Import/Export fields
187 pub ie_focus: ImportExportField,
188 pub ie_mode: ImportExportMode,
189 pub ie_path: String,
190 pub ie_format_idx: usize,
191 pub ie_formats: Vec<&'static str>,
192}
193
194impl App {
195 /// Initializes a new instance of the struct.
196 ///
197 /// This function creates or opens a vault, determines whether the master mode setup
198 /// is required, and initializes the various fields required for managing the application state.
199 ///
200 /// # Returns
201 /// - `Result<Self>`: A `Result` containing the initialized struct instance on success,
202 /// or an error if the vault fails to open or create.
203 ///
204 /// # Fields
205 /// - `vault`: Handles secure storage by opening or creating a vault.
206 /// - `screen`: Represents the current active screen, initialized to the `Unlock` screen.
207 /// - `master_input`: Stores user input for the master password during setup or unlock phase.
208 /// - `master_confirm_input`: Stores user input for confirming the master password during setup.
209 /// - `master_mode_is_setup`: Indicates whether the master mode is set up (false if initialization is incomplete).
210 /// - `unlock_focus`: Tracks which unlock field is currently focused (e.g., Master field).
211 /// - `error`: Holds any error message or state, defaulted to `None`.
212 /// - `items`: A vector holding all items stored in the vault.
213 /// - `selected`: Tracks the index of the currently selected item in the items list.
214 /// - `view_mode`: Specifies the current filter/view mode for items (e.g., All items).
215 /// - `filtered_items`: A vector holding the subset of items that match the current search query or filter.
216 /// - `search_query`: Stores the user's current search input or query.
217 /// - `add_name`: Field for the name of an item to be added.
218 /// - `add_kind_idx`: Indicates the index of the kind/type of the item being added.
219 /// - `add_value`: The value of the item being added.
220 /// - `add_value_scroll`: Tracks the scroll state for long values when adding an item.
221 /// - `status_message`: Holds transient status messages to display to the user.
222 /// - `status_type`: Indicates the type of status message (e.g., Info, Warning, Error).
223 ///
224 /// # Change Key Fields
225 /// - `ck_current`: Stores the current master key value.
226 /// - `ck_new`: Stores the new master key value.
227 /// - `ck_confirm`: Confirms the new master key value.
228 /// - `ck_focus`: Tracks which field is focused during the change key process.
229 ///
230 /// # Add Item Fields
231 /// - `add_focus`: Tracks which field is focused when adding a new item (e.g., Name).
232 ///
233 /// # Viewing and Editing Items
234 /// - `view_item`: The currently selected item for viewing, if any.
235 /// - `view_show_value`: Indicates whether to reveal the value of the viewed item.
236 /// - `edit_item`: The item currently being edited, if any.
237 /// - `edit_value`: The edited value of the currently selected item.
238 ///
239 /// # Password Generation
240 /// - `gen_focus`: The current focus field in the password generation process (e.g., Length).
241 /// - `gen_length_str`: String representation of the desired password length (default: "16").
242 /// - `gen_config`: Configuration settings for password generation (e.g., character set, length).
243 /// - `generated_password`: Holds the last generated password, if any.
244 ///
245 /// # Import/Export
246 /// - `ie_focus`: Tracks which field is focused during import/export operations (e.g., File Path).
247 /// - `ie_mode`: Indicates the mode (Import or Export) for import/export operations.
248 /// - `ie_path`: Stores the file path selected for import/export.
249 /// - `ie_format_idx`: Tracks the index of the currently selected format for import/export.
250 /// - `ie_formats`: A vector containing supported file formats for import/export (e.g., "json", "csv").
251 ///
252 /// # Errors
253 /// Return an error if the vault cannot be opened or created successfully.
254 ///
255 /// # Panics
256 pub fn new() -> Result<Self> {
257 let vault = Vault::open_default()?;
258 let vault_manager = VaultManager::new()?;
259 let vault_selector = VaultSelector::new();
260
261 // Determine initial screen based on vault state
262 let (_, master_mode_is_setup) = if vault.is_initialized() {
263 (Screen::Unlock, false) // Just unlock, no setup
264 } else {
265 (Screen::Unlock, true) // Setup mode (create master password)
266 };
267
268 let auto_lock_config = AutoLockConfig::default();
269 let callback = Arc::new(TuiAutoLockCallback {});
270 let auto_lock_service = Some(Arc::new(AutoLockService::new(auto_lock_config, callback)));
271
272 Ok(Self {
273 vault,
274 vault_manager,
275 vault_selector,
276
277 screen: Screen::Unlock,
278 master_input: String::new(),
279 master_confirm_input: String::new(),
280 master_mode_is_setup,
281 unlock_focus: UnlockField::Master,
282 error: None,
283 items: vec![],
284 selected: 0,
285 view_mode: ViewMode::All,
286 filtered_items: vec![],
287 search_query: String::new(),
288 search_mode: false,
289 add_name: String::new(),
290 add_kind_idx: 0,
291 add_value: String::new(),
292 add_value_scroll: 0,
293 status_message: None,
294 status_type: StatusType::Info,
295 scroll_offset: 0,
296 add_value_textarea: {
297 let mut textarea = TextArea::default();
298 // Enable line numbers
299 textarea.set_line_number_style(Style::default().fg(Color::DarkGray));
300 textarea.set_cursor_line_style(Style::default());
301 // Optional: set a placeholder text
302 textarea.set_placeholder_text("Enter your value here...");
303 textarea
304 },
305 auto_lock_service,
306 auto_locked: false,
307 countdown_info: None,
308
309 ck_current: String::new(),
310 ck_new: String::new(),
311 ck_confirm: String::new(),
312 ck_focus: ChangeKeyField::Current,
313 add_focus: AddItemField::Name,
314 view_item: None,
315 view_show_value: false,
316 edit_item: None,
317 edit_value: String::new(),
318
319 // Initialize password generation fields
320 gen_focus: PasswordGenField::Length,
321 gen_length_str: "16".to_string(),
322 gen_config: PasswordConfig::default(),
323 generated_password: None,
324
325 // Initialize import/export fields
326 ie_focus: ImportExportField::Path,
327 ie_mode: ImportExportMode::Export,
328 ie_path: String::new(),
329 ie_format_idx: 0,
330 ie_formats: vec!["json", "csv", "backup"],
331 })
332 }
333
334 fn validate_master_strength(s: &str) -> Result<()> {
335 if s.len() < 8 {
336 return Err(eyre!("Master key must be at least 8 characters long"));
337 }
338 if !s.chars().any(|c| c.is_ascii_lowercase()) {
339 return Err(eyre!("Master key must contain a lowercase letter"));
340 }
341 if !s.chars().any(|c| c.is_ascii_uppercase()) {
342 return Err(eyre!("Master key must contain an uppercase letter"));
343 }
344 if !s.chars().any(|c| c.is_ascii_digit()) {
345 return Err(eyre!("Master key must contain a digit"));
346 }
347 Ok(())
348 }
349
350 /// Unlocks the application vault using the provided master key and performs necessary validations.
351 ///
352 /// This function checks if the master key setup process is initiated, validates the user input,
353 /// and sets up or unlocks the vault accordingly. In case of errors during validation or unlocking
354 /// operations, appropriate error messages are set.
355 ///
356 /// ## Steps
357 /// 1. If `master_mode_is_setup` is true:
358 /// - Validate the presence of master input and confirmation input. If either is empty, an error
359 /// message is set, and the function will return early.
360 /// - Compare `master_input` and `master_confirm_input`. If they do not match, an error message
361 /// is set, and the function exits.
362 /// - Validate the strength of the `master_input` using `validate_master_strength`. If it fails,
363 /// sets an error message and exits.
364 /// - Initialize the vault with `master_input` and reset the `master_mode_is_setup` flag to false.
365 /// 2. Unlock the vault with the provided `master_input`. On failure to unlock, sets an error message
366 /// with the reason and exits with the corresponding error.
367 /// 3. Refresh the items in the application to reflect the unlocked state.
368 /// 4. Set the current screen to `Screen::Main`.
369 /// 5. Clear any existing error messages to indicate successful operation.
370 /// 6. Return `Ok(())` if no errors occurred.
371 ///
372 /// ## Returns
373 /// - `Ok(())` on successful unlocking and initialization of the vault.
374 /// - `Err` with the propagation of error from validation or unlocking operations.
375 ///
376 /// ## Errors
377 /// This function sets the `error` field with one of the following messages on failure:
378 /// - "Please enter and confirm your master key." - if either master input or confirmation input is missing.
379 /// - "Master keys do not match." - if the confirmation of the master key does not match the input.
380 /// - Error message returned by `validate_master_strength` - if the master key is deemed weak or invalid.
381 /// - "Unlock failed: {e}" - if unlocking the vault fails.
382 ///
383 /// ## Side Effects
384 /// - Updates the `error` field in the struct to reflect any issues encountered during execution.
385 /// - Modifies the state of `screen`, `master_mode_is_setup`, and `vault` upon successful execution.
386 pub fn unlock(&mut self) -> Result<()> {
387 if self.master_mode_is_setup {
388 // Setup mode: create new master password (needs confirmation)
389 if self.master_input != self.master_confirm_input {
390 self.error = Some("Passwords do not match".to_string());
391 return Ok(());
392 }
393
394 Self::validate_master_strength(&self.master_input)?;
395
396 // Initialize the vault
397 self.vault.initialize(&self.master_input)?;
398 }
399
400 // Always try to unlock (works for both setup and normal mode)
401 if let Ok(()) = self.vault.unlock(&self.master_input) {
402 self.refresh_items()?;
403 self.screen = Screen::Main;
404 self.error = None;
405 self.master_input.clear();
406 self.master_confirm_input.clear();
407 } else {
408 self.error = Some("Invalid master password".to_string());
409 self.master_input.clear();
410 if self.master_mode_is_setup {
411 self.master_confirm_input.clear();
412 }
413 }
414
415 Ok(())
416 }
417
418 /// Refreshes the list of items and updates the filtered items.
419 ///
420 /// This method performs the following actions:
421 /// 1. Updates the `items` field by retrieving the latest list of items from the `vault`.
422 /// 2. Applies filtering logic to update the `filtered_items` list.
423 /// 3. Ensures that the current selection (`selected`) is within the bounds of the updated `filtered_items` list.
424 /// If the current selection is out of bounds but the `filtered_items` list is not empty,
425 /// it adjusts `selected` to the last valid index.
426 ///
427 /// # Errors
428 /// Returns an error if fetching the list of items from the `vault` fails.
429 ///
430 /// # Returns
431 /// - `Ok(())` if the operation is successful.
432 /// - `Err` with the specific error encountered when listing items from the `vault`.
433 pub fn refresh_items(&mut self) -> Result<()> {
434 self.items = self.vault.list_items()?;
435 self.update_filtered_items();
436 if self.selected >= self.filtered_items.len() && !self.filtered_items.is_empty() {
437 self.selected = self.filtered_items.len().saturating_sub(1);
438 }
439 Ok(())
440 }
441
442 pub fn update_filtered_items(&mut self) {
443 let mut filtered = self.items.clone();
444
445 // Apply view mode filter
446 if self.view_mode != ViewMode::All {
447 filtered.retain(|item| match self.view_mode {
448 ViewMode::Passwords => matches!(item.kind, ItemKind::Password),
449 ViewMode::Environment => matches!(item.kind, ItemKind::EnvVar),
450 ViewMode::Notes => matches!(item.kind, ItemKind::Note),
451 ViewMode::All => true,
452 });
453 }
454
455 // Apply search filter
456 if !self.search_query.is_empty() {
457 let query_lower = self.search_query.to_lowercase();
458 filtered.retain(|item| {
459 item.name.to_lowercase().contains(&query_lower) || item.value.to_lowercase().contains(&query_lower)
460 });
461 }
462
463 // Sort by kind first, then by name
464 filtered.sort_by(|a, b| {
465 use std::cmp::Ordering;
466 match a.kind.as_str().cmp(b.kind.as_str()) {
467 Ordering::Equal => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
468 other => other,
469 }
470 });
471
472 self.filtered_items = filtered;
473
474 // Adjust selection if needed
475 if self.selected >= self.filtered_items.len() && !self.filtered_items.is_empty() {
476 self.selected = self.filtered_items.len() - 1;
477 }
478 }
479
480 pub fn get_selected_item(&self) -> Option<&Item> {
481 self.filtered_items.get(self.selected)
482 }
483
484 pub fn get_item_counts(&self) -> ItemCounts {
485 let passwords = self
486 .items
487 .iter()
488 .filter(|i| matches!(i.kind, ItemKind::Password))
489 .count();
490 let env_vars = self.items.iter().filter(|i| matches!(i.kind, ItemKind::EnvVar)).count();
491 let notes = self.items.iter().filter(|i| matches!(i.kind, ItemKind::Note)).count();
492 let api_keys = self.items.iter().filter(|i| matches!(i.kind, ItemKind::ApiKey)).count();
493 let ssh_keys = self.items.iter().filter(|i| matches!(i.kind, ItemKind::SshKey)).count();
494 let certificates = self
495 .items
496 .iter()
497 .filter(|i| matches!(i.kind, ItemKind::Certificate))
498 .count();
499 let databases = self
500 .items
501 .iter()
502 .filter(|i| matches!(i.kind, ItemKind::Database))
503 .count();
504
505 // New categories
506 let credit_cards = self
507 .items
508 .iter()
509 .filter(|i| matches!(i.kind, ItemKind::CreditCard))
510 .count();
511 let secure_notes = self
512 .items
513 .iter()
514 .filter(|i| matches!(i.kind, ItemKind::SecureNote))
515 .count();
516 let identities = self
517 .items
518 .iter()
519 .filter(|i| matches!(i.kind, ItemKind::Identity))
520 .count();
521 let servers = self.items.iter().filter(|i| matches!(i.kind, ItemKind::Server)).count();
522 let wifi_passwords = self
523 .items
524 .iter()
525 .filter(|i| matches!(i.kind, ItemKind::WifiPassword))
526 .count();
527 let licenses = self
528 .items
529 .iter()
530 .filter(|i| matches!(i.kind, ItemKind::License))
531 .count();
532 let bank_accounts = self
533 .items
534 .iter()
535 .filter(|i| matches!(i.kind, ItemKind::BankAccount))
536 .count();
537 let documents = self
538 .items
539 .iter()
540 .filter(|i| matches!(i.kind, ItemKind::Document))
541 .count();
542 let recovery_codes = self
543 .items
544 .iter()
545 .filter(|i| matches!(i.kind, ItemKind::Recovery))
546 .count();
547 let oauth_tokens = self.items.iter().filter(|i| matches!(i.kind, ItemKind::OAuth)).count();
548
549 ItemCounts {
550 total: self.items.len(),
551 passwords,
552 env_vars,
553 notes,
554 api_keys,
555 ssh_keys,
556 certificates,
557 databases,
558 credit_cards,
559 secure_notes,
560 identities,
561 servers,
562 wifi_passwords,
563 licenses,
564 bank_accounts,
565 documents,
566 recovery_codes,
567 oauth_tokens,
568 }
569 }
570
571 /// Adds a new item to the vault with the specified details and updates the UI.
572 ///
573 /// # Description
574 /// This function creates a new item based on the user input, validates it,
575 /// and stores it in the vault. If the operation is successful, the UI is updated
576 /// to reflect the addition and the input fields are reset. If an error occurs,
577 /// appropriate error messages and statuses are set.
578 ///
579 /// # Fields Used
580 /// - `add_kind_idx`: Determines the type of item being added (e.g., `Password`, `EnvVar`, `Note`, etc.).
581 /// - `add_name`: The name of the new item, trimmed of whitespace.
582 /// - `add_value_textarea`: The content or value of the new item, usually multi-line.
583 /// - `vault`: The storage structure which handles item creation.
584 /// - `add_value`: Secondary field for item value, cleared after addition.
585 /// - `add_value_scroll`: Resets the scroll position of the textarea after addition.
586 /// - `screen`: Sets the screen to the main view upon successful addition.
587 /// - `error`: Displays error messages for failed operations.
588 /// - `status`: Updates the user-visible status of the addition operation.
589 ///
590 /// # Process
591 /// 1. Determines the item type (`kind`) based on `add_kind_idx`:
592 /// - `0` -> Password
593 /// - `1` -> Environment Variable
594 /// - `3` -> API Key
595 /// - `4` -> SSH Key
596 /// - `5` -> Certificate
597 /// - `6` -> Database
598 /// - Default -> Note
599 /// 2. Fetches the item's value from the textarea (`add_value_textarea`), joining multiple lines with `\n`.
600 /// 3. Creates a `NewItem` structure with the gathered data.
601 /// 4. Attempts to add the item using `vault.create_item`.
602 /// 5. Handles responses:
603 /// - **Success**: Resets input fields, updates item list, switches to the main screen, and displays a success message.
604 /// - **Failure**: If the name already exists, prompts the user to choose a different name. For other errors, displays a generic error message.
605 ///
606 /// # Returns
607 /// Returns an `Ok(())` on successful completion of the process or propagates an error if any step fails.
608 ///
609 /// # Errors
610 /// - Returns an error if refreshing the items (`refresh_items`) fails.
611 /// - Updates the `error` and `status` fields with detailed context if item creation fails.
612 ///
613 /// # Notes
614 /// - Resets both single-line (`add_value`) and multi-line (`add_value_textarea`) value fields upon successful addition.
615 /// - Automatically trims leading and trailing whitespace from the item name.
616 pub fn add_item(&mut self) -> Result<()> {
617 let kind = ItemKind::all()[self.add_kind_idx.min(ItemKind::all().len() - 1)];
618
619 // Get the value from the textarea instead of add_value
620 let value = self.add_value_textarea.lines().join("\n");
621
622 let new_item = NewItem {
623 name: self.add_name.trim().to_string(),
624 kind,
625 value, // Use the textarea content
626 };
627
628 match self.vault.create_item(&new_item) {
629 Ok(()) => {
630 self.add_name.clear();
631 self.add_value.clear();
632 // Reset the textarea as well
633 self.add_value_textarea = TextArea::default();
634 self.add_value_scroll = 0;
635 self.refresh_items()?;
636 self.screen = Screen::Main;
637 self.error = Some("Item added.".into());
638 self.set_status("Item added successfully.".to_string(), StatusType::Success);
639 }
640 Err(e) => {
641 let msg = e.to_string();
642 if msg.contains("already exists") {
643 self.error = Some(format!("Item '{}' already exists.", new_item.name));
644 self.set_status(
645 format!(
646 "Item '{}' already exists. Please choose a different name.",
647 new_item.name
648 ),
649 StatusType::Warning,
650 );
651 } else {
652 self.error = Some(format!("Failed to add item: {msg}"));
653 self.set_status(format!("Failed to add item: {msg}"), StatusType::Error);
654 }
655 }
656 }
657 Ok(())
658 }
659
660 /// Deletes the currently selected item from the vault.
661 ///
662 /// This function retrieves the currently selected item, deletes it from the vault
663 /// using its unique identifier, and then refreshes the list of items to reflect the changes.
664 /// If no item is selected, the function does nothing.
665 ///
666 /// # Errors
667 ///
668 /// Returns an error if:
669 /// - Retrieving the selected item fails.
670 /// - Deleting the item from the vault fails.
671 /// - Refreshing the item list fails.
672 pub fn delete_selected(&mut self) -> Result<()> {
673 if let Some(item) = self.get_selected_item() {
674 let item_id = item.id;
675 self.vault.delete_item(item_id)?;
676 self.refresh_items()?;
677 }
678 Ok(())
679 }
680
681 /// Changes the master key for the application if provided inputs meet the necessary conditions.
682 ///
683 /// This function performs several validations to ensure the master key change process is secure:
684 /// 1. It checks if all required input fields (`ck_current`, `ck_new`, and `ck_confirm`) are filled.
685 /// 2. It validates that the new master key (`ck_new`) matches the confirmation key (`ck_confirm`).
686 /// 3. It verifies the strength of the new master key using the `validate_master_strength` method.
687 ///
688 /// If any of these conditions fail, an appropriate error message is stored in the `error` field,
689 /// and the process halts without changing the master key.
690 ///
691 /// Once all validations are passed, the function updates the master key by calling the
692 /// `change_master_key` method of the `vault`. After a successful update, it clears all input fields,
693 /// resets the error message, and navigates back to the main screen.
694 ///
695 /// # Returns
696 ///
697 /// * `Ok(())` - If the master key has been successfully changed or
698 /// the process ended due to a validation failure without panicking.
699 /// * `Err(Error)` - If an error occurs while attempting to change the key in the `vault`.
700 ///
701 /// # Errors
702 ///
703 /// - If any of the following conditions occur, an error is stored in the `error` field,
704 /// and the function returns `Ok`:
705 /// - Any of the required fields (`ck_current`, `ck_new`, or `ck_confirm`) are empty.
706 /// - The new master key and confirmation key do not match.
707 /// - The new master key fails the strength validation.
708 /// - If the `vault.change_master_key` method returns an error, it will propagate as a `Result::Err`.
709 pub fn change_master(&mut self) -> Result<()> {
710 if self.ck_current.is_empty() || self.ck_new.is_empty() || self.ck_confirm.is_empty() {
711 self.error = Some("Please fill out all fields.".into());
712 return Ok(());
713 }
714 if self.ck_new != self.ck_confirm {
715 self.error = Some("New master keys do not match.".into());
716 return Ok(());
717 }
718 if let Err(e) = Self::validate_master_strength(&self.ck_new) {
719 self.error = Some(e.to_string());
720 return Ok(());
721 }
722 self.vault.change_master_key(&self.ck_current, &self.ck_new)?;
723 self.ck_current.clear();
724 self.ck_new.clear();
725 self.ck_confirm.clear();
726 self.screen = Screen::Main;
727 self.error = None;
728 Ok(())
729 }
730
731 /// Copies the currently selected item to the clipboard.
732 ///
733 /// This function retrieves the currently selected item using the `get_selected_item` method.
734 /// If an item is selected, it initializes the system clipboard, attempts to copy the selected
735 /// item's value to the clipboard, and updates the status message to indicate success.
736 ///
737 /// # Returns
738 ///
739 /// * `Ok(())` - If the item is successfully copied to the clipboard or no item is selected.
740 /// * `Err(anyhow::Error)` - If there's an error while accessing the clipboard or copying the
741 /// item to the clipboard.
742 ///
743 /// # Errors
744 ///
745 /// - Returns an error if accessing the clipboard fails.
746 /// - Returns an error if copying the selected item's value to the clipboard fails.
747 ///
748 /// # Behavior
749 ///
750 /// - If no item is selected (`get_selected_item` returns `None`), the function does nothing and
751 /// returns `Ok(())`.
752 ///
753 /// - If an item is selected (`get_selected_item` returns `Some`), it:
754 /// - Initializes a new `arboard::Clipboard` instance.
755 /// - Copies the `value` of the selected item to the clipboard.
756 /// - Sets a status message indicating that the item has been successfully copied to the clipboard.
757 ///
758 /// # Dependencies
759 ///
760 /// This function relies on the `arboard` crate for clipboard interactions and the `anyhow` crate
761 /// for error handling. It also assumes the existence of the following methods:
762 /// - `get_selected_item`: Retrieves the currently selected item, returning an option.
763 /// - `set_status`: Updates the application's status message and type.
764 ///
765 pub fn copy_selected(&mut self) -> Result<()> {
766 if let Some(item) = self.get_selected_item() {
767 let mut clipboard = arboard::Clipboard::new().map_err(|e| eyre!("Failed to access clipboard: {}", e))?;
768 clipboard
769 .set_text(&item.value)
770 .map_err(|e| eyre!("Failed to copy to clipboard: {}", e))?;
771 self.set_status(format!("Copied '{}' to clipboard", item.name), StatusType::Success);
772 }
773 Ok(())
774 }
775
776 pub fn view_selected(&mut self) {
777 if let Some(item) = self.get_selected_item() {
778 self.view_item = Some(item.clone());
779 self.view_show_value = false;
780 self.screen = Screen::ViewItem;
781 }
782 }
783
784 pub const fn toggle_value_visibility(&mut self) {
785 self.view_show_value = !self.view_show_value;
786 }
787
788 pub fn edit_selected(&mut self) {
789 let selected_item = self.get_selected_item().cloned();
790 if let Some(item) = selected_item {
791 self.edit_item = Some(item.clone());
792 self.edit_value.clone_from(&item.value);
793 self.screen = Screen::EditItem;
794 }
795 }
796
797 /// Attempts to save edits made to an item in the vault and updates the user interface accordingly.
798 ///
799 /// # Behavior
800 /// - If the `edit_value` is empty (after trimming), it sets an error message ("Value cannot be empty") and exits early.
801 /// - Otherwise, it updates the item in the vault identified by `edit_item.id` with the new `edit_value`.
802 /// - After updating the item, it clears the `edit_item` and `edit_value`, refreshes the list of items, and switches
803 /// the application's screen back to the main screen while clearing any previous errors.
804 ///
805 /// # Errors
806 /// - If updating the vault fails, an error is propagated from the `vault.update_item` method.
807 /// - If refreshing items fails, an error is propagated from the `refresh_items` method.
808 ///
809 /// # Returns
810 /// - `Ok(())` if the edit is successfully saved or if the `edit_value` is empty.
811 /// - `Err` if an error occurs during vault updates or refreshing items.
812 ///
813 /// # Fields/State
814 /// - `self.edit_item`: The item currently being edited. If `None`, the method does nothing.
815 /// - `self.edit_value`: The new value to be saved to the item. If empty (once trimmed), the method sets an error message and exits early.
816 /// - `self.error`: An optional error message for display purposes. This is set if the `edit_value` is empty or cleared on successful operation.
817 /// - `self.vault`: The storage mechanism used to update the item.
818 /// - `self.screen`: Controls the application screen flow. Set to `Screen::Main` after a successful edit.
819 ///
820 pub fn save_edit(&mut self) -> Result<()> {
821 if let Some(item) = &self.edit_item {
822 if self.edit_value.trim().is_empty() {
823 self.error = Some("Value cannot be empty".into());
824 return Ok(());
825 }
826
827 self.vault.update_item(item.id, &self.edit_value)?;
828 self.edit_item = None;
829 self.edit_value.clear();
830 self.refresh_items()?;
831 self.screen = Screen::Main;
832 self.error = None;
833 }
834 Ok(())
835 }
836
837 // Password generation methods
838 pub fn open_password_generator(&mut self) {
839 self.gen_focus = PasswordGenField::Length;
840 self.gen_length_str = self.gen_config.length.to_string();
841 self.generated_password = None;
842 self.error = None;
843 self.screen = Screen::GeneratePassword;
844 }
845
846 pub fn generate_password(&mut self) {
847 if let Ok(length) = self.gen_length_str.parse::<usize>() {
848 self.gen_config.length = length.clamp(4, 128);
849 } else {
850 self.error = Some("Invalid length".into());
851 return;
852 }
853
854 match self.gen_config.generate() {
855 Ok(password) => {
856 self.generated_password = Some(password);
857 self.error = None;
858 }
859 Err(e) => {
860 self.error = Some(format!("Generation failed: {e}"));
861 }
862 }
863 }
864
865 /// Copies the generated password to the system clipboard.
866 ///
867 /// This function attempts to access the generated password stored in the `self.generated_password`
868 /// field and copies it to the system clipboard using the `arboard` crate. If the operation succeeds,
869 /// a success message is set in the `self.error` field. In the event of an error while accessing the
870 /// clipboard or copying the text, the function returns a corresponding error.
871 ///
872 /// # Returns
873 ///
874 /// * `Ok(())` - If the password is successfully copied to the clipboard or no password was generated.
875 /// * `Err(anyhow::Error)` - If accessing the clipboard or copying the password fails.
876 ///
877 /// # Errors
878 ///
879 /// This function may return an error in the following scenarios:
880 /// * Failure to access or initialize the system clipboard.
881 /// * Failure to copy the generated password to the clipboard.
882 ///
883 /// # Side Effects
884 ///
885 /// * If a password is successfully copied to the clipboard, the `self.error` field is set with a success message.
886 ///
887 /// # Dependencies
888 ///
889 /// This function makes use of the `arboard` crate for clipboard access and text manipulation.
890 pub fn copy_generated_password(&mut self) -> Result<()> {
891 if let Some(password) = &self.generated_password {
892 let mut clipboard = arboard::Clipboard::new().map_err(|e| eyre!("Failed to access clipboard: {}", e))?;
893 clipboard
894 .set_text(password)
895 .map_err(|e| eyre!("Failed to copy to clipboard: {}", e))?;
896 self.error = Some("Password copied to clipboard".into());
897 }
898 Ok(())
899 }
900
901 pub fn use_generated_password(&mut self) {
902 if let Some(password) = &self.generated_password {
903 self.add_value = password.clone();
904 self.screen = Screen::AddItem;
905 self.add_focus = AddItemField::Name;
906 }
907 }
908
909 // Import/Export methods
910 pub fn open_import_export(&mut self, mode: ImportExportMode) {
911 self.ie_mode = mode;
912 self.ie_focus = ImportExportField::Path;
913 self.ie_path.clear();
914 self.ie_format_idx = 0;
915 self.error = None;
916 self.screen = Screen::ImportExport;
917 }
918
919 /// Executes the import or export operation based on the current application state.
920 ///
921 /// This method performs the following operations:
922 /// 1. Validates the provided file path and ensures it is not empty.
923 /// 2. Normalizes the file path to handle different path separators and expand the home directory.
924 /// 3. Determines the format of import/export (CSV, JSON, or backup).
925 /// 4. Executes the import or export operation based on the selected mode (`ImportExportMode`).
926 ///
927 /// ### Export Mode
928 /// - Creates necessary parent directories if they do not exist.
929 /// - Exports the current items to the specified file path in the selected format.
930 /// - Sets an appropriate success message indicating the number of items exported and the file path.
931 ///
932 /// ### Import Mode
933 /// - Ensures the specified file exists before proceeding.
934 /// - Imports items from the file in the specified format.
935 /// - Avoids importing duplicate items by checking against existing item names.
936 /// - Records the number of imported and skipped items due to duplication or errors.
937 /// - Updates the item list after a successful import and presents an appropriate summary message.
938 ///
939 /// ### Errors
940 /// - If the file path is empty, a user-friendly error message is set and the operation is aborted.
941 /// - If a directory creation fails during export, an error is returned.
942 /// - If certain items cannot be imported due to conflicts or other errors, they are counted as skipped.
943 ///
944 /// ### Remarks
945 /// - Upon completion (successful or not), the application state is updated to the main screen.
946 ///
947 /// ### Returns
948 /// - `Ok(())` if the operation completes successfully (even if some items were skipped).
949 /// - `Err` if a file path normalization or file operation fails during the execution.
950 ///
951 /// ### Preconditions
952 /// - `self.ie_path` must be set to a valid file path.
953 /// - The `self.ie_formats` array must include supported formats ("csv", "backup", "json").
954 /// - The `self.items` list is expected to contain the current application items for export/import validation.
955 ///
956 /// ### Postconditions
957 /// - Updates `self.error` with a descriptive message about the operation result.
958 /// - Changes the application state screen to `Screen::Main`.
959 pub fn execute_import_export(&mut self) -> Result<()> {
960 if self.ie_path.trim().is_empty() {
961 self.error = Some("Please enter a file path".into());
962 return Ok(());
963 }
964
965 // Normalize the path to handle different separators and expand the home directory
966 let normalized_path = app::App::normalize_path(&self.ie_path)?;
967 let path = PathBuf::from(normalized_path);
968
969 let format = match self.ie_formats[self.ie_format_idx] {
970 "csv" => ExportFormat::Csv,
971 "backup" => ExportFormat::ChamberBackup,
972 _ => ExportFormat::Json,
973 };
974
975 match self.ie_mode {
976 ImportExportMode::Export => {
977 // Create parent directories if they don't exist
978 if let Some(parent) = path.parent() {
979 if !parent.exists() {
980 std::fs::create_dir_all(parent)
981 .map_err(|e| eyre!("Failed to create directory {}: {}", parent.display(), e))?;
982 }
983 }
984
985 export_items(&self.items, &format, &path)?;
986 self.error = Some(format!("Exported {} items to {}", self.items.len(), path.display()));
987 }
988 ImportExportMode::Import => {
989 if !path.exists() {
990 self.error = Some(format!("File does not exist: {}", path.display()));
991 return Ok(());
992 }
993
994 let new_items = import_items(&path, &format)?;
995 if new_items.is_empty() {
996 self.error = Some("No items found in file".into());
997 return Ok(());
998 }
999
1000 let existing_names: std::collections::HashSet<String> =
1001 self.items.iter().map(|item| item.name.clone()).collect();
1002
1003 let mut imported_count = 0;
1004 let mut skipped_count = 0;
1005
1006 for item in new_items {
1007 if existing_names.contains(&item.name) {
1008 skipped_count += 1;
1009 continue;
1010 }
1011
1012 match self.vault.create_item(&item) {
1013 Ok(()) => imported_count += 1,
1014 Err(_) => skipped_count += 1,
1015 }
1016 }
1017
1018 self.refresh_items()?;
1019 self.error = Some(format!("Imported {imported_count} items, skipped {skipped_count}"));
1020 }
1021 }
1022
1023 self.screen = Screen::Main;
1024 Ok(())
1025 }
1026
1027 // Helper method to normalize file paths
1028 fn normalize_path(input_path: &str) -> Result<String> {
1029 let path_str = input_path.trim();
1030
1031 // Handle home directory expansion
1032 let expanded_path = if path_str.strip_prefix('~').is_some() {
1033 if let Some(home_dir) = dirs::home_dir() {
1034 let rest = &path_str[1..];
1035 let rest = if rest.starts_with('/') || rest.starts_with('\\') {
1036 &rest[1..]
1037 } else {
1038 rest
1039 };
1040 home_dir.join(rest).to_string_lossy().to_string()
1041 } else {
1042 return Err(eyre!("Unable to determine home directory"));
1043 }
1044 } else {
1045 path_str.to_string()
1046 };
1047
1048 // Convert forward slashes to native path separators on Windows
1049 #[cfg(windows)]
1050 let normalized = expanded_path.replace('/', "\\");
1051
1052 #[cfg(not(windows))]
1053 let normalized = expanded_path;
1054
1055 Ok(normalized)
1056 }
1057
1058 pub fn set_status(&mut self, message: String, status_type: StatusType) {
1059 self.status_message = Some(message);
1060 self.status_type = status_type;
1061 }
1062
1063 pub fn clear_status(&mut self) {
1064 self.status_message = None;
1065 }
1066
1067 pub fn is_in_input_mode(&self) -> bool {
1068 match self.screen {
1069 Screen::AddItem
1070 | Screen::EditItem
1071 | Screen::ChangeMaster
1072 | Screen::GeneratePassword
1073 | Screen::ImportExport
1074 | Screen::Unlock => true,
1075 Screen::Main if !self.search_query.is_empty() => true, // Search mode
1076 _ => false,
1077 }
1078 }
1079
1080 /// Pastes content from the clipboard to the add item value field.
1081 ///
1082 /// This function retrieves text content from the system clipboard and appends it to
1083 /// the current `add_value` field. If the clipboard contains text, it will be added
1084 /// to the existing value. If accessing the clipboard fails, an appropriate status
1085 /// message is displayed.
1086 ///
1087 /// # Returns
1088 ///
1089 /// * `Ok(())` - If the paste operation completes successfully or if there's no text in clipboard.
1090 /// * `Err(anyhow::Error)` - If accessing the clipboard fails.
1091 ///
1092 /// # Errors
1093 ///
1094 /// - Returns an error if accessing the clipboard fails.
1095 /// - Sets a warning status if the clipboard is empty or contains no text.
1096 ///
1097 /// # Behavior
1098 ///
1099 /// - Retrieves text from the system clipboard using `arboard::Clipboard`.
1100 /// - Appends the clipboard content to the current `add_value` field.
1101 /// - Sets a success status message indicating the paste operation completed.
1102 /// - If clipboard is empty or contains no text, shows a warning message.
1103 pub fn paste_to_add_value(&mut self) -> Result<()> {
1104 if let Ok(mut clipboard) = arboard::Clipboard::new() {
1105 if let Ok(text) = clipboard.get_text() {
1106 // Clear existing content and insert new text
1107 self.add_value_textarea.select_all();
1108 self.add_value_textarea.cut();
1109 self.add_value_textarea.insert_str(text);
1110 self.set_status("Pasted from clipboard".to_string(), StatusType::Success);
1111 Ok(())
1112 } else {
1113 self.set_status("No text in clipboard".to_string(), StatusType::Warning);
1114 Ok(())
1115 }
1116 } else {
1117 self.set_status("Failed to access clipboard".to_string(), StatusType::Error);
1118 Ok(())
1119 }
1120 }
1121
1122 pub fn open_vault_selector(&mut self) {
1123 self.vault_selector.load_vaults(&self.vault_manager);
1124 self.vault_selector.show();
1125 self.screen = Screen::VaultSelector;
1126 }
1127
1128 /// Handles various actions related to vault management.
1129 ///
1130 /// This function processes the provided `VaultAction` and executes the corresponding logic
1131 /// to manage vaults, such as creating, updating, deleting, importing, and more.
1132 ///
1133 /// # Arguments
1134 ///
1135 /// * `action` - A `VaultAction` enum that specifies the action to be performed. Each variant
1136 /// of `VaultAction` corresponds to a specific vault-related operation.
1137 ///
1138 /// # Returns
1139 ///
1140 /// * `Result<()>` - Returns `Ok(())` if the action was processed successfully; otherwise,
1141 /// an error is returned if any operation fails.
1142 ///
1143 /// # `VaultAction` Variants
1144 ///
1145 /// - `VaultAction::Switch(vault_id)`
1146 /// Switches to the specified vault by its ID.
1147 ///
1148 /// - `VaultAction::Create { name, description, category }`
1149 /// Creates a new vault with the provided name, description, and category.
1150 ///
1151 /// - `VaultAction::Update { vault_id, name, description, category, favorite }`
1152 /// Updates an existing vault with the given parameters, including optional fields like the
1153 /// vault name, description, category, and favorite status.
1154 ///
1155 /// - `VaultAction::Delete { vault_id, delete_file }`
1156 /// Deletes a vault specified by its ID. If `delete_file` is true, related files are also deleted.
1157 ///
1158 /// - `VaultAction::Import { path }`
1159 /// Imports a vault from a specified file path.
1160 ///
1161 /// - `VaultAction::Refresh`
1162 /// Reloads the list of available vaults and updates the UI to reflect any changes. Sets
1163 /// a success status message indicating the vaults have been refreshed.
1164 ///
1165 /// - `VaultAction::Close`
1166 /// Hides the vault selector and switches back to the main screen.
1167 ///
1168 /// - `VaultAction::ShowHelp`
1169 /// Displays help information. (This action may be handled or ignored based on needs.)
1170 ///
1171 /// # Errors
1172 ///
1173 /// Returns an error if:
1174 /// - Switching to a vault fails.
1175 /// - The requested vault action encounters an issue (e.g., file access issues during imports or
1176 /// failures in vault creation/deletion).
1177 pub fn handle_vault_action(&mut self, action: VaultAction) -> Result<()> {
1178 match action {
1179 VaultAction::Switch(vault_id) => {
1180 self.switch_to_vault(&vault_id)?;
1181 }
1182 VaultAction::Create {
1183 name,
1184 description,
1185 category,
1186 } => {
1187 self.create_vault(&name, description, &category);
1188 }
1189 VaultAction::Update {
1190 vault_id,
1191 name,
1192 description,
1193 category,
1194 favorite,
1195 } => {
1196 self.update_vault(vault_id, name, description, category, favorite);
1197 }
1198 VaultAction::Delete { vault_id, delete_file } => {
1199 self.delete_vault(&vault_id, delete_file);
1200 }
1201 VaultAction::Import { path } => {
1202 self.import_vault(&path);
1203 }
1204 VaultAction::Refresh => {
1205 self.vault_selector.load_vaults(&self.vault_manager);
1206 self.set_status("Vaults refreshed".to_string(), StatusType::Success);
1207 }
1208 VaultAction::Close => {
1209 self.vault_selector.hide();
1210 self.screen = Screen::Main;
1211 }
1212 VaultAction::ShowHelp => {
1213 // You can handle help display here or ignore it
1214 }
1215 }
1216 Ok(())
1217 }
1218
1219 fn switch_to_vault(&mut self, vault_id: &str) -> Result<String> {
1220 // First, switch the active vault in the registry
1221 self.vault_manager.switch_active_vault(vault_id)?;
1222
1223 // Try to open the vault with the current master password
1224 match self.vault_manager.open_vault(vault_id, &self.master_input) {
1225 Ok(()) => {
1226 // Successfully opened vault in the manager
1227 // Now we need to create our own unlocked instance
1228 let vault_info = self
1229 .vault_manager
1230 .registry
1231 .get_vault(vault_id)
1232 .ok_or_else(|| eyre!("Vault with id {} not found", vault_id))?;
1233 let mut new_vault = chamber_vault::Vault::open_or_create(Some(&vault_info.path))?;
1234 new_vault.unlock(&self.master_input)?;
1235
1236 // Replace our vault with the newly unlocked one
1237 self.vault = new_vault;
1238 self.refresh_items()?;
1239 self.set_status(format!("Switched to vault: {vault_id}"), StatusType::Success);
1240 }
1241 Err(_) => {
1242 // This vault has a different master password
1243 self.vault_selector.error_message = Some(format!(
1244 "Vault '{vault_id}' was created with a different master password. \
1245 Please delete this vault and create a new one, or use the master password change feature to migrate it."
1246 ));
1247 }
1248 }
1249
1250 Ok(String::from(vault_id))
1251 }
1252
1253 fn create_vault(&mut self, name: &str, description: Option<String>, category: &str) {
1254 // Parse category
1255 let vault_category = match category.to_lowercase().as_str() {
1256 "personal" => chamber_vault::VaultCategory::Personal,
1257 "work" => chamber_vault::VaultCategory::Work,
1258 "team" => chamber_vault::VaultCategory::Team,
1259 "project" => chamber_vault::VaultCategory::Project,
1260 "testing" => chamber_vault::VaultCategory::Testing,
1261 "archive" => chamber_vault::VaultCategory::Archive,
1262 custom => chamber_vault::VaultCategory::Custom(custom.to_string()),
1263 };
1264
1265 // In a real implementation, you'd prompt for a password
1266 let password = &self.master_input;
1267
1268 // Validate that we have a password
1269 if password.is_empty() {
1270 self.vault_selector.error_message = Some("Master password is required to create vault".to_string());
1271 }
1272
1273 match self
1274 .vault_manager
1275 .create_vault(name.to_string(), None, vault_category, description, password)
1276 {
1277 Ok(_vault_id) => {
1278 self.vault_selector.load_vaults(&self.vault_manager);
1279 self.vault_selector.mode = VaultSelectorMode::Select;
1280 self.set_status(format!("Created vault: {name}"), StatusType::Success);
1281 }
1282 Err(e) => {
1283 self.vault_selector.error_message = Some(format!("Failed to create vault: {e}"));
1284 }
1285 }
1286 }
1287
1288 fn update_vault(
1289 &mut self,
1290 vault_id: String,
1291 name: Option<String>,
1292 description: Option<String>,
1293 category: Option<String>,
1294 favorite: Option<bool>,
1295 ) {
1296 let vault_category = category.map(|cat_str| match cat_str.to_lowercase().as_str() {
1297 "personal" => chamber_vault::VaultCategory::Personal,
1298 "work" => chamber_vault::VaultCategory::Work,
1299 "team" => chamber_vault::VaultCategory::Team,
1300 "project" => chamber_vault::VaultCategory::Project,
1301 "testing" => chamber_vault::VaultCategory::Testing,
1302 "archive" => chamber_vault::VaultCategory::Archive,
1303 custom => chamber_vault::VaultCategory::Custom(custom.to_string()),
1304 });
1305
1306 match self
1307 .vault_manager
1308 .update_vault_info(&vault_id, name.clone(), description, vault_category, favorite)
1309 {
1310 Ok(()) => {
1311 self.vault_selector.load_vaults(&self.vault_manager);
1312 self.vault_selector.mode = VaultSelectorMode::Select;
1313 self.set_status(
1314 format!("Updated vault: {}", name.unwrap_or(vault_id)),
1315 StatusType::Success,
1316 );
1317 }
1318 Err(e) => {
1319 self.vault_selector.error_message = Some(format!("Failed to update vault: {e}"));
1320 }
1321 }
1322 }
1323
1324 fn delete_vault(&mut self, vault_id: &str, delete_file: bool) {
1325 let is_active_vault = self.vault_manager.registry.active_vault_id.as_ref() == Some(&vault_id.to_string());
1326 let vault_count = self.vault_manager.registry.vaults.len();
1327
1328 // Prevent deletion of an active vault unless it's the only one
1329 if is_active_vault && vault_count > 1 {
1330 self.set_status(
1331 "Cannot delete active vault. Switch to another vault first.".to_string(),
1332 StatusType::Error,
1333 );
1334 return;
1335 }
1336
1337 match self.vault_manager.delete_vault(vault_id, delete_file) {
1338 Ok(()) => {
1339 self.vault_selector.load_vaults(&self.vault_manager);
1340 self.vault_selector.mode = VaultSelectorMode::Select;
1341 self.set_status(format!("Deleted vault: {vault_id}"), StatusType::Success);
1342 }
1343 Err(e) => {
1344 self.set_status(format!("Failed to delete vault: {e}"), StatusType::Error);
1345 }
1346 }
1347 }
1348
1349 fn import_vault(&mut self, path: &str) {
1350 let path_buf = std::path::PathBuf::from(&path);
1351 let name = path_buf
1352 .file_stem()
1353 .and_then(|s| s.to_str())
1354 .unwrap_or("Imported Vault")
1355 .to_string();
1356
1357 match self
1358 .vault_manager
1359 .import_vault(&path_buf, name.clone(), chamber_vault::VaultCategory::Personal, true)
1360 {
1361 Ok(_vault_id) => {
1362 self.vault_selector.load_vaults(&self.vault_manager);
1363 self.vault_selector.mode = VaultSelectorMode::Select;
1364 self.set_status(format!("Imported vault: {name}"), StatusType::Success);
1365 }
1366 Err(e) => {
1367 self.vault_selector.error_message = Some(format!("Failed to import vault: {e}"));
1368 }
1369 }
1370 }
1371 pub async fn update_activity(&mut self) {
1372 if let Some(service) = &self.auto_lock_service {
1373 service.update_activity().await;
1374 }
1375 }
1376
1377 pub async fn check_auto_lock(&mut self) -> bool {
1378 if let Some(service) = &self.auto_lock_service {
1379 if service.activity_tracker.should_auto_lock().await {
1380 self.auto_locked = true;
1381 self.screen = Screen::Unlock;
1382 return true;
1383 }
1384 }
1385 false
1386 }
1387
1388 pub async fn get_time_until_auto_lock(&self) -> Option<chrono::Duration> {
1389 if let Some(service) = &self.auto_lock_service {
1390 service.get_time_until_lock().await
1391 } else {
1392 None
1393 }
1394 }
1395
1396 pub async fn update_countdown_info(&mut self) {
1397 if let Some(time_left) = self.get_time_until_auto_lock().await {
1398 self.countdown_info = Some(CountdownInfo {
1399 enabled: true,
1400 minutes_left: time_left.num_minutes(),
1401 seconds_left: time_left.num_seconds(),
1402 });
1403 } else {
1404 self.countdown_info = None;
1405 }
1406 }
1407
1408 // Synchronous method for the drawing function
1409 pub const fn get_countdown_info(&self) -> Option<&CountdownInfo> {
1410 self.countdown_info.as_ref()
1411 }
1412}