From e5f42924f8e753ed12675103d5ec1bf4b539e113 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Sun, 5 Apr 2026 21:24:36 -0700 Subject: [PATCH] Simplify desktop unlock flow --- main.go | 88 +++++++++++++++++++++-------------------- main_test.go | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ ui_forms.go | 65 +++++++++++++++++++++++-------- 3 files changed, 202 insertions(+), 59 deletions(-) diff --git a/main.go b/main.go index a7ae279..4d0e392 100644 --- a/main.go +++ b/main.go @@ -1776,6 +1776,44 @@ func (u *ui) showLocalVaultChooser() bool { return u.lifecycleMode != "local" || !u.hasSelectedVaultPath() } +func (u *ui) switchToLifecycleSelection(mode string) { + u.state.Session = &session.Manager{} + u.state.CurrentPath = nil + u.state.SelectedEntryID = "" + u.state.Section = appstate.SectionEntries + u.state.Dirty = false + u.state.ErrorMessage = "" + u.state.StatusMessage = "" + u.loadingMessage = "" + u.loadingActionLabel = "" + u.lastLifecycleAction = "" + u.lifecycleMode = mode + u.editingEntry = false + u.currentPath = nil + u.syncedPath = nil + u.clearMasterPassword() + u.keyFilePath.SetText("") + u.search.SetText("") + switch mode { + case "remote": + u.vaultPath.SetText("") + u.remoteBaseURL.SetText("") + u.remotePath.SetText("") + u.remoteUsername.SetText("") + u.remotePassword.SetText("") + u.rememberRemoteAuth.Value = false + default: + u.vaultPath.SetText("") + u.remoteBaseURL.SetText("") + u.remotePath.SetText("") + u.remoteUsername.SetText("") + u.remotePassword.SetText("") + u.rememberRemoteAuth.Value = false + } + u.requestMasterPassFocus = u.hasSelectedLifecycleTarget() + u.filter() +} + func (u *ui) latestRecentRemote() (recentRemoteRecord, bool, time.Time) { for _, record := range u.recentRemotes { if strings.TrimSpace(record.BaseURL) == "" || strings.TrimSpace(record.Path) == "" { @@ -3153,6 +3191,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { if u.lifecycleBusy() { continue } + if u.shouldUseLockedSinglePane() { + u.switchToLifecycleSelection("local") + continue + } u.vaultPath.SetText("") u.state.ErrorMessage = "" u.state.StatusMessage = "" @@ -3162,6 +3204,10 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { if u.lifecycleBusy() { continue } + if u.shouldUseLockedSinglePane() { + u.switchToLifecycleSelection("remote") + continue + } u.remoteBaseURL.SetText("") u.remotePath.SetText("") u.remoteUsername.SetText("") @@ -4692,7 +4738,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions { _ = panel return layout.Flex{Axis: layout.Vertical}.Layout(gtx, func() []layout.FlexChild { if u.isVaultLocked() { - summary := u.currentVaultSummary() return []layout.FlexChild{ layout.Rigid(func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(18), "Unlock Vault") @@ -4705,47 +4750,6 @@ func (u *ui) detailPanelContent(gtx layout.Context) layout.Dimensions { lbl.Color = mutedColor return lbl.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if strings.TrimSpace(summary.Title) == "" { - return layout.Dimensions{} - } - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(11), "UNLOCK TARGET") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(15), summary.Title) - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if strings.TrimSpace(summary.Detail) == "" || summary.Detail == summary.Title { - return layout.Dimensions{} - } - return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), summary.Detail) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if strings.TrimSpace(summary.Context) == "" { - return layout.Dimensions{} - } - return layout.Inset{Top: unit.Dp(2)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), summary.Context) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }) - }), - ) - }) - }), layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout), layout.Rigid(u.unlockPanel), } diff --git a/main_test.go b/main_test.go index 94c7eed..46c00cb 100644 --- a/main_test.go +++ b/main_test.go @@ -4377,6 +4377,114 @@ func TestShowLocalVaultChooser(t *testing.T) { } } +func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", summarySession{hasVault: true, locked: true}) + u.lifecycleMode = "local" + u.vaultPath.SetText("/vaults/bellagio.kdbx") + u.remoteBaseURL.SetText("https://dav.crew.example.invalid") + u.remotePath.SetText("vaults/remote.kdbx") + u.remoteUsername.SetText("dannyocean") + u.remotePassword.SetText("topsecret") + u.rememberRemoteAuth.Value = true + u.masterPassword.SetText("correct horse battery staple") + u.keyFilePath.SetText("/vaults/keyfile.keyx") + u.search.SetText("crew") + u.state.CurrentPath = []string{"Crew"} + u.state.SelectedEntryID = "entry-1" + u.state.Section = appstate.SectionTemplates + u.state.Dirty = true + + u.switchToLifecycleSelection("local") + + if !u.shouldShowLifecycleSetup() { + t.Fatal("shouldShowLifecycleSetup() = false, want true after switching away from locked local vault") + } + if got := u.lifecycleMode; got != "local" { + t.Fatalf("lifecycleMode = %q, want local", got) + } + if got := u.vaultPath.Text(); got != "" { + t.Fatalf("vaultPath = %q, want empty", got) + } + if got := u.remoteBaseURL.Text(); got != "" { + t.Fatalf("remoteBaseURL = %q, want empty", got) + } + if got := u.remotePath.Text(); got != "" { + t.Fatalf("remotePath = %q, want empty", got) + } + if got := u.remoteUsername.Text(); got != "" { + t.Fatalf("remoteUsername = %q, want empty", got) + } + if got := u.remotePassword.Text(); got != "" { + t.Fatalf("remotePassword = %q, want empty", got) + } + if u.rememberRemoteAuth.Value { + t.Fatal("rememberRemoteAuth = true, want false") + } + if got := u.masterPassword.Text(); got != "" { + t.Fatalf("masterPassword = %q, want empty", got) + } + if got := u.keyFilePath.Text(); got != "" { + t.Fatalf("keyFilePath = %q, want empty", got) + } + if got := u.search.Text(); got != "" { + t.Fatalf("search = %q, want empty", got) + } + if got := u.state.Section; got != appstate.SectionEntries { + t.Fatalf("state.Section = %q, want %q", got, appstate.SectionEntries) + } + if len(u.state.CurrentPath) != 0 { + t.Fatalf("state.CurrentPath = %v, want empty", u.state.CurrentPath) + } + if got := u.state.SelectedEntryID; got != "" { + t.Fatalf("state.SelectedEntryID = %q, want empty", got) + } + if u.state.Dirty { + t.Fatal("state.Dirty = true, want false") + } +} + +func TestSwitchToLifecycleSelectionResetsLockedRemoteSession(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", summarySession{hasVault: true, locked: true, remote: true}) + u.lifecycleMode = "local" + u.vaultPath.SetText("/vaults/bellagio.kdbx") + u.remoteBaseURL.SetText("https://dav.crew.example.invalid") + u.remotePath.SetText("vaults/remote.kdbx") + u.remoteUsername.SetText("rustyryan") + u.remotePassword.SetText("topsecret") + u.rememberRemoteAuth.Value = true + + u.switchToLifecycleSelection("remote") + + if !u.shouldShowLifecycleSetup() { + t.Fatal("shouldShowLifecycleSetup() = false, want true after switching away from locked remote vault") + } + if got := u.lifecycleMode; got != "remote" { + t.Fatalf("lifecycleMode = %q, want remote", got) + } + if got := u.vaultPath.Text(); got != "" { + t.Fatalf("vaultPath = %q, want empty", got) + } + if got := u.remoteBaseURL.Text(); got != "" { + t.Fatalf("remoteBaseURL = %q, want empty", got) + } + if got := u.remotePath.Text(); got != "" { + t.Fatalf("remotePath = %q, want empty", got) + } + if got := u.remoteUsername.Text(); got != "" { + t.Fatalf("remoteUsername = %q, want empty", got) + } + if got := u.remotePassword.Text(); got != "" { + t.Fatalf("remotePassword = %q, want empty", got) + } + if u.rememberRemoteAuth.Value { + t.Fatal("rememberRemoteAuth = true, want false") + } +} + func TestSelectingRecentRemoteSwitchesToRemoteMode(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index 9af73bb..5035f08 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -1250,12 +1250,14 @@ func selectorEditorHelp(th *material.Theme, label, help string, editor *widget.E func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { targetLabel := "Locked vault" targetValue := "Unlock the active vault to continue." + changeLabel := "Open Different Vault" if u.state.Session != nil { if strings.TrimSpace(u.remoteBaseURL.Text()) != "" || strings.TrimSpace(u.remotePath.Text()) != "" { baseURL := strings.TrimSpace(u.remoteBaseURL.Text()) path := strings.TrimSpace(u.remotePath.Text()) targetLabel = "Remote vault" targetValue = friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: path}) + changeLabel = "Open Different Connection" if strings.TrimSpace(targetValue) == "" { targetValue = "Remote WebDAV vault" } @@ -1271,25 +1273,42 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { } } } + targetCard := func(gtx layout.Context) layout.Dimensions { + return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel)) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Body1(u.theme, targetValue) + lbl.Color = accentColor + return lbl.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if !u.shouldUseLockedSinglePane() { + return layout.Dimensions{} + } + return layout.Inset{Top: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + if targetLabel == "Remote vault" { + return tonedButton(gtx, u.theme, &u.clearRemoteSelection, changeLabel) + } + return tonedButton(gtx, u.theme, &u.clearVaultSelection, changeLabel) + }) + }), + ) + }) + }) + } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), strings.ToUpper(targetLabel)) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Body1(u.theme, targetValue) - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - ) - }) - }) + if u.mode == "desktop" { + return layout.Dimensions{} + } + return targetCard(gtx) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { @@ -1301,6 +1320,18 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock") }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.mode != "desktop" { + return layout.Dimensions{} + } + return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if u.mode != "desktop" { + return layout.Dimensions{} + } + return targetCard(gtx) + }), ) }