From fae8e1cbb4cebdfaba27e6c1f5b27cc055d9db58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Thu, 15 Oct 2026 10:39:38 +0200 Subject: [PATCH] refactor: drop dead code and use the standard library where it already does the job (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No behaviour change. - Deleted, unused or one-line wrappers: panes.RuneIndexes, Detail.Refresh (SetDescribed does it), SecretValue.Len, Query.Empty, Clipboard.Native, config.Load, get's value, Model.done, providerOf (cachefile.ProviderOf), contextWithTimeout, searchCoverage, startCrawl, showKeybindings, the expiry aliases and daysLeft. - cmp.Or instead of the two cmpOr copies; slices.Sorted(maps.Keys(…)), maps.Copy, wg.Go, utf8.RuneCountInString and new(true) instead of loops and helpers that do the same. - A container's pane title is set once, not twice; a redundant length check in share links and a no-op assignment in linkhandler.Launch are gone. - Tests call ansi.Strip and lastLines directly; gofmt -s on links.go. Reviewed-on: https://git.kebcloud.no/keb/lazysecrets/pulls/76 Co-authored-by: Karl-Einar Bergstrøm --- cmd/lazysecrets/get.go | 14 ++---- cmd/lazysecrets/get_test.go | 4 +- cmd/lazysecrets/main.go | 11 +---- internal/clipboard/clipboard.go | 3 -- internal/commands/commands.go | 6 +-- internal/config/config.go | 14 ++---- internal/config/config_test.go | 14 +++--- internal/instance/instance.go | 3 +- internal/linkhandler/install_darwin.go | 8 +--- internal/linkhandler/terminal.go | 1 - internal/match/match.go | 3 -- internal/match/match_test.go | 6 --- internal/provider/azure/details.go | 8 +--- internal/provider/fake/fake.go | 65 ++++++++++---------------- internal/provider/repo/repo.go | 6 +-- internal/provider/value.go | 2 - internal/share/rules.go | 3 +- internal/share/share.go | 2 +- internal/ui/app/app.go | 25 ++++------ internal/ui/app/app_test.go | 8 ++-- internal/ui/app/cache.go | 2 +- internal/ui/app/commands.go | 29 ++++-------- internal/ui/app/commands_test.go | 3 +- internal/ui/app/edit.go | 2 +- internal/ui/app/expiring.go | 9 ++-- internal/ui/app/expiry.go | 18 ++----- internal/ui/app/favourites.go | 7 +-- internal/ui/app/favourites_test.go | 3 +- internal/ui/app/fields.go | 9 ++-- internal/ui/app/help.go | 10 ++-- internal/ui/app/layout_test.go | 11 ++--- internal/ui/app/links.go | 7 +-- internal/ui/app/notes_test.go | 12 ++--- internal/ui/app/repo_test.go | 5 +- internal/ui/app/search.go | 30 +++--------- internal/ui/app/search_test.go | 39 ++++++++-------- internal/ui/app/searchview.go | 6 +-- internal/ui/app/status.go | 5 +- internal/ui/app/usage_test.go | 5 +- internal/ui/keys/keys.go | 19 ++------ internal/ui/panes/detail.go | 10 ---- internal/ui/panes/list.go | 9 ---- internal/ui/styles/theme.go | 9 +--- 43 files changed, 161 insertions(+), 304 deletions(-) diff --git a/cmd/lazysecrets/get.go b/cmd/lazysecrets/get.go index b02f192..a3db4d2 100644 --- a/cmd/lazysecrets/get.go +++ b/cmd/lazysecrets/get.go @@ -18,7 +18,7 @@ import ( // It ends with a newline only when printing to a terminal, so command // substitution and pipes get the value exactly. func get(ctx context.Context, providers []provider.Provider, st *store.Store, r ref.Ref) error { - v, err := value(ctx, providers, st, r) + v, _, err := readKey(ctx, providers, st, r) if err != nil { return err } @@ -30,15 +30,9 @@ func get(ctx context.Context, providers []provider.Provider, st *store.Store, r return err } -// value reads what r points at (ref.Read), with the account that last -// read it first. -func value(ctx context.Context, providers []provider.Provider, st *store.Store, r ref.Ref) (provider.SecretValue, error) { - v, _, err := readKey(ctx, providers, st, r) - return v, err -} - -// readKey is value, also returning the version read. The account asked -// for (--account) goes first, else the one that last read it. +// readKey reads what r points at (ref.Read), and the version read. The +// account asked for (--account) goes first, else the one that last read +// it. func readKey(ctx context.Context, providers []provider.Provider, st *store.Store, r ref.Ref) (provider.SecretValue, string, error) { key := r key.Version = "" diff --git a/cmd/lazysecrets/get_test.go b/cmd/lazysecrets/get_test.go index 60dc19b..65a4429 100644 --- a/cmd/lazysecrets/get_test.go +++ b/cmd/lazysecrets/get_test.go @@ -20,7 +20,7 @@ func TestValue(t *testing.T) { "lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/app/storage-key": "storage-demo-key", } { r, _ := ref.Parse(link) - v, err := value(context.Background(), providers, st, r) + v, _, err := readKey(context.Background(), providers, st, r) if err != nil || v.Reveal() != want { t.Errorf("%s: %q, %v", link, v.Reveal(), err) } @@ -31,7 +31,7 @@ func TestValue(t *testing.T) { "{{secrets/app/storage-key}}": "names no workspace", } { r, _ := ref.Parse(link) - if _, err := value(context.Background(), providers, st, r); err == nil || !strings.Contains(err.Error(), wantErr) { + if _, _, err := readKey(context.Background(), providers, st, r); err == nil || !strings.Contains(err.Error(), wantErr) { t.Errorf("%s: %v, want %q", link, err, wantErr) } } diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 8b789f9..79369f3 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -68,27 +68,20 @@ func checkCommandKeys(km *keys.Map, cmds []commands.Command) error { if c.Key == "" { continue } - name := fmt.Sprintf("custom command %q", cmpOr(c.Description, c.Command)) + name := fmt.Sprintf("custom command %q", cmp.Or(c.Description, c.Command)) if err := km.CheckExtra(c.Key, name); err != nil { return err } for _, d := range cmds[:i] { overlap := c.Context == d.Context || c.Context == commands.Global || d.Context == commands.Global if d.Key == c.Key && overlap { - return fmt.Errorf("%s: %q is also the key of custom command %q", name, c.Key, cmpOr(d.Description, d.Command)) + return fmt.Errorf("%s: %q is also the key of custom command %q", name, c.Key, cmp.Or(d.Description, d.Command)) } } } return nil } -func cmpOr(a, b string) string { - if a != "" { - return a - } - return b -} - // refArg parses get's and open's arguments: a reference, and optionally // the account to use when several could (lazysecrets list's column) and, // for open, what to do with it rather than asking (--action). diff --git a/internal/clipboard/clipboard.go b/internal/clipboard/clipboard.go index cbab604..b7a894c 100644 --- a/internal/clipboard/clipboard.go +++ b/internal/clipboard/clipboard.go @@ -42,9 +42,6 @@ func New() Clipboard { } } -// Native reports whether the system clipboard is used rather than OSC52. -func (c Clipboard) Native() bool { return c.native } - // Write puts s on the clipboard. func (c Clipboard) Write(s string) tea.Cmd { if !c.native { diff --git a/internal/commands/commands.go b/internal/commands/commands.go index d1fd71e..d17651a 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -16,6 +16,7 @@ package commands import ( + "cmp" "fmt" "regexp" "strings" @@ -60,10 +61,7 @@ var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) // Validate checks a command from the config. func (c Command) Validate() error { - name := c.Description - if name == "" { - name = c.Command - } + name := cmp.Or(c.Description, c.Command) switch { case c.Command == "": return fmt.Errorf("custom command %q has no command", c.Description) diff --git a/internal/config/config.go b/internal/config/config.go index c1db051..145afd1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "maps" "os" "path/filepath" "strings" @@ -204,17 +205,12 @@ func xdgDir(env, fallback string) (string, error) { return filepath.Join(base, "lazysecrets"), nil } -// Load reads path. A missing file is an empty config. Unknown keys are an -// error, so typos don't silently do nothing. -func Load(path string) (Config, error) { - return LoadAll([]string{path}) -} - // LoadAll reads several config files in order, each on top of the ones // before, like lazygit's LG_CONFIG_FILE: a later file overrides the // settings it sets, and theme roles merge one by one. That lets an editor // plugin add a generated theme file to the user's own config. Missing -// files are skipped. +// files are skipped. Unknown keys are an error, so typos don't silently do +// nothing. func LoadAll(paths []string) (Config, error) { var c Config keybinding := map[string]map[string][]string{} @@ -229,9 +225,7 @@ func LoadAll(paths []string) (Config, error) { if keybinding[ctx] == nil { keybinding[ctx] = map[string][]string{} } - for name, keys := range actions { - keybinding[ctx][name] = keys - } + maps.Copy(keybinding[ctx], actions) } } c.Keybinding = keybinding diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7cd0c4d..f3aea3a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,7 +18,7 @@ func write(t *testing.T, content string) string { } func TestLoad(t *testing.T) { - c, err := Load(write(t, ` + c, err := LoadAll([]string{write(t, ` ui: icons: false sourcesView: flat @@ -36,7 +36,7 @@ providers: databricks: enabled: false hideProfiles: [test] -`)) +`)}) if err != nil { t.Fatal(err) } @@ -61,7 +61,7 @@ func TestLoadMissingAndEmpty(t *testing.T) { "missing": filepath.Join(t.TempDir(), "nope.yaml"), "empty": write(t, ""), } { - c, err := Load(path) + c, err := LoadAll([]string{path}) if err != nil || c.UI.Icons != nil || c.Values.ClipboardClear != nil { t.Errorf("%s: %+v, %v", name, c, err) } @@ -85,7 +85,7 @@ func TestLoadRejectsMistakes(t *testing.T) { "short keys": "sharing:\n keyRules:\n minLength: 8\n", "classes": "sharing:\n keyRules:\n minClasses: 5\n", } { - if _, err := Load(write(t, content)); err == nil { + if _, err := LoadAll([]string{write(t, content)}); err == nil { t.Errorf("%s: no error", name) } else if !strings.Contains(err.Error(), "config.yaml") { t.Errorf("%s: error doesn't name the file: %v", name, err) @@ -132,18 +132,18 @@ theme: } func TestUpdates(t *testing.T) { - c, err := Load(write(t, "updates:\n autoCheck: false\n autoCheckInterval: 168h\n sources: [https://git.example.com/team/lazysecrets]\n")) + c, err := LoadAll([]string{write(t, "updates:\n autoCheck: false\n autoCheckInterval: 168h\n sources: [https://git.example.com/team/lazysecrets]\n")}) if err != nil || On(c.Updates.AutoCheck) || *c.Updates.AutoCheckInterval != 7*24*time.Hour || len(c.Updates.Sources) != 1 { t.Errorf("updates = %+v, %v", c.Updates, err) } } func TestLoadDelay(t *testing.T) { - c, err := Load(write(t, "ui:\n loadDelay: 300ms\n")) + c, err := LoadAll([]string{write(t, "ui:\n loadDelay: 300ms\n")}) if err != nil || c.UI.LoadDelay == nil || *c.UI.LoadDelay != 300*time.Millisecond { t.Errorf("loadDelay = %v, %v", c.UI.LoadDelay, err) } - if _, err := Load(write(t, "ui:\n loadDelay: 1m\n")); err == nil { + if _, err := LoadAll([]string{write(t, "ui:\n loadDelay: 1m\n")}); err == nil { t.Error("a minute's delay accepted") } } diff --git a/internal/instance/instance.go b/internal/instance/instance.go index f15375f..4358f8c 100644 --- a/internal/instance/instance.go +++ b/internal/instance/instance.go @@ -9,7 +9,6 @@ package instance import ( - "bufio" "context" "encoding/json" "errors" @@ -114,7 +113,7 @@ func (s *Server) handle(c net.Conn) { defer c.Close() _ = c.SetDeadline(time.Now().Add(5 * time.Second)) var req request - if err := json.NewDecoder(bufio.NewReader(c)).Decode(&req); err != nil { + if err := json.NewDecoder(c).Decode(&req); err != nil { return } var resp response diff --git a/internal/linkhandler/install_darwin.go b/internal/linkhandler/install_darwin.go index c9eb062..d2e6761 100644 --- a/internal/linkhandler/install_darwin.go +++ b/internal/linkhandler/install_darwin.go @@ -116,17 +116,13 @@ func Check() string { if err != nil { return "" // can't tell: assume it works } - if exe := strings.TrimSpace(string(out)); !exists(exe) { + exe := strings.TrimSpace(string(out)) + if _, err := os.Stat(exe); err != nil { return "lazysecrets:// links run " + exe + ", which no longer exists" } return "" } -func exists(path string) bool { - _, err := os.Stat(path) - return err == nil -} - // Uninstall removes the applet and its registration. func Uninstall() (string, error) { app, err := AppPath() diff --git a/internal/linkhandler/terminal.go b/internal/linkhandler/terminal.go index fd90735..f50b5a3 100644 --- a/internal/linkhandler/terminal.go +++ b/internal/linkhandler/terminal.go @@ -144,7 +144,6 @@ func appleString(s string) string { // it. func Launch(argv []string) error { cmd := exec.Command(argv[0], argv[1:]...) - cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil if err := cmd.Start(); err != nil { return err } diff --git a/internal/match/match.go b/internal/match/match.go index d8be6be..aed58e2 100644 --- a/internal/match/match.go +++ b/internal/match/match.go @@ -107,9 +107,6 @@ func parseTerm(tok string) (term, bool) { return t, true } -// Empty reports whether the query has no terms: everything matches. -func (q Query) Empty() bool { return len(q.groups) == 0 } - // Item is a row to match: Text is everything searched, and runes // NameStart up to NameEnd of it are the row's name (the rest is context, // like a path or a note). Without a name, nothing gets the name bonus. diff --git a/internal/match/match_test.go b/internal/match/match_test.go index ad3161c..acd53ac 100644 --- a/internal/match/match_test.go +++ b/internal/match/match_test.go @@ -99,9 +99,3 @@ func TestAnchorsOnTheName(t *testing.T) { t.Errorf("password$: %v", got) } } - -func TestEmpty(t *testing.T) { - if !Parse(" | ").Empty() || Parse("x").Empty() { - t.Error("Empty") - } -} diff --git a/internal/provider/azure/details.go b/internal/provider/azure/details.go index 2bd4c65..323285c 100644 --- a/internal/provider/azure/details.go +++ b/internal/provider/azure/details.go @@ -2,6 +2,7 @@ package azure import ( "fmt" + "maps" "slices" "strings" @@ -134,12 +135,7 @@ func (v vaultInfo) attrs() []provider.Attr { if v.endpoints > 0 { add("private", fmt.Sprintf("%d endpoint%s", v.endpoints, map[bool]string{true: "s"}[v.endpoints > 1])) } - keys := make([]string, 0, len(v.tags)) - for k := range v.tags { - keys = append(keys, k) - } - slices.Sort(keys) - for i, k := range keys { + for i, k := range slices.Sorted(maps.Keys(v.tags)) { label := "" if i == 0 { label = "tags" diff --git a/internal/provider/fake/fake.go b/internal/provider/fake/fake.go index 6e467ef..baeeaab 100644 --- a/internal/provider/fake/fake.go +++ b/internal/provider/fake/fake.go @@ -48,19 +48,8 @@ func newProvider(kind provider.Kind, delay time.Duration) *Provider { func (p *Provider) Kind() provider.Kind { return p.kind } -func (p *Provider) ID() string { - if p.id != "" { - return p.id - } - return string(p.kind) -} - -func (p *Provider) Label() string { - if p.label != "" { - return p.label - } - return string(p.kind) -} +func (p *Provider) ID() string { return cmp.Or(p.id, string(p.kind)) } +func (p *Provider) Label() string { return cmp.Or(p.label, string(p.kind)) } // As renames the provider, to simulate several accounts of one kind. func (p *Provider) As(id, label string) *Provider { @@ -252,11 +241,7 @@ func (p *Provider) fail(c provider.Container, err error, types ...provider.Objec } } -var ( - base = time.Date(2026, 9, 1, 9, 30, 0, 0, time.UTC) - yes = true - no = false -) +var base = time.Date(2026, 9, 1, 9, 30, 0, 0, time.UTC) // ErrForbidden mimics a data-plane permission error from a backend. var ErrForbidden = errors.New("403 Forbidden: caller lacks permission to list secrets") @@ -284,32 +269,32 @@ func Azure(delay time.Duration) *Provider { prod := p.addSource("sub-prod", "platform-prod", "tenant: example.onmicrosoft.com") kv := p.addContainer(prod, "kv-app-prod", "https://kv-app-prod.vault.azure.net", allTypes...) dbPassword := p.add(kv, provider.TypeSecret, provider.Object{ - Name: "db-password", Version: "3f9c2a", Enabled: &yes, Updated: base, + Name: "db-password", Version: "3f9c2a", Enabled: new(true), Updated: base, Expires: base.AddDate(1, 0, 0), ContentType: "text/plain", Tags: map[string]string{"owner": "data", "env": "prod"}, Reference: secretRef("kv-app-prod", "db-password"), }, "correct-horse-battery-staple") p.versions[dbPassword.ID] = []provider.Object{ - {Version: "3f9c2a", Enabled: &yes, Updated: base, Expires: base.AddDate(1, 0, 0), ContentType: "text/plain"}, - {Version: "91d3e0", Enabled: &yes, Updated: base.AddDate(0, -6, 0), Expires: base.AddDate(0, -1, 0)}, - {Version: "0a77c4", Enabled: &no, Updated: base.AddDate(-1, 0, 0)}, + {Version: "3f9c2a", Enabled: new(true), Updated: base, Expires: base.AddDate(1, 0, 0), ContentType: "text/plain"}, + {Version: "91d3e0", Enabled: new(true), Updated: base.AddDate(0, -6, 0), Expires: base.AddDate(0, -1, 0)}, + {Version: "0a77c4", Enabled: new(false), Updated: base.AddDate(-1, 0, 0)}, } p.values[dbPassword.ID+"@91d3e0"] = "previous-password" p.values[dbPassword.ID+"@0a77c4"] = "original-password" p.add(kv, provider.TypeSecret, provider.Object{ - Name: "api-key", Version: "a17b04", Enabled: &yes, Updated: base.AddDate(0, -2, 0), + Name: "api-key", Version: "a17b04", Enabled: new(true), Updated: base.AddDate(0, -2, 0), Reference: secretRef("kv-app-prod", "api-key"), }, "sk_live_51HxDEMO0000000000000000") p.add(kv, provider.TypeSecret, provider.Object{ - Name: "legacy-token", Version: "0c11de", Enabled: &yes, Updated: base.AddDate(-1, 0, 0), + Name: "legacy-token", Version: "0c11de", Enabled: new(true), Updated: base.AddDate(-1, 0, 0), Expires: base.AddDate(0, -3, 0), Reference: secretRef("kv-app-prod", "legacy-token"), }, "deprecated") p.add(kv, provider.TypeSecret, provider.Object{ - Name: "smtp-password", Version: "7d2e90", Enabled: &no, Updated: base.AddDate(0, -2, 0), + Name: "smtp-password", Version: "7d2e90", Enabled: new(false), Updated: base.AddDate(0, -2, 0), Reference: secretRef("kv-app-prod", "smtp-password"), }, "smtp-demo-password") p.add(kv, provider.TypeKey, provider.Object{ - Name: "signing-key", Version: "5e21b7", Enabled: &yes, Updated: base.AddDate(0, -3, 0), + Name: "signing-key", Version: "5e21b7", Enabled: new(true), Updated: base.AddDate(0, -3, 0), Attrs: []provider.Attr{ {Label: "key type", Value: "RSA 2048"}, {Label: "ops", Value: "sign, verify"}, @@ -317,7 +302,7 @@ func Azure(delay time.Duration) *Provider { Reference: objectURL("kv-app-prod", "keys", "signing-key"), }, demoPublicKey) p.add(kv, provider.TypeKey, provider.Object{ - Name: "data-encryption", Version: "c40f19", Enabled: &yes, Updated: base.AddDate(0, -5, 0), + Name: "data-encryption", Version: "c40f19", Enabled: new(true), Updated: base.AddDate(0, -5, 0), Attrs: []provider.Attr{ {Label: "key type", Value: "EC P-256"}, {Label: "ops", Value: "wrapKey, unwrapKey"}, @@ -326,7 +311,7 @@ func Azure(delay time.Duration) *Provider { }, demoPublicKey) p.add(kv, provider.TypeCertificate, provider.Object{ - Name: "app-tls", Version: "77e0aa", Enabled: &yes, Updated: base.AddDate(0, -1, 0), + Name: "app-tls", Version: "77e0aa", Enabled: new(true), Updated: base.AddDate(0, -1, 0), Expires: base.AddDate(0, 11, 0), ContentType: "application/x-pem-file", Attrs: []provider.Attr{ {Label: "subject", Value: "CN=app.example.com"}, @@ -338,7 +323,7 @@ func Azure(delay time.Duration) *Provider { Reference: objectURL("kv-app-prod", "certificates", "app-tls"), }, demoCert) p.add(kv, provider.TypeCertificate, provider.Object{ - Name: "old-tls", Version: "19ab3c", Enabled: &yes, Updated: base.AddDate(-1, 0, 0), + Name: "old-tls", Version: "19ab3c", Enabled: new(true), Updated: base.AddDate(-1, 0, 0), Expires: base.AddDate(0, -6, 0), ContentType: "application/x-pkcs12", Attrs: []provider.Attr{ {Label: "subject", Value: "CN=old.example.com"}, @@ -351,10 +336,10 @@ func Azure(delay time.Duration) *Provider { shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...) // A key to share env files with (S → f): random, as sharing.keyRules wants. p.add(shared, provider.TypeSecret, provider.Object{ - Name: "env-share", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "env-share"), + Name: "env-share", Enabled: new(true), Updated: base, Reference: secretRef("kv-shared-prod", "env-share"), }, "q8Zt3VwL1mXc7Rk2Pj9Ys4Hn6Bf0Ga5Ud+Te/OiWlE=") p.add(shared, provider.TypeSecret, provider.Object{ - Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"), + Name: "smtp-password", Enabled: new(true), Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"), }, "smtp-demo-password") p.fail(shared, ErrForbiddenKeys, provider.TypeKey) @@ -364,19 +349,19 @@ func Azure(delay time.Duration) *Provider { dev := p.addSource("sub-dev", "platform-dev", "tenant: example.onmicrosoft.com") kvDev := p.addContainer(dev, "kv-app-dev", "https://kv-app-dev.vault.azure.net", allTypes...) p.add(kvDev, provider.TypeSecret, provider.Object{ - Name: "db-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-app-dev", "db-password"), + Name: "db-password", Enabled: new(true), Updated: base, Reference: secretRef("kv-app-dev", "db-password"), }, "dev-password") // Left behind, as dev vaults are: expired, disabled, or both. p.add(kvDev, provider.TypeSecret, provider.Object{ - Name: "stripe-webhook-secret", Enabled: &yes, Updated: base.AddDate(-1, 0, 0), + Name: "stripe-webhook-secret", Enabled: new(true), Updated: base.AddDate(-1, 0, 0), Expires: base.AddDate(0, -2, 0), Reference: secretRef("kv-app-dev", "stripe-webhook-secret"), }, "whsec_demo_expired") p.add(kvDev, provider.TypeSecret, provider.Object{ - Name: "test-user-password", Enabled: &no, Updated: base.AddDate(0, -5, 0), + Name: "test-user-password", Enabled: new(false), Updated: base.AddDate(0, -5, 0), Reference: secretRef("kv-app-dev", "test-user-password"), }, "disabled-demo-password") p.add(kvDev, provider.TypeSecret, provider.Object{ - Name: "vendor-token", Enabled: &no, Updated: base.AddDate(-2, 0, 0), + Name: "vendor-token", Enabled: new(false), Updated: base.AddDate(-2, 0, 0), Expires: base.AddDate(-1, 0, 0), Reference: secretRef("kv-app-dev", "vendor-token"), }, "expired-and-disabled") @@ -395,17 +380,17 @@ func AzureOps(delay time.Duration) *Provider { prod := p.addSource("sub-prod", "platform-prod", "tenant: example.onmicrosoft.com") breakGlass := p.addContainer(prod, "kv-breakglass", "https://kv-breakglass.vault.azure.net", allTypes...) p.add(breakGlass, provider.TypeSecret, provider.Object{ - Name: "emergency-admin", Enabled: &yes, Updated: base.AddDate(0, -4, 0), + Name: "emergency-admin", Enabled: new(true), Updated: base.AddDate(0, -4, 0), Expires: time.Now().AddDate(0, 0, 5).Truncate(24 * time.Hour), // soon, whenever the demo runs Tags: map[string]string{"owner": "ops"}, Reference: secretRef("kv-breakglass", "emergency-admin"), }, "break-glass-demo-password") shared := p.addContainer(prod, "kv-shared-prod", "https://kv-shared-prod.vault.azure.net", allTypes...) p.add(shared, provider.TypeSecret, provider.Object{ - Name: "smtp-password", Enabled: &yes, Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"), + Name: "smtp-password", Enabled: new(true), Updated: base, Reference: secretRef("kv-shared-prod", "smtp-password"), }, "smtp-demo-password") // Unlike the first account, ops may read keys here. p.add(shared, provider.TypeKey, provider.Object{ - Name: "backup-encryption", Version: "e9a102", Enabled: &yes, Updated: base.AddDate(0, -2, 0), + Name: "backup-encryption", Version: "e9a102", Enabled: new(true), Updated: base.AddDate(0, -2, 0), Attrs: []provider.Attr{{Label: "key type", Value: "RSA-HSM 3072"}, {Label: "ops", Value: "wrapKey, unwrapKey"}}, Reference: "https://kv-shared-prod.vault.azure.net/keys/backup-encryption", }, demoPublicKey) @@ -413,12 +398,12 @@ func AzureOps(delay time.Duration) *Provider { sandbox := p.addSource("sub-ops", "ops-sandbox", "tenant: example.onmicrosoft.com") kv := p.addContainer(sandbox, "kv-ops-tools", "https://kv-ops-tools.vault.azure.net", allTypes...) p.add(kv, provider.TypeSecret, provider.Object{ - Name: "pagerduty-token", Enabled: &yes, Updated: base.AddDate(0, -1, 0), Reference: secretRef("kv-ops-tools", "pagerduty-token"), + Name: "pagerduty-token", Enabled: new(true), Updated: base.AddDate(0, -1, 0), Reference: secretRef("kv-ops-tools", "pagerduty-token"), }, "pd-demo-token") // Expiring soon whenever the demo runs: the other dates are fixed, // and would have passed. Tests use the first account only. p.add(kv, provider.TypeSecret, provider.Object{ - Name: "grafana-api-key", Enabled: &yes, Updated: base.AddDate(0, -11, 0), + Name: "grafana-api-key", Enabled: new(true), Updated: base.AddDate(0, -11, 0), Expires: time.Now().AddDate(0, 0, 12).Truncate(24 * time.Hour), Reference: secretRef("kv-ops-tools", "grafana-api-key"), }, "glsa_demo_expiring") diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index 1a79067..a372a5a 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -203,13 +203,11 @@ func parallel(n int, f func(i int)) { var wg sync.WaitGroup sem := make(chan struct{}, max(8, runtime.NumCPU())) for i := range n { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { sem <- struct{}{} defer func() { <-sem }() f(i) - }() + }) } wg.Wait() } diff --git a/internal/provider/value.go b/internal/provider/value.go index a794468..e3c693d 100644 --- a/internal/provider/value.go +++ b/internal/provider/value.go @@ -21,8 +21,6 @@ func NewSecretValue(b []byte) SecretValue { // Reveal returns the plaintext. Call it only to display or copy the value. func (v SecretValue) Reveal() string { return string(v.b) } -func (v SecretValue) Len() int { return len(v.b) } - func (v SecretValue) String() string { return masked } func (v SecretValue) GoString() string { return masked } diff --git a/internal/share/rules.go b/internal/share/rules.go index 9274cb1..77dd4fd 100644 --- a/internal/share/rules.go +++ b/internal/share/rules.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "unicode" + "unicode/utf8" ) // Rules are what a key's value must be to share with it: random enough @@ -40,7 +41,7 @@ func (r Rules) Check(key []byte) error { kinds++ } } - n := len([]rune(s)) + n := utf8.RuneCountInString(s) var has, needs []string if n < r.MinLength { has = append(has, fmt.Sprintf("%d characters", n)) diff --git a/internal/share/share.go b/internal/share/share.go index 8a65a66..0fad4a8 100644 --- a/internal/share/share.go +++ b/internal/share/share.go @@ -174,7 +174,7 @@ func Read(r ref.Ref) (Sealed, error) { return Sealed{}, errors.New("share link is from a newer lazysecrets: update it (lazysecrets update)") } n, size := binary.Uvarint(b[1:]) - if size <= 0 || n > uint64(len(b)) { + if size <= 0 { return Sealed{}, bad } rest := b[1+size:] diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index d9e01a9..7dbe147 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -3,6 +3,7 @@ package app import ( + "cmp" "context" "errors" "fmt" @@ -654,7 +655,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case detailsMsg: if msg.req == m.reqID[slotDetails] { - m.done(slotDetails) + m.cancelReq(slotDetails) } if errors.Is(msg.err, context.Canceled) || errors.Is(msg.err, context.DeadlineExceeded) { delete(m.details, msg.key) // tried again when shown again @@ -685,7 +686,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.req != m.reqID[slotContainers] { break } - m.done(slotContainers) + m.cancelReq(slotContainers) if msg.err != nil { m.noteErr(msg.err) if src, ok := m.selectedSource(); ok { @@ -703,7 +704,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.req != m.reqID[slotObjects] { break } - m.done(slotObjects) + m.cancelReq(slotObjects) if errors.Is(msg.err, provider.ErrUnsupported) && m.versionsOf != nil { m.flash = warning(fmt.Sprintf("versions aren't supported for %s", m.versionsOf.Container.Source.Kind)) m.closeVersions() @@ -733,7 +734,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.req != m.reqID[slotValue] { break } - m.done(slotValue) + m.cancelReq(slotValue) if msg.err != nil { m.noteErr(msg.err) m.logErrOf(msg.obj.Container.Name+"/"+msg.obj.Name, msg.err) @@ -748,7 +749,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.req != m.reqID[slotVersions] { break } - m.done(slotVersions) + m.cancelReq(slotVersions) // Errors and unsupported providers just leave the count out. if msg.err == nil { m.cache.objects[msg.key] = entry[[]provider.Object]{msg.items, m.now()} @@ -760,7 +761,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.req != m.reqID[slotDescribe] { break } - m.done(slotDescribe) + m.cancelReq(slotDescribe) if msg.err != nil { m.noteErr(msg.err) m.logErrOf(msg.obj.Container.Name+"/"+msg.obj.Name, msg.err) @@ -775,7 +776,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.req != m.reqID[slotCopy] { break } - m.done(slotCopy) + m.cancelReq(slotCopy) if msg.err != nil { m.noteErr(msg.err) m.flash = flash{text: "copy failed: " + msg.err.Error(), isErr: true} @@ -887,7 +888,7 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) (cmd tea.Cmd, quit bool) { case key.Matches(msg, k.Quit): return nil, true case key.Matches(msg, k.Help): - return m.showKeybindings(), false + return m.showKeybindingsFor(""), false case key.Matches(msg, k.Up): m.move(-1) case key.Matches(msg, k.Down): @@ -1167,14 +1168,11 @@ func (m *Model) syncSelection() tea.Cmd { if m.onLinks() { m.showLinkedContainers() } else if src, ok := m.selectedSource(); ok { - m.containers.Title = "[2] Vaults" m.containers.Empty = "no vaults found" switch src.Kind { case provider.KindDatabricks: - m.containers.Title = "[2] Scopes" m.containers.Empty = "no secret scopes" case provider.KindRepo: - m.containers.Title = "[2] Files" m.containers.Empty = "no SOPS or env files here (see docs/repos.md)" } cmds = append(cmds, m.showContainers(src, id)) @@ -1516,9 +1514,6 @@ func (m *Model) settled(ctx context.Context, load tea.Cmd) tea.Cmd { } } -// done releases the context of a completed request. -func (m *Model) done(p slot) { m.cancelReq(p) } - // loadSources starts a new generation of source loads, superseding any in // flight. func (m *Model) loadSources() tea.Cmd { @@ -1638,7 +1633,7 @@ func (m *Model) describe(o provider.Object) tea.Cmd { func (m *Model) copyValue() tea.Cmd { if m.focus == paneDetail && m.fields.Shown() { if f, ok := m.fields.Selected(); ok { - return m.copyText(fieldName(f), cmpOr(f.URL, f.Value), false) + return m.copyText(fieldName(f), cmp.Or(f.URL, f.Value), false) } return nil } diff --git a/internal/ui/app/app_test.go b/internal/ui/app/app_test.go index 39d19e9..48ee44a 100644 --- a/internal/ui/app/app_test.go +++ b/internal/ui/app/app_test.go @@ -991,11 +991,11 @@ func TestRebindingShowsEverywhere(t *testing.T) { if !strings.Contains(scr, "z to reveal") || strings.Contains(scr, "space to reveal") { t.Errorf("detail hint doesn't follow the binding:\n%s", scr) } - if !strings.Contains(lastLine(scr), "z show") { - t.Errorf("help bar: %q", lastLine(scr)) + if !strings.Contains(lastLines(scr, 1), "z show") { + t.Errorf("help bar: %q", lastLines(scr, 1)) } d.keys("z") - if bar := lastLine(d.screen()); !strings.Contains(bar, "z hide") { + if bar := lastLines(d.screen(), 1); !strings.Contains(bar, "z hide") { t.Errorf("help bar with the value shown: %q", bar) } d.keys("z") @@ -1125,7 +1125,7 @@ func TestAccountPane(t *testing.T) { if !strings.Contains(scr, "Alice Example") { t.Errorf("account not shown:\n%s", scr) } - bar := lastLine(scr) + bar := lastLines(scr, 1) if !strings.Contains(bar, "copy") || strings.Contains(bar, "reveal") || strings.Contains(bar, "versions") { t.Errorf("help bar: %q", bar) } diff --git a/internal/ui/app/cache.go b/internal/ui/app/cache.go index 69d6192..1d71fbc 100644 --- a/internal/ui/app/cache.go +++ b/internal/ui/app/cache.go @@ -66,7 +66,7 @@ func (m *Model) refreshShown(before []provider.Object) tea.Cmd { if i < 0 || sameListing(before[i], s) { return nil } - m.detail.Refresh(s) + m.detail.SetDescribed(s) uncache(m, m.cache.details, detailKey(s)) return m.describe(s) } diff --git a/internal/ui/app/commands.go b/internal/ui/app/commands.go index 08b0999..bfc727c 100644 --- a/internal/ui/app/commands.go +++ b/internal/ui/app/commands.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "context" "fmt" "os" @@ -84,10 +85,7 @@ func (m *Model) showCommands() tea.Cmd { } var opts []option for _, c := range cmds { - label := c.Description - if label == "" { - label = c.Command - } + label := cmp.Or(c.Description, c.Command) if c.Key != "" { label = m.st.Label.Render(fmt.Sprintf("%-8s", c.Key)) + label } @@ -122,7 +120,7 @@ func (m *Model) askPrompt(c commands.Command, it commands.Item, obj *provider.Ob in.SetValue(p.Default) in.CursorEnd() m.prompt = &modal{ - title: cmpOr(c.Description, "Command"), input: &in, + title: cmp.Or(c.Description, "Command"), input: &in, submit: func(m *Model, text string) tea.Cmd { it.Prompt[p.Name] = commands.Q(text) return m.askPrompt(c, it, obj, i+1) @@ -151,7 +149,7 @@ func (m *Model) confirmCommand(c commands.Command, it commands.Item, obj *provid case strings.HasPrefix(c.Value, "env:"): lines = append(lines, "", "It gets the value of "+string(it.Name)+" in $"+strings.TrimPrefix(c.Value, "env:")+".") } - m.showMenu("Run "+cmpOr(c.Description, "command")+"?", lines, []option{ + m.showMenu("Run "+cmp.Or(c.Description, "command")+"?", lines, []option{ {key: "y", label: "Run it", run: func(m *Model) tea.Cmd { return m.runCommand(c, rendered, obj) }}, }) return nil @@ -177,7 +175,7 @@ func (m *Model) runCommand(c commands.Command, rendered string, obj *provider.Ob // execCommand runs the command as its output mode says. func (m *Model) execCommand(c commands.Command, rendered string, value provider.SecretValue) tea.Cmd { - shell := cmpOr(os.Getenv("SHELL"), "/bin/sh") + shell := cmp.Or(os.Getenv("SHELL"), "/bin/sh") setValue := func(cmd *exec.Cmd) { switch { case c.Value == "stdin": @@ -195,7 +193,7 @@ func (m *Model) execCommand(c commands.Command, rendered string, value provider. m.flash = flash{} return tea.ExecProcess(cmd, func(err error) tea.Msg { return commandDoneMsg{cmd: c, err: err} }) } - m.flash = flash{text: "running " + cmpOr(c.Description, "command") + "…"} + m.flash = flash{text: "running " + cmp.Or(c.Description, "command") + "…"} return func() tea.Msg { ctx, cancel := context.WithTimeout(context.Background(), commandTimeout) defer cancel() @@ -219,7 +217,7 @@ func (m *Model) handleCommandValue(msg commandValueMsg) tea.Cmd { const maxOutputLines = 30 func (m *Model) handleCommandDone(msg commandDoneMsg) { - name := cmpOr(msg.cmd.Description, "command") + name := cmp.Or(msg.cmd.Description, "command") if msg.cmd.Out() != commands.Popup || m.prompt != nil || m.search != nil { switch { case msg.err != nil: @@ -298,11 +296,7 @@ func (m *Model) commandItem(ctx string) (commands.Item, *provider.Object, bool) if r, err := ref.ForObject(o, version); err == nil { it.Link = commands.Q(r.String()) } - if version != "" { - o.Version = version - } else { - o.Version = "" // the current value, like copying it - } + o.Version = version // "": the current value, like copying it return it, &o, true } @@ -312,10 +306,3 @@ func newTextInput(prompt string, width int) textinput.Model { in.SetWidth(width) return in } - -func cmpOr(a, b string) string { - if a != "" { - return a - } - return b -} diff --git a/internal/ui/app/commands_test.go b/internal/ui/app/commands_test.go index 2b18459..107fb47 100644 --- a/internal/ui/app/commands_test.go +++ b/internal/ui/app/commands_test.go @@ -8,6 +8,7 @@ import ( "time" "git.kebcloud.no/keb/lazysecrets/internal/commands" + "github.com/charmbracelet/x/ansi" ) func commandDriver(t *testing.T, cmds ...commands.Command) *driver { @@ -34,7 +35,7 @@ func TestCommandGetsValueOnInput(t *testing.T) { // temporary path's length. shown := "" if d.m.prompt != nil { - shown = ansiStrip(strings.Join(d.m.prompt.lines, "")) + shown = ansi.Strip(strings.Join(d.m.prompt.lines, "")) } if !strings.Contains(scr, "Run Save it?") || !strings.Contains(shown, "db-password 'team a'") || !strings.Contains(shown, "on its input") { t.Fatalf("no confirmation with the command %q:\n%s", shown, scr) diff --git a/internal/ui/app/edit.go b/internal/ui/app/edit.go index b3f9db9..aeca355 100644 --- a/internal/ui/app/edit.go +++ b/internal/ui/app/edit.go @@ -242,7 +242,7 @@ func (m *Model) openInEditor(f provider.File, finished func(error) tea.Msg) tea. return tea.QuitMsg{} } } - if editor := cmpOr(m.getenv("VISUAL"), m.getenv("EDITOR")); editor != "" { + if editor := cmp.Or(m.getenv("VISUAL"), m.getenv("EDITOR")); editor != "" { // Through the shell, so EDITOR can carry arguments ("code -w"). args := append([]string{"-c", editor + ` "$@"`, "lazysecrets"}, editorArgs(editor, f)...) m.flash = flash{} diff --git a/internal/ui/app/expiring.go b/internal/ui/app/expiring.go index acdc123..03dd93f 100644 --- a/internal/ui/app/expiring.go +++ b/internal/ui/app/expiring.go @@ -6,6 +6,7 @@ import ( "slices" "git.kebcloud.no/keb/lazysecrets/internal/cachefile" + "git.kebcloud.no/keb/lazysecrets/internal/expiry" "git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/ref" "git.kebcloud.no/keb/lazysecrets/internal/ui/panes" @@ -30,7 +31,7 @@ func (m Model) expiringItems() []expiringItem { for _, c := range m.cache.containers[row].value { for _, t := range c.ObjectTypes() { for _, o := range m.cache.objects[cachefile.ObjectsKey(c, t)].value { - if state, _ := m.expiry(o); state != notExpiring { + if state, _ := m.expiry(o); state != expiry.None { out = append(out, expiringItem{o, row}) } } @@ -51,11 +52,11 @@ func (m Model) expiringFields() []panes.Field { var out []panes.Field for _, it := range items { state, left := m.expiry(it.o) - mark := "⚠ " + daysLeft(left) - if state == expired { + mark := "⚠ " + expiry.Left(left) + if state == expiry.Expired { mark = "✗ expired" } - f := panes.Field{Value: mark + " " + it.o.Name + " · " + it.o.Container.Name, Err: state == expired, Row: it.row} + f := panes.Field{Value: mark + " " + it.o.Name + " · " + it.o.Container.Name, Err: state == expiry.Expired, Row: it.row} if r, err := ref.ForObject(it.o, ""); err == nil { f.Link = r.String() } diff --git a/internal/ui/app/expiry.go b/internal/ui/app/expiry.go index afc6d93..5adc67e 100644 --- a/internal/ui/app/expiry.go +++ b/internal/ui/app/expiry.go @@ -4,7 +4,6 @@ import ( "time" "git.kebcloud.no/keb/lazysecrets/internal/expiry" - "git.kebcloud.no/keb/lazysecrets/internal/provider" ) @@ -12,30 +11,19 @@ import ( // list once they have expired (✗ expired) or will within the configured // warning period (⚠ 12d), and typing "expir" in the filter finds them. -type expiryState = expiry.State - -const ( - notExpiring = expiry.None - expiresSoon = expiry.Soon - expired = expiry.Expired -) - // expiry tells whether o has expired or will within the warning period, // and how long it has left. -func (m Model) expiry(o provider.Object) (expiryState, time.Duration) { +func (m Model) expiry(o provider.Object) (expiry.State, time.Duration) { return expiry.Of(o.Expires, m.now(), m.expiryWarning) } // expiryWord is matched by the filter: "expired" or "expiring". func (m Model) expiryWord(o provider.Object) string { switch state, _ := m.expiry(o); state { - case expired: + case expiry.Expired: return "expired" - case expiresSoon: + case expiry.Soon: return "expiring" } return "" } - -// daysLeft is a short "12d", or "5h" on the last day. -func daysLeft(d time.Duration) string { return expiry.Left(d) } diff --git a/internal/ui/app/favourites.go b/internal/ui/app/favourites.go index 4932634..b96eaa6 100644 --- a/internal/ui/app/favourites.go +++ b/internal/ui/app/favourites.go @@ -8,6 +8,7 @@ import ( tea "charm.land/bubbletea/v2" "git.kebcloud.no/keb/lazysecrets/internal/cachefile" + "git.kebcloud.no/keb/lazysecrets/internal/expiry" "git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/ref" "git.kebcloud.no/keb/lazysecrets/internal/store" @@ -134,10 +135,10 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string { } line = marks(sty, n, o.Container.Source.Provider, m.recentIcon, m.now()) + line switch state, left := m.expiry(o); state { - case expired: + case expiry.Expired: line += " " + sty.Error.Render("✗ expired") - case expiresSoon: - line += " " + sty.Warning.Render("⚠ "+daysLeft(left)) + case expiry.Soon: + line += " " + sty.Warning.Render("⚠ "+expiry.Left(left)) } return withNote(sty, line, n.Note, width) } diff --git a/internal/ui/app/favourites_test.go b/internal/ui/app/favourites_test.go index 40ecbbb..e22fa8f 100644 --- a/internal/ui/app/favourites_test.go +++ b/internal/ui/app/favourites_test.go @@ -14,6 +14,7 @@ import ( "git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/provider/fake" "git.kebcloud.no/keb/lazysecrets/internal/store" + "github.com/charmbracelet/x/ansi" ) // jump opens the favourites list, filters it and jumps to the first match. @@ -429,7 +430,7 @@ func TestStarIsPerAccount(t *testing.T) { d.keys("F") var labels []string for _, o := range d.m.search.shown { - labels = append(labels, ansiStrip(o.label)) + labels = append(labels, ansi.Strip(o.label)) } if len(labels) != 1 || !strings.Contains(labels[0], "platform-prod · alice") { t.Errorf("favourites should list alice's star only: %q", labels) diff --git a/internal/ui/app/fields.go b/internal/ui/app/fields.go index 71b3610..6d65442 100644 --- a/internal/ui/app/fields.go +++ b/internal/ui/app/fields.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "errors" "fmt" "strings" @@ -61,7 +62,7 @@ func (m *Model) syncFields() tea.Cmd { case !ok: m.fields.Show("", nil) case m.selectedProblem() != "": - m.fields.Show(cmpOr(r.label, r.source.Name), m.problemFields(m.selectedProblem())) + m.fields.Show(cmp.Or(r.label, r.source.Name), m.problemFields(m.selectedProblem())) case r.kind == rowGroup: m.showAccount(r) case r.kind == rowSource: @@ -134,7 +135,7 @@ func (m Model) sourceFields(src provider.Source) []panes.Field { add("web", src.URL) if e, ok := m.cache.containers[sourceRowKey(src.Provider, src.ID)]; ok { label := map[provider.Kind]string{provider.KindDatabricks: "scopes", provider.KindRepo: "files"}[src.Kind] - add(cmpOr(label, "vaults"), fmt.Sprint(len(e.value))) + add(cmp.Or(label, "vaults"), fmt.Sprint(len(e.value))) } return out } @@ -158,7 +159,7 @@ func (m Model) containerFields(c provider.Container) []panes.Field { case provider.KindRepo: add("format", c.Detail) } - add("source", cmpOr(c.Source.ProviderLabel, string(c.Source.Kind))+" / "+c.Source.Name) + add("source", cmp.Or(c.Source.ProviderLabel, string(c.Source.Kind))+" / "+c.Source.Name) add("web", c.URL) for _, t := range c.ObjectTypes() { if e, ok := m.cache.objects[cachefile.ObjectsKey(c, t)]; ok { @@ -221,5 +222,5 @@ func (m Model) problemFields(problem string) []panes.Field { // fieldName names a copied field: its label, or a row of several under // one label (a tag), its value. func fieldName(f panes.Field) string { - return cmpOr(f.Label, f.Value) + return cmp.Or(f.Label, f.Value) } diff --git a/internal/ui/app/help.go b/internal/ui/app/help.go index 0dfa1ed..38594b0 100644 --- a/internal/ui/app/help.go +++ b/internal/ui/app/help.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "fmt" "slices" "strings" @@ -16,7 +17,7 @@ import ( ) // The help bar shows the keys that matter in the focused pane; ? lists all -// of them in a menu (showKeybindings). +// of them in a menu (showKeybindingsFor). // newHelp styles the help bar to read clearly: keys like labels elsewhere, // descriptions in the normal text colour, only the separators dimmed. The @@ -84,11 +85,6 @@ func (c contextHelp) FullHelp() [][]key.Binding { return [][]key.Binding{c.Short var paneNames = [paneCount]string{"Sources", "Vaults", "Secrets", "Detail", "Status"} -// showKeybindings opens the ? menu, like lazygit's: every binding with -// what it does, the focused pane's first, filterable by key, description -// and group. Enter runs the selected one. -func (m *Model) showKeybindings() tea.Cmd { return m.showKeybindingsFor("") } - // showKeybindingsFor opens the ? menu, like lazygit's: every binding with // what it does, filterable by key, description and group; enter runs the // selected one. Opened from the panes, it lists the focused pane's keys @@ -154,7 +150,7 @@ func (m *Model) showKeybindingsFor(ctx string) tea.Cmd { if cmds := m.commandsHere(); ctx == "" && len(cmds) > 0 { opts = append(opts, option{label: "Custom commands", header: true}) for _, c := range cmds { - label := m.st.Label.Render(fmt.Sprintf("%-14s", c.Key)) + fmt.Sprintf("%-26s", cmpOr(c.Description, "command")) + m.st.Muted.Render("custom") + label := m.st.Label.Render(fmt.Sprintf("%-14s", c.Key)) + fmt.Sprintf("%-26s", cmp.Or(c.Description, "command")) + m.st.Muted.Render("custom") opts = append(opts, option{label: label, tip: c.Command, run: func(m *Model) tea.Cmd { return m.startCommand(c) }}) } } diff --git a/internal/ui/app/layout_test.go b/internal/ui/app/layout_test.go index 5c6becb..aadcfe7 100644 --- a/internal/ui/app/layout_test.go +++ b/internal/ui/app/layout_test.go @@ -136,11 +136,11 @@ func TestLayoutIsSaved(t *testing.T) { func TestContextHelpBar(t *testing.T) { t.Parallel() d := newDriver(t) - if bar := lastLine(d.screen()); !strings.Contains(bar, "S share") || !strings.HasSuffix(strings.TrimSpace(bar), "? help • q quit") || strings.Contains(bar, "space show") { + if bar := lastLines(d.screen(), 1); !strings.Contains(bar, "S share") || !strings.HasSuffix(strings.TrimSpace(bar), "? help • q quit") || strings.Contains(bar, "space show") { t.Errorf("Sources help bar: %q", bar) } d.keys("3") - if bar := lastLine(d.screen()); !strings.Contains(bar, "s copy link") || !strings.Contains(bar, "S share") { + if bar := lastLines(d.screen(), 1); !strings.Contains(bar, "s copy link") || !strings.Contains(bar, "S share") { t.Errorf("Secrets help bar: %q", bar) } } @@ -225,11 +225,6 @@ func TestKeyPress(t *testing.T) { } } -func lastLine(s string) string { - lines := strings.Split(strings.TrimRight(s, "\n"), "\n") - return lines[len(lines)-1] -} - func TestNoHighlightWhileTerminalBlurred(t *testing.T) { t.Parallel() d := newDriver(t) @@ -264,7 +259,7 @@ func TestVersionInStatus(t *testing.T) { if !strings.Contains(d.screen(), "[0] lazysecrets v0.1.0") { t.Errorf("no version in the status pane:\n%s", d.screen()) } - if bar := lastLine(d.screen()); strings.Contains(bar, "v0.1.0") || !strings.Contains(bar, "? help • q quit") { + if bar := lastLines(d.screen(), 1); strings.Contains(bar, "v0.1.0") || !strings.Contains(bar, "? help • q quit") { t.Errorf("help line %q", bar) } } diff --git a/internal/ui/app/links.go b/internal/ui/app/links.go index 465e936..8ddc4f9 100644 --- a/internal/ui/app/links.go +++ b/internal/ui/app/links.go @@ -11,6 +11,7 @@ import ( "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" + "git.kebcloud.no/keb/lazysecrets/internal/cachefile" "git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/ref" "git.kebcloud.no/keb/lazysecrets/internal/ui/panes" @@ -433,7 +434,7 @@ func (m *Model) handleResolved(msg resolvedMsg) { // (ref.Ref.Account), if it's one of them. func preferredRow(r ref.Ref, choices []choice) (string, bool) { for _, c := range choices { - if r.Account != "" && providerOf(c.sourceRow) == r.Account { + if r.Account != "" && cachefile.ProviderOf(c.sourceRow) == r.Account { return c.sourceRow, true } } @@ -686,7 +687,7 @@ func (m *Model) share(menu bool) tea.Cmd { share := func(k string) func(*Model) tea.Cmd { return func(m *Model) tea.Cmd { return m.shareContainer(c, k) } } - opts := []option{option{key: "o", label: "Lazysecrets link", run: share("o")}} + opts := []option{{key: "o", label: "Lazysecrets link", run: share("o")}} if c.Source.Kind == provider.KindAzure && m.objType != provider.TypeSecret { opts = append(opts, option{key: "t", label: "Lazysecrets link to the " + tabLabels[m.objType] + " tab", run: share("t")}) } @@ -714,7 +715,7 @@ func (m *Model) share(menu bool) tea.Cmd { return func(m *Model) tea.Cmd { return m.shareObject(o, k) } } opts := []option{ - option{key: "o", label: "Lazysecrets link", run: share("o")}, + {key: "o", label: "Lazysecrets link", run: share("o")}, } if o.Container.Source.Kind == provider.KindAzure { opts = append(opts, option{key: "u", label: "Key Vault URL", run: share("u")}) diff --git a/internal/ui/app/notes_test.go b/internal/ui/app/notes_test.go index 1c3a850..40587e6 100644 --- a/internal/ui/app/notes_test.go +++ b/internal/ui/app/notes_test.go @@ -77,17 +77,15 @@ func TestNotesOnRows(t *testing.T) { func TestWithNoteGivesWay(t *testing.T) { t.Parallel() st := styles.Default() - if got := withNote(st, "db-password", "prod SQL admin", 30); len([]rune(stripANSI(got))) != 30 || - !strings.HasSuffix(stripANSI(got), "prod SQL admin") { - t.Errorf("fits: %q", stripANSI(got)) + if got := withNote(st, "db-password", "prod SQL admin", 30); len([]rune(ansi.Strip(got))) != 30 || + !strings.HasSuffix(ansi.Strip(got), "prod SQL admin") { + t.Errorf("fits: %q", ansi.Strip(got)) } - if got := stripANSI(withNote(st, "db-password", "prod SQL admin", 20)); !strings.HasPrefix(got, "db-password") || + if got := ansi.Strip(withNote(st, "db-password", "prod SQL admin", 20)); !strings.HasPrefix(got, "db-password") || !strings.HasSuffix(got, "…") { t.Errorf("tight: %q", got) } - if got := stripANSI(withNote(st, "a-very-long-secret-name", "note", 20)); got != "a-very-long-secret-name" { + if got := ansi.Strip(withNote(st, "a-very-long-secret-name", "note", 20)); got != "a-very-long-secret-name" { t.Errorf("no room: %q", got) } } - -func stripANSI(s string) string { return ansi.Strip(s) } diff --git a/internal/ui/app/repo_test.go b/internal/ui/app/repo_test.go index 248b799..08bf65a 100644 --- a/internal/ui/app/repo_test.go +++ b/internal/ui/app/repo_test.go @@ -16,6 +16,7 @@ import ( "git.kebcloud.no/keb/lazysecrets/internal/provider/repo" "git.kebcloud.no/keb/lazysecrets/internal/ref" "git.kebcloud.no/keb/lazysecrets/internal/store" + "github.com/charmbracelet/x/ansi" ) const sopsFile = `db: @@ -225,9 +226,9 @@ func TestSearchAllRepos(t *testing.T) { var got []string for _, o := range d.m.search.shown { if o.kind != provider.KindRepo { - t.Errorf("not a repo: %s", ansiStrip(o.label)) + t.Errorf("not a repo: %s", ansi.Strip(o.label)) } - got = append(got, ansiStrip(o.label)) + got = append(got, ansi.Strip(o.label)) } if len(got) != 2 { t.Errorf("want db.password in both repos: %v", got) diff --git a/internal/ui/app/search.go b/internal/ui/app/search.go index 7648853..296a976 100644 --- a/internal/ui/app/search.go +++ b/internal/ui/app/search.go @@ -6,7 +6,6 @@ import ( "fmt" "slices" "strings" - "time" tea "charm.land/bubbletea/v2" @@ -46,10 +45,6 @@ type crawlMsg struct { err error } -func contextWithTimeout(d time.Duration) (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), d) -} - // searchEntries lists everything loaded: the sources (with how much of // each is loaded) and the vaults, scopes and secrets in them, each with // its source's row key as group. @@ -67,7 +62,7 @@ func (m *Model) searchEntries() (sources, items []option) { sort := strings.ToLower(strings.Join(path, "/")) + " " + account es = append(es, entry{depth, sort, option{label: m.pathLabel(kind, path, account), note: n.Note, star: key, - starAccount: providerOf(row), starred: n.StarredFor(providerOf(row)), + starAccount: cachefile.ProviderOf(row), starred: n.StarredFor(cachefile.ProviderOf(row)), mark: mark, run: run, reload: reload, id: sort, group: row, kind: kind}}) return &es[len(es)-1].opt } @@ -134,8 +129,6 @@ func (c coverage) complete() bool { return c.sourcesListed == c.sources && c.vaultsLoaded == c.vaults } -func (m *Model) searchCoverage() coverage { return m.coverageOf(nil) } - // providerFilter picks the providers a search or a load covers; nil is // all of them. type providerFilter func(provider.Provider) bool @@ -148,7 +141,7 @@ func ofProvider(id string) providerFilter { return func(p provider.Provider) bool { return p.ID() == id } } -// coverageOf is searchCoverage for the providers in. +// coverageOf is the coverage of the providers in. func (m *Model) coverageOf(in providerFilter) coverage { var c coverage for _, p := range m.providers { @@ -183,7 +176,7 @@ func (m *Model) coverageOf(in providerFilter) coverage { // searchLines says what the search covers. func (m *Model) searchLines() []string { - c := m.searchCoverage() + c := m.coverageOf(nil) switch { case m.crawl.running: return []string{m.st.Notice.Render(fmt.Sprintf("Loading… %d lists left; results appear as they arrive.", @@ -210,11 +203,8 @@ func plural(n int, word string) string { return fmt.Sprintf("%d %ss", n, word) } -// startCrawl loads every unlisted subscription's vaults and every vault's -// secrets not loaded yet. -func (m *Model) startCrawl() tea.Cmd { return m.startCrawlOf(nil) } - -// startCrawlOf is startCrawl for the providers in. +// startCrawlOf loads, for the providers in, every unlisted subscription's +// vaults and every vault's secrets not loaded yet. func (m *Model) startCrawlOf(in providerFilter) tea.Cmd { for _, p := range m.providers { if in != nil && !in(p) { @@ -301,7 +291,7 @@ func (m *Model) crawlCmd(job crawlJob) tea.Cmd { src := *job.src timeout := m.timeout.of(src.Kind) return func() tea.Msg { - ctx, cancel := contextWithTimeout(timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() items, err := p.Containers(ctx, src) for i := range items { @@ -314,7 +304,7 @@ func (m *Model) crawlCmd(job crawlJob) tea.Cmd { p := m.providerFor(c.Source) timeout := m.timeout.of(c.Source.Kind) return func() tea.Msg { - ctx, cancel := contextWithTimeout(timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() items, err := p.Objects(ctx, c, provider.TypeSecret) for i := range items { @@ -396,9 +386,3 @@ func (m *Model) vaultMark(c provider.Container) string { } return " " } - -// providerOf is the provider ID in a source row key. -func providerOf(row string) string { - p, _ := splitSourceRow(row) - return p -} diff --git a/internal/ui/app/search_test.go b/internal/ui/app/search_test.go index 3246df4..cba8e98 100644 --- a/internal/ui/app/search_test.go +++ b/internal/ui/app/search_test.go @@ -6,6 +6,7 @@ import ( tea "charm.land/bubbletea/v2" + "git.kebcloud.no/keb/lazysecrets/internal/cachefile" "git.kebcloud.no/keb/lazysecrets/internal/provider/fake" "github.com/charmbracelet/x/ansi" ) @@ -115,19 +116,19 @@ func TestSearchSourceNarrowsAndLoads(t *testing.T) { t.Fatal("platform-dev not in sources") } } - if !strings.Contains(ansiStrip(d.m.search.scopes[d.m.search.src].mark), "○") { + if !strings.Contains(ansi.Strip(d.m.search.scopes[d.m.search.src].mark), "○") { t.Errorf("unloaded source mark %q", d.m.search.scopes[d.m.search.src].mark) } if len(d.m.search.shown) != 0 { t.Errorf("results not narrowed to platform-dev: %d", len(d.m.search.shown)) } d.send(ctrl('r')) - if !strings.Contains(ansiStrip(d.m.search.scopes[d.m.search.src].mark), "●") { + if !strings.Contains(ansi.Strip(d.m.search.scopes[d.m.search.src].mark), "●") { t.Errorf("loaded source mark %q", d.m.search.scopes[d.m.search.src].mark) } for _, o := range d.m.search.shown { - if !strings.Contains(ansiStrip(o.label), "platform-dev") { - t.Errorf("result from elsewhere: %q", ansiStrip(o.label)) + if !strings.Contains(ansi.Strip(o.label), "platform-dev") { + t.Errorf("result from elsewhere: %q", ansi.Strip(o.label)) } } if len(d.m.search.shown) == 0 { @@ -177,19 +178,17 @@ func TestStarFromSearch(t *testing.T) { } } -func ansiStrip(s string) string { return ansi.Strip(s) } - func TestSearchStartsWhereYouAre(t *testing.T) { t.Parallel() d := newDriver(t) d.keys("2", "j") // platform-prod / kv-shared-prod d.openSearch() v := d.m.search - scope := func() string { return ansiStrip(v.scopes[v.src].label) } + scope := func() string { return ansi.Strip(v.scopes[v.src].label) } if scope() != "All" { t.Errorf("opened on %q, want All", scope()) } - if v.res >= len(v.shown) || !strings.Contains(ansiStrip(v.shown[v.res].label), "kv-shared-prod") { + if v.res >= len(v.shown) || !strings.Contains(ansi.Strip(v.shown[v.res].label), "kv-shared-prod") { t.Errorf("vault not under the cursor:\n%s", d.screen()) } d.send(ctrl('s')) // narrows to the source, and back @@ -233,13 +232,13 @@ func TestSearchNarrowsToAccount(t *testing.T) { t.Errorf("no results in the account:\n%s", d.screen()) } for _, o := range v.shown { - if providerOf(o.group) != header.provider { - t.Errorf("result from another account: %q", ansiStrip(o.label)) + if cachefile.ProviderOf(o.group) != header.provider { + t.Errorf("result from another account: %q", ansi.Strip(o.label)) } } // The account's sources are under it, indented. - if next := v.scopes[v.src+1]; next.group == "" || providerOf(next.group) != header.provider || !strings.HasPrefix(ansiStrip(next.label), " ") { - t.Errorf("after the account: %q", ansiStrip(next.label)) + if next := v.scopes[v.src+1]; next.group == "" || cachefile.ProviderOf(next.group) != header.provider || !strings.HasPrefix(ansi.Strip(next.label), " ") { + t.Errorf("after the account: %q", ansi.Strip(next.label)) } } @@ -327,7 +326,7 @@ func TestSearchHighlightsMatches(t *testing.T) { t.Fatal("no results") } o := d.m.search.shown[0] - plain := []rune(ansiStrip(o.label)) + plain := []rune(ansi.Strip(o.label)) var got string for _, h := range o.hits { got += string(plain[h]) @@ -348,14 +347,14 @@ func TestHelpBarFollowsPopup(t *testing.T) { t.Parallel() d := newDriver(t) d.keys("F") - bar := lastLine(d.screen()) + bar := lastLines(d.screen(), 1) for _, want := range []string{"[ prev tab", "] next tab", "/ filter", "f star"} { if !strings.Contains(bar, want) { t.Errorf("browsing: help bar %q lacks %q", bar, want) } } d.keys("/") - if bar := lastLine(d.screen()); !strings.Contains(bar, "ctrl+f star") || !strings.Contains(bar, "esc stop typing") { + if bar := lastLines(d.screen(), 1); !strings.Contains(bar, "ctrl+f star") || !strings.Contains(bar, "esc stop typing") { t.Errorf("typing: help bar %q", bar) } } @@ -375,8 +374,8 @@ func TestHelpFromPopups(t *testing.T) { if o.header { sections = append(sections, o.label) } - if strings.Contains(ansiStrip(o.label), "copy value") { - t.Errorf("pane key listed in Go to's help: %q", ansiStrip(o.label)) + if strings.Contains(ansi.Strip(o.label), "copy value") { + t.Errorf("pane key listed in Go to's help: %q", ansi.Strip(o.label)) } } if strings.Join(sections, ",") != "Go to,While typing, after /" { @@ -428,7 +427,7 @@ func TestGoToHelpBarEndsWithHelp(t *testing.T) { t.Parallel() d := newDriver(t) // 100 columns: Go to's keys don't all fit d.keys("ctrl+s", "esc") - bar := strings.TrimSpace(lastLine(d.screen())) + bar := strings.TrimSpace(lastLines(d.screen(), 1)) if !strings.HasSuffix(bar, "? help") || !strings.Contains(bar, "…") { t.Errorf("help bar %q", bar) } @@ -443,11 +442,11 @@ func TestLoadWithEmptyResults(t *testing.T) { for d.m.search.src > 0 { d.send(tea.KeyPressMsg{Code: tea.KeyUp}) } - for i := 0; !strings.Contains(ansiStrip(d.m.search.scope().label), "platform-dev"); i++ { + for i := 0; !strings.Contains(ansi.Strip(d.m.search.scope().label), "platform-dev"); i++ { if i > len(d.m.search.scopes) { var labels []string for _, o := range d.m.search.scopes { - labels = append(labels, ansiStrip(o.label)) + labels = append(labels, ansi.Strip(o.label)) } t.Fatalf("no platform-dev scope: %q (at %d, results %v)", labels, d.m.search.src, d.m.search.results) } diff --git a/internal/ui/app/searchview.go b/internal/ui/app/searchview.go index 8f7b469..cfd2ee9 100644 --- a/internal/ui/app/searchview.go +++ b/internal/ui/app/searchview.go @@ -203,7 +203,7 @@ func (m *Model) fillSearch(keep bool) { id: accountScope + p.ID(), kind: k, mark: m.coverageMark(ofProvider(p.ID())), }) for _, o := range sources { - if providerOf(o.group) == p.ID() { + if cachefile.ProviderOf(o.group) == p.ID() { o.label = " " + o.label scopes = append(scopes, o) } @@ -267,7 +267,7 @@ func (v *searchView) match() []option { default: account, ofAccount := v.scopeAccount() for _, o := range v.items { - if sc.id == allSources || ofAccount && providerOf(o.group) == account || sc.group != "" && o.group == sc.group { + if sc.id == allSources || ofAccount && cachefile.ProviderOf(o.group) == account || sc.group != "" && o.group == sc.group { cand = append(cand, o) } } @@ -523,7 +523,7 @@ func (m *Model) searchLoads() tea.Cmd { return m.startCrawlOf(v.scopeFilter()) } if !v.onSource() { - return m.startCrawl() + return m.startCrawlOf(nil) } providerID, sourceID := splitSourceRow(sc.group) for _, src := range m.sourceResults[providerID].sources { diff --git a/internal/ui/app/status.go b/internal/ui/app/status.go index 829c4c7..e1586f7 100644 --- a/internal/ui/app/status.go +++ b/internal/ui/app/status.go @@ -1,6 +1,7 @@ package app import ( + "cmp" "errors" "fmt" "strings" @@ -83,7 +84,7 @@ func (m Model) statusFields() []panes.Field { } } // A release's rows open its release page. - version := panes.Field{Label: "version", Value: cmpOr(m.versionLabel(), "dev")} + version := panes.Field{Label: "version", Value: cmp.Or(m.versionLabel(), "dev")} if strings.HasPrefix(version.Value, "v") { version.URL = update.DefaultRepo + "/releases/tag/" + version.Value } @@ -159,7 +160,7 @@ func (m Model) accountStatus(p provider.Provider) string { return r.err.Error() } noun := map[provider.Kind]string{provider.KindAzure: "subscription", provider.KindDatabricks: "workspace", provider.KindRepo: "repo"}[p.Kind()] - return plural(len(r.sources), cmpOr(noun, "source")) + return plural(len(r.sources), cmp.Or(noun, "source")) } // ago is a duration as people say it: "just now", "5m ago", "2h ago". diff --git a/internal/ui/app/usage_test.go b/internal/ui/app/usage_test.go index dbe931f..9022822 100644 --- a/internal/ui/app/usage_test.go +++ b/internal/ui/app/usage_test.go @@ -5,6 +5,7 @@ import ( "testing" "git.kebcloud.no/keb/lazysecrets/internal/store" + "github.com/charmbracelet/x/ansi" ) func TestRecentScope(t *testing.T) { @@ -21,7 +22,7 @@ func TestRecentScope(t *testing.T) { d.keys("R") var recent []string for _, o := range d.m.search.shown { - recent = append(recent, ansiStrip(o.label)) + recent = append(recent, ansi.Strip(o.label)) } if len(recent) < 2 || !strings.Contains(recent[0], "legacy-token") || !strings.Contains(recent[1], "api-key") { t.Fatalf("want newest first: %q", recent) @@ -31,7 +32,7 @@ func TestRecentScope(t *testing.T) { t.Errorf("jumped to %q", s) } d.keys("F") - if len(d.m.search.shown) != 1 || !strings.Contains(ansiStrip(d.m.search.shown[0].label), "kv-shared-prod") { + if len(d.m.search.shown) != 1 || !strings.Contains(ansi.Strip(d.m.search.shown[0].label), "kv-shared-prod") { t.Errorf("favourites: %d entries\n%s", len(d.m.search.shown), d.screen()) } } diff --git a/internal/ui/keys/keys.go b/internal/ui/keys/keys.go index be1adad..0ec340f 100644 --- a/internal/ui/keys/keys.go +++ b/internal/ui/keys/keys.go @@ -15,8 +15,8 @@ package keys import ( "fmt" + "maps" "slices" - "sort" "strings" "charm.land/bubbles/v2/key" @@ -266,11 +266,11 @@ func (m *Map) Apply(cfg map[string]map[string][]string) error { byName[a.Context+"."+a.Name] = a contexts[a.Context] = true } - for _, ctx := range sortedKeys(cfg) { + for _, ctx := range slices.Sorted(maps.Keys(cfg)) { if !contexts[ctx] { - return fmt.Errorf("keybinding: unknown context %q (known: %s)", ctx, strings.Join(sortedKeys(contexts), ", ")) + return fmt.Errorf("keybinding: unknown context %q (known: %s)", ctx, strings.Join(slices.Sorted(maps.Keys(contexts)), ", ")) } - for _, name := range sortedKeys(cfg[ctx]) { + for _, name := range slices.Sorted(maps.Keys(cfg[ctx])) { a, ok := byName[ctx+"."+name] if !ok { return fmt.Errorf("keybinding.%s: unknown action %q (known: %s)", ctx, name, strings.Join(m.names(ctx), ", ")) @@ -289,16 +289,7 @@ func (m *Map) names(ctx string) []string { out = append(out, a.Name) } } - sort.Strings(out) - return out -} - -func sortedKeys[V any](set map[string]V) []string { - out := make([]string, 0, len(set)) - for k := range set { - out = append(out, k) - } - sort.Strings(out) + slices.Sort(out) return out } diff --git a/internal/ui/panes/detail.go b/internal/ui/panes/detail.go index 8d363c9..4800e26 100644 --- a/internal/ui/panes/detail.go +++ b/internal/ui/panes/detail.go @@ -60,16 +60,6 @@ func (d *Detail) ShowSecret(s provider.Object) { d.container = &s.Container } -// Refresh shows s's newer metadata if s is the secret shown, keeping -// its account and any value shown or loading. -func (d *Detail) Refresh(s provider.Object) { - if d.secret == nil || d.secret.ID != s.ID { - return - } - s.Container = d.secret.Container - d.secret = &s -} - // ShowContainer shows a container summary when no secret is selected. func (d *Detail) ShowContainer(c provider.Container) { d.Clear() diff --git a/internal/ui/panes/list.go b/internal/ui/panes/list.go index c1feb06..bfbb588 100644 --- a/internal/ui/panes/list.go +++ b/internal/ui/panes/list.go @@ -301,15 +301,6 @@ func matchRanges(plain string, hits []int, style lipgloss.Style) []lipgloss.Rang return out } -// RuneIndexes converts byte offsets in s to rune offsets. -func RuneIndexes(s string, bytes []int) []int { - out := make([]int, 0, len(bytes)) - for _, b := range bytes { - out = append(out, utf8.RuneCountInString(s[:b])) - } - return out -} - // highlight returns the rune indexes to highlight in a row's plain text: // the filter hits, shifted to where the Filter text appears in the row. // It returns nil if the row doesn't contain the Filter text verbatim. diff --git a/internal/ui/styles/theme.go b/internal/ui/styles/theme.go index 74b2193..e1aecc0 100644 --- a/internal/ui/styles/theme.go +++ b/internal/ui/styles/theme.go @@ -3,8 +3,8 @@ package styles import ( "fmt" "image/color" + "maps" "slices" - "sort" "strconv" "strings" @@ -51,12 +51,7 @@ var Roles = map[string]string{ func FromTheme(theme map[string][]string) (Styles, color.Color, error) { st := Default() var fg color.Color - roles := make([]string, 0, len(theme)) - for r := range theme { - roles = append(roles, r) - } - sort.Strings(roles) - for _, role := range roles { + for _, role := range slices.Sorted(maps.Keys(theme)) { if _, ok := Roles[role]; !ok { continue }