379 lines
8.2 KiB
Go
379 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gioui.org/io/key"
|
|
"git.julianfamily.org/keepassgo/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.runAction("unlock vault", u.unlockAction)
|
|
return true
|
|
}
|
|
if u.shouldShowLifecycleSetup() && name == key.NameReturn {
|
|
if u.lifecycleMode == "remote" {
|
|
u.runAction("open remote vault", u.openRemoteAction)
|
|
} else {
|
|
u.runAction("open vault", u.openVaultAction)
|
|
}
|
|
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
|
|
}
|