Simplify KeePassGO desktop vault workflow

This commit is contained in:
Joe Julian
2026-03-29 14:50:33 -07:00
parent 62fc343ecf
commit 4a629c16bd
6 changed files with 777 additions and 281 deletions
+371 -158
View File
@@ -7,6 +7,8 @@ import (
"image"
"image/color"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
@@ -57,102 +59,111 @@ type uiSurface struct {
Locked bool
}
type sessionStatus interface {
HasVault() bool
IsLocked() bool
IsRemote() bool
}
type attachmentItem struct {
Name string
Size int
}
type ui struct {
mode string
theme *material.Theme
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
historyIndex widget.Editor
groupName widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
list widget.List
detailList widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
replaceAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
createGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
togglePasswordInline widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
masterKeyPasswordOnly widget.Clickable
masterKeyKeyFileOnly widget.Clickable
masterKeyComposite widget.Clickable
entryClicks []widget.Clickable
historyClicks []widget.Clickable
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
state appstate.State
masterKeyMode vault.MasterKeyMode
visible []entry
currentPath []string
syncedPath []string
selectedHistoryIndex int
showPassword bool
togglePassword widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
lifecycleMode string
keyboardFocus focusID
mode string
theme *material.Theme
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
historyIndex widget.Editor
groupName widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
list widget.List
detailList widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
synchronizeVault widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
pickKeyFile widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
replaceAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
createGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
togglePasswordInline widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
entryClicks []widget.Clickable
historyClicks []widget.Clickable
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
state appstate.State
visible []entry
currentPath []string
syncedPath []string
selectedHistoryIndex int
showPassword bool
togglePassword widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
lifecycleMode string
keyboardFocus focusID
defaultSaveAsPath string
editingEntry bool
}
var (
@@ -225,9 +236,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui {
List: layout.List{Axis: layout.Vertical},
},
state: appstate.State{},
masterKeyMode: vault.MasterKeyModePasswordOnly,
selectedHistoryIndex: -1,
lifecycleMode: "local",
defaultSaveAsPath: defaultSaveAsPath(),
}
u.state.Session = sess
u.phoneSplit.Value = 0.46
@@ -253,6 +264,14 @@ func (u *ui) filter() {
}
}
func defaultSaveAsPath() string {
cacheDir, err := os.UserCacheDir()
if err != nil || strings.TrimSpace(cacheDir) == "" {
cacheDir = os.TempDir()
}
return filepath.Join(cacheDir, "keepassgo", "vault.kdbx")
}
func (u *ui) selectedAttachmentItems() []attachmentItem {
item, ok := u.selectedEntry()
if !ok || len(item.Attachments) == 0 {
@@ -370,26 +389,11 @@ func (u *ui) ensureHistoryClickables() {
func (u *ui) currentMasterKey() (vault.MasterKey, error) {
password := u.masterPassword.Text()
path := strings.TrimSpace(u.keyFilePath.Text())
switch u.masterKeyMode {
case vault.MasterKeyModeKeyFileOnly:
if path == "" {
return vault.MasterKey{}, fmt.Errorf("key file is required")
}
case vault.MasterKeyModePasswordAndKeyFile:
if password == "" {
return vault.MasterKey{}, fmt.Errorf("master password is required")
}
if path == "" {
return vault.MasterKey{}, fmt.Errorf("key file is required")
}
default:
if path == "" {
if password == "" {
return vault.MasterKey{}, fmt.Errorf("master password is required")
}
return vault.MasterKey{Password: password}, nil
}
if password == "" && path == "" {
return vault.MasterKey{}, fmt.Errorf("master password or key file is required")
}
if path == "" {
return vault.MasterKey{Password: password}, nil
}
content, err := os.ReadFile(path)
@@ -406,9 +410,7 @@ func (u *ui) currentMasterKey() (vault.MasterKey, error) {
}, nil
}
func (u *ui) setMasterKeyMode(mode vault.MasterKeyMode) {
u.masterKeyMode = mode
}
func (u *ui) setMasterKeyMode(vault.MasterKeyMode) {}
func (u *ui) createVaultAction() error {
key, err := u.currentMasterKey()
@@ -418,7 +420,14 @@ func (u *ui) createVaultAction() error {
if err := u.state.CreateVault(key); err != nil {
return err
}
if u.lifecycleMode == "local" {
if err := u.state.SaveAs(u.saveAsTargetPath()); err != nil {
return err
}
u.vaultPath.SetText(u.saveAsTargetPath())
}
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.editingEntry = false
u.filter()
return nil
}
@@ -436,6 +445,7 @@ func (u *ui) openVaultAction() error {
return err
}
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.editingEntry = false
u.filter()
return nil
}
@@ -449,14 +459,11 @@ func (u *ui) saveAction() error {
}
func (u *ui) saveAsAction() error {
path := strings.TrimSpace(u.saveAsPath.Text())
if path == "" {
return errors.New(errSaveAsPathRequired)
}
path := u.saveAsTargetPath()
if err := u.state.SaveAs(path); err != nil {
return err
}
u.vaultPath.SetText(path)
u.filter()
return nil
}
@@ -474,6 +481,7 @@ func (u *ui) openRemoteAction() error {
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
return err
}
u.editingEntry = false
u.filter()
return nil
}
@@ -484,6 +492,7 @@ func (u *ui) lockAction() error {
}
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.showPassword = false
u.editingEntry = false
u.filter()
return nil
}
@@ -497,6 +506,7 @@ func (u *ui) unlockAction() error {
return err
}
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.editingEntry = false
u.filter()
return nil
}
@@ -509,6 +519,22 @@ func (u *ui) changeMasterKeyAction() error {
return u.state.ChangeMasterKey(key)
}
func (u *ui) synchronizeAction() error {
if err := u.state.Synchronize(); err != nil {
return err
}
u.filter()
return nil
}
func (u *ui) saveAsTargetPath() string {
path := strings.TrimSpace(u.saveAsPath.Text())
if path != "" {
return path
}
return u.defaultSaveAsPath
}
func (u *ui) runAction(label string, action func() error) {
u.loadingMessage = actionLoadingLabel(label)
if err := action(); err != nil {
@@ -572,6 +598,37 @@ func (u *ui) sessionSurface() uiSurface {
return uiSurface{}
}
func (u *ui) hasOpenVault() bool {
status, ok := u.state.Session.(sessionStatus)
if ok {
return status.HasVault()
}
_, err := u.state.Session.Current()
return err == nil
}
func (u *ui) isVaultLocked() bool {
status, ok := u.state.Session.(sessionStatus)
if ok {
return status.IsLocked()
}
_, err := u.state.Session.Current()
return errors.Is(err, session.ErrLocked)
}
func (u *ui) shouldShowLifecycleSetup() bool {
return !u.hasOpenVault()
}
func (u *ui) chooseExistingFileAction(target *widget.Editor) error {
path, err := pickExistingFile()
if err != nil {
return err
}
target.SetText(path)
return nil
}
func (u *ui) listEmptyMessage() string {
if surface := u.sessionSurface(); surface.Locked {
return "Unlock the vault to browse entries and groups."
@@ -661,18 +718,12 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.changeMasterKey.Clicked(gtx) {
u.runAction("change master key", u.changeMasterKeyAction)
}
for u.synchronizeVault.Clicked(gtx) {
u.runAction("synchronize vault", u.synchronizeAction)
}
for u.unlockVault.Clicked(gtx) {
u.runAction("unlock vault", u.unlockAction)
}
for u.masterKeyPasswordOnly.Clicked(gtx) {
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
}
for u.masterKeyKeyFileOnly.Clicked(gtx) {
u.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
}
for u.masterKeyComposite.Clicked(gtx) {
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
}
for u.showEntries.Clicked(gtx) {
u.showEntriesSection()
}
@@ -691,10 +742,25 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.lockVault.Clicked(gtx) {
u.runAction("lock vault", u.lockAction)
}
for u.editEntry.Clicked(gtx) {
u.editingEntry = true
u.loadSelectedEntryIntoEditor()
}
for u.cancelEdit.Clicked(gtx) {
u.editingEntry = false
u.loadSelectedEntryIntoEditor()
}
for u.pickVaultPath.Clicked(gtx) {
u.runAction("choose vault path", func() error { return u.chooseExistingFileAction(&u.vaultPath) })
}
for u.pickKeyFile.Clicked(gtx) {
u.runAction("choose key file", func() error { return u.chooseExistingFileAction(&u.keyFilePath) })
}
for u.addEntry.Clicked(gtx) {
u.state.BeginNewEntry()
u.loadSelectedEntryIntoEditor()
u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / "))
u.editingEntry = true
}
for u.saveEntry.Clicked(gtx) {
u.runAction("save entry", u.saveEntryAction)
@@ -778,6 +844,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
)
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if u.shouldShowLifecycleSetup() {
return layout.Dimensions{}
}
if u.mode == "phone" || gtx.Constraints.Max.X < gtx.Dp(unit.Dp(720)) {
u.phoneSpan = gtx.Constraints.Max.Y
listHeight := int(float32(gtx.Constraints.Max.Y) * u.phoneSplit.Value)
@@ -821,15 +890,19 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
return btn.Layout(gtx)
}),
layout.Rigid(u.headerActions),
)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !u.shouldShowLifecycleSetup() {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(u.lifecycleControls),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(u.lifecycleControls),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(u.feedbackBanner),
)
})
@@ -843,20 +916,45 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions {
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
return btn.Layout(gtx)
}),
layout.Rigid(u.headerActions),
)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !u.shouldShowLifecycleSetup() {
return layout.Dimensions{}
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(u.lifecycleControls),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(u.lifecycleControls),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(u.feedbackBanner),
)
})
}
func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
if u.shouldShowLifecycleSetup() {
return layout.Dimensions{}
}
if u.isVaultLocked() {
btn := material.Button(u.theme, &u.unlockVault, "Unlock")
return btn.Layout(gtx)
}
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.synchronizeVault, "Synchronize")
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
return btn.Layout(gtx)
}),
)
}
func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
panel := card
spacing := unit.Dp(12)
@@ -867,13 +965,33 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
u.ensureNavClickables()
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(u.sectionBar),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() {
return layout.Dimensions{}
}
return u.sectionBar(gtx)
}),
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(u.pathBar),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() {
return layout.Dimensions{}
}
return u.pathBar(gtx)
}),
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(u.groupBar),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() {
return layout.Dimensions{}
}
return u.groupBar(gtx)
}),
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(u.groupControls),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() {
return layout.Dimensions{}
}
return u.groupControls(gtx)
}),
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.mode == "phone" {
@@ -888,6 +1006,9 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
}),
layout.Rigid(layout.Spacer{Height: spacing}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.isVaultLocked() || u.state.Section != appstate.SectionEntries {
return layout.Dimensions{}
}
label := "Add Entry"
if u.mode == "phone" {
label = "+ Add Entry"
@@ -1065,16 +1186,28 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
panel = compactCard
}
return panel(gtx, func(gtx layout.Context) layout.Dimensions {
item, ok := u.selectedEntry()
if !ok {
if u.isVaultLocked() {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
surface := u.sessionSurface()
title := surface.Title
if title == "" {
title = "Entry details"
}
lbl := material.Label(u.theme, unit.Sp(18), title)
lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), "Enter the master password, choose a key file, or provide both.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(u.unlockPanel),
)
}
item, ok := u.selectedEntry()
if !ok && !u.editingEntry {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(18), "Entry details")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
@@ -1084,7 +1217,20 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
)
}
if u.editingEntry {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
title := "New Entry"
if ok {
title = "Edit Entry"
}
lbl := material.Label(u.theme, unit.Sp(18), title)
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(u.entryEditorPanel),
)
}
@@ -1156,7 +1302,40 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
layout.Spacer{Height: unit.Dp(12)}.Layout,
u.historyPanel,
layout.Spacer{Height: unit.Dp(12)}.Layout,
u.entryEditorPanel,
func(gtx layout.Context) layout.Dimensions {
switch u.state.Section {
case appstate.SectionTemplates:
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.editEntry, "Edit Template")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.instantiateTemplate, "Instantiate")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.deleteTemplate, "Delete Template")
}),
)
case appstate.SectionRecycleBin:
return tonedButton(gtx, u.theme, &u.restoreEntry, "Restore Entry")
default:
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.editEntry, "Edit")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.duplicateEntry, "Duplicate")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.deleteEntry, "Delete")
}),
)
}
},
}
return material.List(u.theme, &u.detailList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions {
return rows[i](gtx)
@@ -1310,7 +1489,7 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions {
}
u.syncCurrentPath()
crumbs := append([]string{"Vault"}, append([]string{}, u.currentPath...)...)
crumbs := append([]string{"All Entries"}, append([]string{}, u.currentPath...)...)
if u.state.Section == appstate.SectionTemplates {
crumbs = append([]string{"Templates"}, append([]string{}, u.currentPath...)...)
}
@@ -1365,7 +1544,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.filter()
}
btn := material.Button(u.theme, &u.groupClicks[idx], "Folder: "+name)
btn := material.Button(u.theme, &u.groupClicks[idx], name)
btn.Background = color.NRGBA{R: 241, G: 236, B: 227, A: 255}
btn.Color = accentColor
btn.TextSize = unit.Sp(12)
@@ -1575,6 +1754,18 @@ type uiSession struct {
locked bool
}
func (s *uiSession) HasVault() bool {
return len(s.model.Entries) > 0 || len(s.model.Templates) > 0 || len(s.model.RecycleBin) > 0 || len(s.model.Groups) > 0 || s.locked
}
func (s *uiSession) IsLocked() bool {
return s.locked
}
func (s *uiSession) IsRemote() bool {
return false
}
func (s *uiSession) Current() (vault.Model, error) {
if s.locked {
return vault.Model{}, session.ErrLocked
@@ -1598,3 +1789,25 @@ func (s *uiSession) Unlock(vault.MasterKey) error {
s.locked = false
return nil
}
func pickExistingFile() (string, error) {
if path, err := runFilePicker("kdialog", "--getopenfilename", "--title", "Choose KeePass file"); err == nil {
return path, nil
}
if path, err := runFilePicker("zenity", "--file-selection", "--title=Choose KeePass file"); err == nil {
return path, nil
}
return "", fmt.Errorf("no supported file picker found; install kdialog or zenity")
}
func runFilePicker(name string, args ...string) (string, error) {
if _, err := exec.LookPath(name); err != nil {
return "", err
}
cmd := exec.Command(name, args...)
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}