package appui import ( "fmt" "strconv" "strings" "gioui.org/io/key" "git.julianfamily.org/keepassgo/internal/appstate" ) type focusID string type detailField string const ( focusSearch focusID = "search" detailFieldID detailField = "id" detailFieldTitle detailField = "title" detailFieldUsername detailField = "username" detailFieldPassword detailField = "password" detailFieldURL detailField = "url" detailFieldPath detailField = "path" detailFieldTags detailField = "tags" detailFieldPasswordProfile detailField = "password-profile" detailFieldNotes detailField = "notes" detailFieldFields detailField = "fields" detailFieldHistoryIndex detailField = "history-index" ) func breadcrumbFocusID(index int) focusID { return focusID(fmt.Sprintf("breadcrumb:%d", index)) } func listFocusID(index int) focusID { return focusID(fmt.Sprintf("list:%d", index)) } func detailFocusID(field detailField) focusID { return focusID("detail:" + string(field)) } func (u *ui) handleKeyPress(name key.Name, modifiers key.Modifiers) bool { if u.handleShortcutKey(name, modifiers) { return true } if u.isVaultLocked() && name == key.NameReturn { u.startUnlockAction() return true } if u.shouldShowLifecycleSetup() && name == key.NameReturn { if u.lifecycleMode == "remote" { u.startOpenRemoteAction() } else { u.startOpenVaultAction() } return true } switch name { case key.NameTab: delta := 1 if modifiers.Contain(key.ModShift) { delta = -1 } u.moveKeyboardFocus(delta) return true case key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow, key.NameReturn: return u.handleFocusedKey(name) default: return false } } func (u *ui) moveKeyboardFocus(delta int) { order := u.focusOrder() if len(order) == 0 { return } current := canonicalFocusID(u.keyboardFocus) index := 0 for i, item := range order { if canonicalFocusID(item) == current { index = i break } } index += delta if index < 0 { index = len(order) - 1 } if index >= len(order) { index = 0 } u.setKeyboardFocus(order[index]) } func (u *ui) focusOrder() []focusID { if u.isVaultLocked() { return []focusID{detailFocusID(detailFieldPassword)} } order := []focusID{focusSearch} if u.state.Section != appstate.SectionRecycleBin { order = append(order, breadcrumbFocusID(0)) } if len(u.visible) > 0 { order = append(order, listFocusID(u.focusedListIndexOrZero())) } order = append(order, detailFocusID(u.focusedDetailFieldOrDefault())) return order } func (u *ui) setKeyboardFocus(id focusID) { u.keyboardFocus = id if strings.HasPrefix(string(id), "list:") { u.focusListIndex(focusIndex(id)) } } func (u *ui) handleFocusedKey(name key.Name) bool { switch { case u.keyboardFocus == focusSearch: if name == key.NameDownArrow && len(u.visible) > 0 { u.setKeyboardFocus(listFocusID(u.focusedListIndexOrZero())) return true } case strings.HasPrefix(string(u.keyboardFocus), "breadcrumb:"): return u.handleBreadcrumbKey(name) case strings.HasPrefix(string(u.keyboardFocus), "list:"): return u.handleListKey(name) case strings.HasPrefix(string(u.keyboardFocus), "detail:"): return u.handleDetailKey(name) } return false } func (u *ui) handleBreadcrumbKey(name key.Name) bool { crumbs := u.breadcrumbLabels() if len(crumbs) == 0 { return false } index := focusIndex(u.keyboardFocus) switch name { case key.NameLeftArrow: if index > 0 { u.keyboardFocus = breadcrumbFocusID(index - 1) } return true case key.NameRightArrow: if index < len(crumbs)-1 { u.keyboardFocus = breadcrumbFocusID(index + 1) } return true case key.NameDownArrow: if len(u.visible) > 0 { u.setKeyboardFocus(listFocusID(u.focusedListIndexOrZero())) } return true case key.NameReturn: u.activateBreadcrumb(index) return true default: return false } } func (u *ui) handleListKey(name key.Name) bool { if len(u.visible) == 0 { return false } index := focusIndex(u.keyboardFocus) switch name { case key.NameUpArrow: if index > 0 { u.setKeyboardFocus(listFocusID(index - 1)) } return true case key.NameDownArrow: if index < len(u.visible)-1 { u.setKeyboardFocus(listFocusID(index + 1)) } return true case key.NameLeftArrow: u.keyboardFocus = breadcrumbFocusID(len(u.breadcrumbLabels()) - 1) return true case key.NameRightArrow, key.NameReturn: u.keyboardFocus = detailFocusID(u.focusedDetailFieldOrDefault()) return true default: return false } } func (u *ui) handleDetailKey(name key.Name) bool { fields := detailFocusOrder() index := u.focusedDetailIndex() switch name { case key.NameUpArrow: if index > 0 { u.keyboardFocus = detailFocusID(fields[index-1]) } return true case key.NameDownArrow: if index < len(fields)-1 { u.keyboardFocus = detailFocusID(fields[index+1]) } return true case key.NameLeftArrow: if len(u.visible) > 0 { u.setKeyboardFocus(listFocusID(u.focusedListIndexOrZero())) } return true default: return false } } func (u *ui) handleShortcutKey(name key.Name, modifiers key.Modifiers) bool { if !modifiers.Contain(key.ModShortcut) { return false } switch name { case "F": _ = u.performShortcut(shortcutSearch) case "S": _ = u.performShortcut(shortcutSave) case "L": _ = u.performShortcut(shortcutLock) case "N": _ = u.performShortcut(shortcutNewEntry) case "U": _ = u.performShortcut(shortcutCopyUser) case "P": _ = u.performShortcut(shortcutCopyPassword) case "O": _ = u.performShortcut(shortcutCopyURL) default: return false } return true } func (u *ui) activateBreadcrumb(index int) { var path []string if index <= 0 { path = nil } else { crumbs := u.breadcrumbLabels() path = append([]string{}, crumbs[1:index+1]...) } u.state.NavigateToPath(path) u.filter() if index >= len(u.breadcrumbLabels()) { index = len(u.breadcrumbLabels()) - 1 } if index < 0 { index = 0 } u.keyboardFocus = breadcrumbFocusID(index) } func (u *ui) breadcrumbLabels() []string { if u.state.Section == appstate.SectionRecycleBin { return nil } labels := append([]string{"Vault"}, u.state.CurrentPath...) if u.state.Section == appstate.SectionTemplates { labels = append([]string{"Templates"}, u.state.CurrentPath...) } return labels } func (u *ui) focusListIndex(index int) { if len(u.visible) == 0 { return } if index < 0 { index = 0 } if index >= len(u.visible) { index = len(u.visible) - 1 } u.keyboardFocus = listFocusID(index) u.state.SelectedEntryID = u.visible[index].ID u.loadSelectedEntryIntoEditor() } func (u *ui) focusedListIndexOrZero() int { if strings.HasPrefix(string(u.keyboardFocus), "list:") { index := focusIndex(u.keyboardFocus) if index >= 0 && index < len(u.visible) { return index } } for i, item := range u.visible { if item.ID == u.state.SelectedEntryID { return i } } return 0 } func (u *ui) focusedDetailFieldOrDefault() detailField { if strings.HasPrefix(string(u.keyboardFocus), "detail:") { name := strings.TrimPrefix(string(u.keyboardFocus), "detail:") for _, field := range detailFocusOrder() { if string(field) == name { return field } } } return detailFieldTitle } func (u *ui) focusedDetailIndex() int { current := u.focusedDetailFieldOrDefault() for i, field := range detailFocusOrder() { if field == current { return i } } return 0 } func detailFocusOrder() []detailField { return []detailField{ detailFieldID, detailFieldTitle, detailFieldUsername, detailFieldPassword, detailFieldURL, detailFieldPath, detailFieldTags, detailFieldPasswordProfile, detailFieldNotes, detailFieldFields, detailFieldHistoryIndex, } } func canonicalFocusID(id focusID) focusID { switch { case strings.HasPrefix(string(id), "breadcrumb:"): return breadcrumbFocusID(0) case strings.HasPrefix(string(id), "list:"): return listFocusID(0) case strings.HasPrefix(string(id), "detail:"): return detailFocusID(detailFieldTitle) default: return id } } func focusIndex(id focusID) int { _, value, ok := strings.Cut(string(id), ":") if !ok { return 0 } index, err := strconv.Atoi(value) if err != nil { return 0 } return index }