use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use keepass::config::DatabaseVersion;
use crate::app::{App, Screen};
use crate::db::{parse_totp_edit_value, save_database, totp_edit_value};
use crate::input::command::preview_entry;
use crate::input::fieldedit::{EditAction, FieldEditor};
const FIELD_COUNT: usize = 7;
const NAME_INDEX: usize = 0;
const USER_INDEX: usize = 1;
const PASSWORD_INDEX: usize = 2;
const URL_INDEX: usize = 3;
const TOTP_INDEX: usize = 4;
const LAST_MODIFIED_INDEX: usize = 5;
const NOTES_INDEX: usize = 6;
pub fn handle_edit_input(app: &mut App, key: KeyEvent) {
if app.editing_field {
handle_field_input(app, key);
return;
}
if app.confirm_exit {
handle_exit_confirmation(app, key.code);
return;
}
match key.code {
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.pending_save = true;
app.status = match app.kdbx.as_ref().map(|db| db.config.version.clone()) {
Some(DatabaseVersion::KDB4(1)) => Some("Saving...".to_string()),
Some(version) => Some(format!("Saving {version} to KDBX4.1...")),
None => Some("Saving...".to_string()),
};
}
_ => {
app.status = None;
match key.code {
KeyCode::Esc => request_close_edit(app),
KeyCode::Enter => start_editing_field(app),
KeyCode::PageUp => {
app.field_editor_scroll = app.field_editor_scroll.saturating_sub(1);
}
KeyCode::PageDown => {
app.field_editor_scroll = app.field_editor_scroll.saturating_add(1);
}
KeyCode::Down => move_selection(app, 1),
KeyCode::Up => move_selection(app, -1),
_ => {}
}
}
}
}
fn move_selection(app: &mut App, delta: i32) {
let current = app.edit_state.selected().unwrap_or(0) as i32;
let next = (current + delta).rem_euclid(FIELD_COUNT as i32) as usize;
app.edit_state.select(Some(next));
}
fn start_editing_field(app: &mut App) {
let selected = app.edit_state.selected().unwrap_or(0);
if selected == LAST_MODIFIED_INDEX {
app.status = Some("Last modified is set automatically".to_string());
return;
}
let Some(entry) = app.edit_entry.as_ref() else {
return;
};
match selected {
NAME_INDEX => {
app.field_editor = Some(FieldEditor::with_text(&entry.name));
}
USER_INDEX => {
app.field_editor = Some(FieldEditor::with_text(&entry.user));
}
PASSWORD_INDEX => {
app.field_editor = Some(FieldEditor::with_text(&entry.password));
}
URL_INDEX => {
app.field_editor = Some(FieldEditor::with_text(&entry.url));
}
TOTP_INDEX => {
app.field_editor = Some(FieldEditor::with_text(totp_edit_value(&entry.totp)));
}
NOTES_INDEX => {
app.field_editor = Some(FieldEditor::with_multiline_text(&entry.notes));
}
_ => return,
}
app.editing_field = true;
app.status = None;
}
fn handle_field_input(app: &mut App, key: KeyEvent) {
let action = match app.field_editor.as_mut() {
Some(editor) => editor.handle_key(key),
None => {
app.editing_field = false;
return;
}
};
match action {
EditAction::Continue => {}
EditAction::Accept => commit_field(app),
EditAction::Cancel => {
app.editing_field = false;
app.field_editor = None;
}
}
}
fn commit_field(app: &mut App) {
let selected = app.edit_state.selected().unwrap_or(0);
let value = app.field_editor.take().map(|editor| editor.into_text()).unwrap_or_default();
if selected == TOTP_INDEX {
match parse_totp_edit_value(&value) {
Ok(totp) => {
if let Some(entry) = app.edit_entry.as_mut() {
entry.totp = totp;
}
app.editing_field = false;
}
Err(err) => {
app.status = Some(format!("TOTP should be SECRET DIGITS PERIOD: {err}"));
app.field_editor = Some(FieldEditor::with_text(&value));
}
}
return;
}
if let Some(entry) = app.edit_entry.as_mut() {
match selected {
NAME_INDEX => entry.name = value,
USER_INDEX => entry.user = value,
PASSWORD_INDEX => entry.password = value,
URL_INDEX => entry.url = value,
NOTES_INDEX => entry.notes = value,
LAST_MODIFIED_INDEX => {}
_ => {}
}
}
app.editing_field = false;
}
fn request_close_edit(app: &mut App) {
if has_unsaved_changes(app) {
app.confirm_exit = true;
app.status = Some("Save changes? [y] Yes [n] No".to_string());
} else {
reset_edit_state(app);
}
}
fn has_unsaved_changes(app: &App) -> bool {
let (Some(entry), Some(original)) = (app.edit_entry.as_ref(), app.edit_original.as_ref()) else {
return false;
};
entry.name != original.name
|| entry.user != original.user
|| entry.password != original.password
|| entry.url != original.url
|| entry.totp != original.totp
|| entry.notes != original.notes
}
fn handle_exit_confirmation(app: &mut App, key: KeyCode) {
match crate::input::normalize_shortcut(key) {
KeyCode::Char('y') => {
app.confirm_exit = false;
close_edit(app);
}
KeyCode::Char('n') => {
app.confirm_exit = false;
reset_edit_state(app);
app.status = Some("Changes discarded".to_string());
}
KeyCode::Esc => {
app.confirm_exit = false;
app.status = None;
}
_ => {}
}
}
fn close_edit(app: &mut App) {
save_edit(app);
reset_edit_state(app);
}
fn reset_edit_state(app: &mut App) {
app.edit_entry = None;
app.edit_original = None;
app.edit_target = None;
app.editing_field = false;
app.field_editor = None;
app.field_editor_scroll = 0;
app.confirm_exit = false;
app.edit_state.select(None);
app.screen = Screen::Index;
}
fn save_edit(app: &mut App) -> Option<usize> {
let entry = app.edit_entry.take()?;
let saved_idx = match app.edit_target {
Some(idx) => {
if let Some(slot) = app.entries.get_mut(idx) {
*slot = entry;
}
Some(idx)
}
None => {
if entry.name.trim().is_empty() {
None
} else {
app.entries.push(entry);
Some(app.entries.len() - 1)
}
}
};
app.refresh_filter();
saved_idx
}
pub(crate) fn process_pending_save(app: &mut App) {
if !app.pending_save {
return;
}
app.pending_save = false;
let Some(saved_idx) = save_edit(app) else {
reset_edit_state(app);
return;
};
persist_entry(app, saved_idx);
reset_edit_state(app);
if let Some(filtered_pos) = app.filtered.iter().position(|&idx| idx == saved_idx) {
app.index_state.select(Some(filtered_pos));
preview_entry(app);
}
}
fn persist_entry(app: &mut App, idx: usize) {
let (Some(db), Some(key), Some(path)) = (app.kdbx.as_mut(), app.db_key.as_ref(), app.config.default_database.as_ref()) else {
app.status = Some("Not saved: database is locked".to_string());
return;
};
let Some(target) = app.entries.get_mut(idx) else {
return;
};
match save_database(path, key, db, std::slice::from_mut(target)) {
Ok(()) => {}
Err(err) => app.status = Some(format!("Failed to save: {err:#}")),
}
}