package main import ( "fmt" "os" "strconv" "strings" "git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/passwords" "git.julianfamily.org/keepassgo/vault" ) func (u *ui) loadSelectedEntryIntoEditor() { u.selectedHistoryIndex = -1 u.historyIndex.SetText("") item, ok := u.selectedEntry() if !ok { u.entryID.SetText("") u.entryTitle.SetText("") u.entryUsername.SetText("") u.entryPassword.SetText("") u.entryURL.SetText("") u.entryNotes.SetText("") u.entryTags.SetText("") u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / ")) u.entryFields.SetText("") u.attachmentName.SetText("") u.attachmentPath.SetText("") u.exportAttachmentPath.SetText("") return } u.entryID.SetText(item.ID) u.entryTitle.SetText(item.Title) u.entryUsername.SetText(item.Username) u.entryPassword.SetText(item.Password) u.entryURL.SetText(item.URL) u.entryNotes.SetText(item.Notes) u.entryTags.SetText(strings.Join(item.Tags, ", ")) u.entryPath.SetText(strings.Join(item.Path, " / ")) u.entryFields.SetText(marshalFields(item.Fields)) u.attachmentName.SetText("") u.attachmentPath.SetText("") u.exportAttachmentPath.SetText("") } func (u *ui) visibleHistory() []vault.Entry { item, ok := u.selectedEntry() if !ok || len(item.History) == 0 { return nil } return append([]vault.Entry(nil), item.History...) } func (u *ui) selectedHistoryEntry() (vault.Entry, bool) { history := u.visibleHistory() if u.selectedHistoryIndex < 0 || u.selectedHistoryIndex >= len(history) { return vault.Entry{}, false } return history[u.selectedHistoryIndex], true } func (u *ui) selectHistoryVersion(index int) error { history := u.visibleHistory() if index < 0 || index >= len(history) { return fmt.Errorf("history index %d out of range", index) } u.selectedHistoryIndex = index u.historyIndex.SetText(strconv.Itoa(index)) return nil } func (u *ui) saveEntryAction() error { entry, err := u.editorEntry() if err != nil { return err } if err := u.state.UpsertEntry(entry); err != nil { return err } u.filter() return nil } func (u *ui) duplicateSelectedEntryAction() error { baseID := strings.TrimSpace(u.state.SelectedEntryID) if baseID == "" { return fmt.Errorf("no entry selected") } duplicateID := baseID + "-copy" if _, err := u.state.DuplicateSelectedEntry(duplicateID); err != nil { return err } u.loadSelectedEntryIntoEditor() u.filter() return nil } func (u *ui) deleteSelectedEntryAction() error { if err := u.state.DeleteSelectedEntry(); err != nil { return err } u.loadSelectedEntryIntoEditor() u.filter() return nil } func (u *ui) restoreSelectedRecycleEntryAction() error { id := strings.TrimSpace(u.state.SelectedEntryID) if id == "" { return fmt.Errorf("no recycle-bin entry selected") } if err := u.state.RestoreEntry(id); err != nil { return err } u.filter() return nil } func (u *ui) saveTemplateAction() error { entry, err := u.editorEntry() if err != nil { return err } if len(entry.Path) == 0 { entry.Path = []string{"Templates"} } if err := u.state.UpsertTemplate(entry); err != nil { return err } u.filter() return nil } func (u *ui) deleteSelectedTemplateAction() error { id := strings.TrimSpace(u.state.SelectedEntryID) if id == "" { return fmt.Errorf("no template selected") } if err := u.state.DeleteTemplate(id); err != nil { return err } u.loadSelectedEntryIntoEditor() u.filter() return nil } func (u *ui) createGroupAction() error { return u.state.CreateGroup(strings.TrimSpace(u.groupName.Text())) } func (u *ui) renameGroupAction() error { return u.state.RenameCurrentGroup(strings.TrimSpace(u.groupName.Text())) } func (u *ui) deleteCurrentGroupAction() error { if err := u.state.DeleteCurrentGroup(); err != nil { return err } u.filter() return nil } func (u *ui) instantiateSelectedTemplateAction() error { templateID := strings.TrimSpace(u.state.SelectedEntryID) if templateID == "" { return fmt.Errorf("no template selected") } entry, err := u.editorEntry() if err != nil { return err } if _, err := u.state.InstantiateTemplate(templateID, entry); err != nil { return err } u.filter() return nil } func (u *ui) addAttachmentAction() error { content, err := os.ReadFile(strings.TrimSpace(u.attachmentPath.Text())) if err != nil { return fmt.Errorf("read attachment: %w", err) } name := strings.TrimSpace(u.attachmentName.Text()) if name == "" { return fmt.Errorf("attachment name is required") } if err := u.state.AddAttachmentToSelectedEntry(name, content); err != nil { return err } u.loadSelectedEntryIntoEditor() u.attachmentName.SetText(name) u.filter() return nil } func (u *ui) exportAttachmentAction() error { item, ok := u.selectedEntry() if !ok { return fmt.Errorf("no entry selected") } name := strings.TrimSpace(u.attachmentName.Text()) content, ok := item.Attachments[name] if !ok { return fmt.Errorf("attachment not found") } if err := os.WriteFile(strings.TrimSpace(u.exportAttachmentPath.Text()), content, 0o600); err != nil { return fmt.Errorf("write attachment export: %w", err) } return nil } func (u *ui) removeAttachmentAction() error { name := strings.TrimSpace(u.attachmentName.Text()) if name == "" { return fmt.Errorf("attachment name is required") } if err := u.state.DeleteAttachmentFromSelectedEntry(name); err != nil { return err } u.loadSelectedEntryIntoEditor() u.filter() return nil } func (u *ui) restoreSelectedHistoryAction() error { index, err := u.selectedHistoryVersionIndex() if err != nil { return err } if err := u.state.RestoreSelectedEntryVersion(index); err != nil { return err } u.loadSelectedEntryIntoEditor() u.filter() return nil } func (u *ui) selectedHistoryVersionIndex() (int, error) { text := strings.TrimSpace(u.historyIndex.Text()) if text != "" { index, err := strconv.Atoi(text) if err != nil { return 0, fmt.Errorf("invalid history index: %w", err) } return index, nil } if u.selectedHistoryIndex >= 0 { return u.selectedHistoryIndex, nil } return 0, fmt.Errorf("no history version selected") } func (u *ui) copySelectedFieldAction(target clipboard.Target) error { model, err := u.state.Session.Current() if err != nil { return err } service := clipboard.Service{Writer: u.clipboardWriter} return service.Copy(model, u.state.SelectedEntryID, target) } func (u *ui) generatePasswordAction() error { profiles := passwords.DefaultProfiles() profile, ok := profiles[strings.TrimSpace(u.passwordProfile.Text())] if !ok { return fmt.Errorf("unknown password profile") } password, err := passwords.Generate(profile) if err != nil { return err } u.entryPassword.SetText(password) return nil } func (u *ui) editorEntry() (vault.Entry, error) { path := parsePath(u.entryPath.Text()) fields, err := parseFields(u.entryFields.Text()) if err != nil { return vault.Entry{}, err } return vault.Entry{ ID: strings.TrimSpace(u.entryID.Text()), Title: strings.TrimSpace(u.entryTitle.Text()), Username: strings.TrimSpace(u.entryUsername.Text()), Password: u.entryPassword.Text(), URL: strings.TrimSpace(u.entryURL.Text()), Notes: strings.TrimSpace(u.entryNotes.Text()), Tags: parseTags(u.entryTags.Text()), Path: path, Fields: fields, }, nil } func parsePath(text string) []string { var out []string for _, part := range strings.Split(text, "/") { part = strings.TrimSpace(part) if part == "" { continue } out = append(out, part) } return out } func parseTags(text string) []string { var out []string for _, part := range strings.Split(text, ",") { part = strings.TrimSpace(part) if part == "" { continue } out = append(out, part) } return out } func parseFields(text string) (map[string]string, error) { if strings.TrimSpace(text) == "" { return nil, nil } fields := map[string]string{} for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if line == "" { continue } key, value, ok := strings.Cut(line, "=") if !ok { return nil, fmt.Errorf("invalid field line %q", line) } fields[strings.TrimSpace(key)] = strings.TrimSpace(value) } return fields, nil } func marshalFields(fields map[string]string) string { if len(fields) == 0 { return "" } lines := make([]string, 0, len(fields)) for key, value := range fields { lines = append(lines, key+"="+value) } return strings.Join(lines, "\n") }