From f59b77f41929f8d28a7a420538dffdb28bc33d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Thu, 15 Oct 2026 16:39:12 +0200 Subject: [PATCH] feat: .NET appsettings files in repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appsettings.json and appsettings..json tracked by git are listed under Repos, keys named as .NET names them (ConnectionStrings:Default). - The secret patterns pick the keys, the rest hidden, with dotnet.secretPatterns for .NET files' own. - An App Service Key Vault reference, @Microsoft.KeyVault(…), is marked as a reference; a BOM at the start is fine. - A .NET icon, themeable with dotnetColor. --- README.md | 4 +- cmd/lazysecrets/list.go | 2 +- cmd/lazysecrets/main.go | 2 +- docs/config.md | 1 + docs/keys.md | 4 +- docs/repos.md | 24 +++++-- internal/config/config.go | 8 ++- internal/provider/repo/configfile.go | 4 +- internal/provider/repo/dotnet.go | 74 +++++++++++++++++++ internal/provider/repo/dotnet_test.go | 100 ++++++++++++++++++++++++++ internal/provider/repo/jasypt.go | 3 + internal/provider/repo/repo.go | 5 +- internal/ui/app/favourites.go | 6 +- internal/ui/icons/icons.go | 2 + internal/ui/keys/keys.go | 2 +- internal/ui/styles/styles.go | 2 + internal/ui/styles/theme.go | 3 + 17 files changed, 224 insertions(+), 22 deletions(-) create mode 100644 internal/provider/repo/dotnet.go create mode 100644 internal/provider/repo/dotnet_test.go diff --git a/README.md b/README.md index d67590d..60b7259 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A terminal UI for finding, reading and sharing secrets. Inspired by the fantasti - **Your way**: rebind any key, pick a theme, and add custom commands to run on a secret. - **Try it first**: `lazysecrets --demo` runs on built-in sample data. -- **Sources**: Azure Key Vault, Databricks secret scopes, and SOPS, `.env`, Spring Boot and Docker Compose files in your git repos. +- **Sources**: Azure Key Vault, Databricks secret scopes, and SOPS, `.env`, Spring Boot, Docker Compose and .NET appsettings files in your git repos. > **Early development:** expect rough edges. See [CHANGELOG.md](CHANGELOG.md) for what's new. @@ -87,7 +87,7 @@ pickers and scripts: see [Integration](docs/integration.md#an-fzf-picker). - [Keys](docs/keys.md): every key, Go to, the layout, and rebinding keys - [Azure Key Vault](docs/azure.md): accounts, permissions, pinned vaults - [Databricks](docs/databricks.md): profiles and Key Vault-backed scopes -- [SOPS, .env, Spring and compose files](docs/repos.md): which repos and files, decrypting +- [SOPS, .env, Spring, compose and .NET files](docs/repos.md): which repos and files, decrypting - [Sharing](docs/sharing.md): links to secrets, encrypted env files, and opening links, also from the browser and chat - [Integration](docs/integration.md): how lazysecrets can integrate into other systems diff --git a/cmd/lazysecrets/list.go b/cmd/lazysecrets/list.go index c590e3a..051c82a 100644 --- a/cmd/lazysecrets/list.go +++ b/cmd/lazysecrets/list.go @@ -57,7 +57,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t expiry := fs.Bool("expiry", true, "mark and say which have expired (✗ expired) or expire within ui.expiryWarningDays (⚠ expires in 12d) (text)") kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)") showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")") - hidden := fs.Bool("hidden", false, "list what's hidden too, as H does in the TUI: Spring and compose keys that don't look secret, example files, and files and repos with only those (json: \"hidden\")") + hidden := fs.Bool("hidden", false, "list what's hidden too, as H does in the TUI: config file keys (Spring, compose, .NET) that don't look secret, example files, and files and repos with only those (json: \"hidden\")") if err := fs.Parse(args); err != nil { return err } diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 22dcdfd..9a3d04e 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -347,7 +347,7 @@ Flags: repos := repo.New(repo.Settings{ Dir: startDir, Paths: pc.Repos.Paths, Roots: valueOr(pc.Repos.Roots, repo.DefaultRoots), Starred: starredRepos(st), Include: pc.Repos.Include, Exclude: pc.Repos.Exclude, - SecretPatterns: pc.Repos.SecretPatterns, Types: map[string]repo.FileType{"spring": pc.Repos.Spring, "compose": pc.Repos.Compose}, + SecretPatterns: pc.Repos.SecretPatterns, Types: map[string]repo.FileType{"spring": pc.Repos.Spring, "compose": pc.Repos.Compose, "dotnet": pc.Repos.Dotnet}, }) // The repo it started in (shown on top as Repo) and the // others, checked for secret files in the background like diff --git a/docs/config.md b/docs/config.md index d634117..bfc3a71 100644 --- a/docs/config.md +++ b/docs/config.md @@ -76,6 +76,7 @@ theme: repoColor: ["#c099ff"] springColor: ["#c3e88d"] # Spring Boot config files composeColor: ["#82aaff"] # Docker Compose files + dotnetColor: ["#c099ff"] # .NET appsettings files ``` A role lazysecrets doesn't know, a typo or one from a newer version, is diff --git a/docs/keys.md b/docs/keys.md index b9a4008..aeb4530 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -20,7 +20,7 @@ The rule: a letter in the lists, ctrl+ that letter where you type a filter | `[` / `]` | previous / next tab of the focused pane: Sources and Favourites in Sources; Secrets, Keys, Certs in Secrets | | `/` | filter the focused list by name or note ([how it matches](#searching)); `enter` keeps it, `esc` clears it. While typing: `ctrl+j`/`ctrl+k` move, `ctrl+y` copies, `ctrl+r` reveals, `ctrl+f` stars | | `` ` `` | switch the Sources pane between grouped by provider and a flat list; `enter`/`space` on a provider folds it | -| `H` | show / hide what's hidden: [Spring and compose keys](repos.md#spring-boot-config) that don't look secret, example files, and files and repos with only those; shown dimmed | +| `H` | show / hide what's hidden: [config file keys](repos.md#spring-boot-config) (Spring, compose, .NET) that don't look secret, example files, and files and repos with only those; shown dimmed | | `r` | refresh the focused pane | | `q`, `ctrl+c` | quit | @@ -54,7 +54,7 @@ match case exactly. | `Y` | copy the reference (`@Microsoft.KeyVault(...)`, key/certificate URL, `{{secrets/scope/key}}`) | | `v` | versions of the selected item (Azure); `space`/`y`/`Y` then act on that version; `esc`, `⌫` or `v` to go back | | `o` | open the selected item in the browser (over SSH: copy the link) | -| `e` | open the selected SOPS, `.env`, Spring or compose file, or a secret's file at its line ([how](repos.md#opening-files)) | +| `e` | open the selected SOPS, `.env`, Spring, compose or .NET file, or a secret's file at its line ([how](repos.md#opening-files)) | | `s` / `S` | copy a [link](sharing.md) to the selected item / share menu (also env files, [encrypted](sharing.md#sharing-env-files)) | | `ctrl+o` | open a link or reference (or just paste one) | | `x` | [custom commands](custom-commands.md) | diff --git a/docs/repos.md b/docs/repos.md index 0bd0dc9..64d81f4 100644 --- a/docs/repos.md +++ b/docs/repos.md @@ -1,9 +1,10 @@ -# SOPS, .env, Spring and compose files in git repos +# SOPS, .env, Spring, compose and .NET files in git repos lazysecrets shows git repositories under **Repos**: their [SOPS](https://github.com/getsops/sops)-encrypted files (YAML, JSON, `.env`, INI), plain env files, [Spring Boot config](#spring-boot-config) -and [Docker Compose files](#docker-compose) as vaults, and the keys in them (`db.password`, `list[0]`) as secrets. A dot or bracket inside a key is +[Docker Compose files](#docker-compose) and [.NET appsettings](#net-appsettings) +as vaults, and the keys in them (`db.password`, `list[0]`) as secrets. A dot or bracket inside a key is escaped, `app\.name`, so it doesn't read as nesting. ## Which repos @@ -47,7 +48,7 @@ Folders of dependencies and caches are never looked in, tracked or not: 3. where the repo's `.sops.yaml` `creation_rules` say SOPS files go, 4. the whole repo. -`include`/`exclude` apply to env, Spring and compose files too. +`include`/`exclude` apply to env, Spring, compose and .NET files too. ```yaml providers: @@ -106,8 +107,8 @@ providers: ``` A repo's `.lazysecrets.yaml` can set them too, the same way -(`secretPatterns:`, `spring: secretPatterns:`, `compose: -secretPatterns:`), for everyone. Each list +(`secretPatterns:`, `spring:`, `compose:` or `dotnet: secretPatterns:`), +for everyone. Each list comes from the first that sets it: the repo's for the type, the repo's, the config's for the type, the config's, the defaults. @@ -133,6 +134,17 @@ without a value (`DB_PASSWORD:`, `- DB_PASSWORD`), taken from where compose runs, are marked as references; `${DB_PASSWORD:-x}` has its default in the file. +## .NET appsettings + +`appsettings.json` and `appsettings..json`, tracked by git. +Keys are named as .NET's configuration names them, sections joined by +colons: `ConnectionStrings:Default`, `Servers:0:Name`. The same +[secret patterns](#spring-boot-config) pick them, the rest hidden, with +`dotnet: secretPatterns:` for .NET files' own; a connection string with +`Password=` is listed by its value. An App Service Key Vault reference, +`@Microsoft.KeyVault(…)`, is marked as a reference. Jasypt values decrypt +with `$JASYPT_ENCRYPTOR_PASSWORD` only. + ## Decrypting Listing never decrypts: SOPS and Jasypt encrypt values only, so keys and @@ -151,7 +163,7 @@ repo's history, or easily could be. ### Jasypt -A value like `ENC(...)` in a plain `.env`, Spring or compose file is +A value like `ENC(...)` in a plain `.env`, Spring, compose or .NET file is [Jasypt](http://www.jasypt.org)'s, decrypted when it's revealed or copied with jasypt-spring-boot's default algorithm (3.0+: `PBEWITHHMACSHA512ANDAES_256`). The password is, as in Spring: diff --git a/internal/config/config.go b/internal/config/config.go index 078cabf..fa6bbe9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -154,11 +154,12 @@ type Repos struct { // a repo's .sops.yaml creation_rules say where to look. Include []string `yaml:"include"` Exclude []string `yaml:"exclude"` - // SecretPatterns say which keys of config files are secrets; Spring - // and Compose override them for their files, list by list. + // SecretPatterns say which keys of config files are secrets; Spring, + // Compose and Dotnet override them for their files, list by list. SecretPatterns repo.SecretPatterns `yaml:"secretPatterns"` Spring repo.FileType `yaml:"spring"` Compose repo.FileType `yaml:"compose"` + Dotnet repo.FileType `yaml:"dotnet"` } type Azure struct { @@ -300,7 +301,8 @@ func (c Config) validate(path string) error { } if err := errors.Join(c.Providers.Repos.SecretPatterns.Check("providers.repos.secretPatterns"), c.Providers.Repos.Spring.SecretPatterns.Check("providers.repos.spring.secretPatterns"), - c.Providers.Repos.Compose.SecretPatterns.Check("providers.repos.compose.secretPatterns")); err != nil { + c.Providers.Repos.Compose.SecretPatterns.Check("providers.repos.compose.secretPatterns"), + c.Providers.Repos.Dotnet.SecretPatterns.Check("providers.repos.dotnet.secretPatterns")); err != nil { return fmt.Errorf("%s: %w", path, err) } for i, v := range c.Providers.Azure.Vaults { diff --git a/internal/provider/repo/configfile.go b/internal/provider/repo/configfile.go index d594711..0594a1d 100644 --- a/internal/provider/repo/configfile.go +++ b/internal/provider/repo/configfile.go @@ -22,14 +22,14 @@ type configType struct { format func(file string) string // yaml, properties parse func(file string, b []byte) ([]configKey, error) // jasyptSetting is the key that holds the Jasypt password for the - // key named name. + // key named name; nil when only $JASYPT_ENCRYPTOR_PASSWORD can. jasyptSetting func(name string) string // placeholder reads a value that only refers to another, // ${DB_PASSWORD}: what it names, and its default if it has one. placeholder func(v string) (name, def string, hasDef, ok bool) } -var configTypes = []configType{springType, composeType} +var configTypes = []configType{springType, composeType, dotnetType} func isConfig(f string) bool { _, ok := configTypeOf(f) diff --git a/internal/provider/repo/dotnet.go b/internal/provider/repo/dotnet.go new file mode 100644 index 0000000..d8de7fc --- /dev/null +++ b/internal/provider/repo/dotnet.go @@ -0,0 +1,74 @@ +package repo + +import ( + "fmt" + "path" + "regexp" + "strings" + + "go.yaml.in/yaml/v3" +) + +// .NET config files: appsettings.json and appsettings..json. +// Keys are named as .NET's configuration names them, sections joined by +// colons: ConnectionStrings:Default, Servers:0:Name. + +var dotnetType = configType{ + name: "dotnet", is: IsDotnet, format: func(string) string { return "json" }, parse: parseDotnet, + placeholder: dotnetPlaceholder, +} + +var dotnetName = regexp.MustCompile(`^appsettings(\.[^/]+)?\.json$`) + +// IsDotnet reports whether f is named like a .NET appsettings file. +func IsDotnet(f string) bool { + return dotnetName.MatchString(strings.ToLower(path.Base(f))) +} + +// dotnetPlaceholder reads an App Service Key Vault reference, +// @Microsoft.KeyVault(SecretUri=…): the secret is in the vault. +func dotnetPlaceholder(v string) (name, def string, hasDef, ok bool) { + if strings.HasPrefix(v, "@Microsoft.KeyVault(") && strings.HasSuffix(v, ")") { + return v, "", false, true + } + return "", "", false, false +} + +func parseDotnet(_ string, b []byte) ([]configKey, error) { + var doc yaml.Node // JSON is YAML, and the YAML parser says where each key is + if err := yaml.Unmarshal(b, &doc); err != nil { + return nil, err + } + var out []configKey + if len(doc.Content) > 0 { + dotnetWalk(doc.Content[0], "", &out) + } + for i := range out { + out[i].name = out[i].key + out[i].ref = reference(dotnetPlaceholder, out[i].value) + } + return out, nil +} + +func dotnetWalk(n *yaml.Node, name string, out *[]configKey) { + join := func(child string) string { + if name == "" { + return child + } + return name + ":" + child + } + switch n.Kind { + case yaml.MappingNode: + for i := 0; i+1 < len(n.Content); i += 2 { + dotnetWalk(n.Content[i+1], join(n.Content[i].Value), out) + } + case yaml.SequenceNode: + for i, c := range n.Content { + dotnetWalk(c, join(fmt.Sprint(i)), out) + } + case yaml.ScalarNode: + if name != "" { + *out = append(*out, configKey{key: name, value: n.Value, line: n.Line}) + } + } +} diff --git a/internal/provider/repo/dotnet_test.go b/internal/provider/repo/dotnet_test.go new file mode 100644 index 0000000..0723989 --- /dev/null +++ b/internal/provider/repo/dotnet_test.go @@ -0,0 +1,100 @@ +package repo + +import ( + "context" + "maps" + "testing" + + "git.kebcloud.no/keb/lazysecrets/internal/provider" +) + +func TestIsDotnet(t *testing.T) { + for f, want := range map[string]bool{ + "appsettings.json": true, + "src/Api/appsettings.Production.json": true, + "appsettings.Example.json": true, // listed hidden + "appsettings.json.bak": false, + "settings.json": false, + "package.json": false, + } { + if IsDotnet(f) != want { + t.Errorf("%s: %t", f, !want) + } + } +} + +func TestParseDotnet(t *testing.T) { + keys, err := parseDotnet("", []byte("\xef\xbb\xbf"+`{ + "ConnectionStrings": { "Default": "Server=db;Password=hunter2" }, + "AzureAd": { "ClientId": "abc", "ClientSecret": "@Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/ad)" }, + "Servers": [ { "Name": "a", "ApiKey": "k" } ], + "Logging": { "LogLevel": { "Default": "Information" } } +}`)) + if err != nil { + t.Fatal(err) + } + got := map[string]string{} + for _, k := range keys { + ref := "" + if k.ref { + ref = " (ref)" + } + got[k.name] = k.value + ref + } + want := map[string]string{ + "ConnectionStrings:Default": "Server=db;Password=hunter2", + "AzureAd:ClientId": "abc", + "AzureAd:ClientSecret": "@Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/ad) (ref)", + "Servers:0:Name": "a", + "Servers:0:ApiKey": "k", + "Logging:LogLevel:Default": "Information", + } + if !maps.Equal(got, want) { + t.Errorf("got %v\nwant %v", got, want) + } +} + +func TestDotnetFiles(t *testing.T) { + root := gitRepo(t, map[string]string{ + "src/Api/appsettings.json": `{ + "Logging": { "LogLevel": { "Default": "Information" } }, + "ConnectionStrings": { "Default": "Server=db;User Id=app;Password=hunter2" }, + "AzureAd": { "ClientSecret": "@Microsoft.KeyVault(VaultName=kv;SecretName=ad)" } +}`, + "src/Api/appsettings.Development.json": `{ "Logging": { "LogLevel": { "Default": "Debug" } } }`, + }) + t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "") + ctx := context.Background() + p := New(Settings{Dir: root}) + srcs, _ := p.Sources(ctx) + cs, err := p.Containers(ctx, srcs[0]) + if err != nil || len(cs) != 2 { + t.Fatalf("files: %+v %v", cs, err) + } + for _, c := range cs { + if c.Detail != "dotnet · json" || c.Hidden != (c.Name == "src/Api/appsettings.Development.json") { + t.Errorf("%s: %q hidden %t", c.Name, c.Detail, c.Hidden) + } + } + settings := cs[1] + + keys, _ := p.Objects(ctx, settings, provider.TypeSecret) + got := map[string]string{} + for _, k := range shown(keys) { + got[k.Name] = k.Detail + } + // The connection string by its value, the client secret by its name. + if want := map[string]string{"ConnectionStrings:Default": "", "AzureAd:ClientSecret": "reference"}; !maps.Equal(got, want) { + t.Errorf("keys %v, want %v", got, want) + } + link := provider.Object{Container: provider.Container{Source: srcs[0], Name: "src/Api/appsettings.json"}, Name: "ConnectionStrings:Default"} + if v, err := p.Value(ctx, link); err != nil || v.Reveal() != "Server=db;User Id=app;Password=hunter2" { + t.Errorf("value: %q %v", v.Reveal(), err) + } + if f, _ := p.File(settings, &link); f.Line != 3 { + t.Errorf("line %d", f.Line) + } + if d, plain := p.configDetails(dotnetType, settings); !plain || d[1].Value != "1, not matched by the secret patterns" { + t.Errorf("details: %+v %t", d, plain) + } +} diff --git a/internal/provider/repo/jasypt.go b/internal/provider/repo/jasypt.go index c13b5f3..15655dd 100644 --- a/internal/provider/repo/jasypt.go +++ b/internal/provider/repo/jasypt.go @@ -46,6 +46,9 @@ func jasyptPassword(file, name string) (password, from string, err error) { } setting, read, placeholder := jasyptEnv, plainValue, composePlaceholder // env files interpolate like compose if t, ok := configTypeOf(file); ok { + if t.jasyptSetting == nil { + return "", "", fmt.Errorf("jasypt: no password: set $%s", jasyptEnv) + } setting, read, placeholder = t.jasyptSetting(name), t.value, t.placeholder } v, err := read(file, setting) diff --git a/internal/provider/repo/repo.go b/internal/provider/repo/repo.go index 8537997..7c6d8ba 100644 --- a/internal/provider/repo/repo.go +++ b/internal/provider/repo/repo.go @@ -1,7 +1,7 @@ // Package repo browses the encrypted secret files in git repositories: // a repo is a source, its secret files the containers (vault level), and // their keys the secrets. Secret files are SOPS files, plain env files and -// config files: Spring Boot's and Docker Compose's (configfile.go). +// config files: Spring Boot's, Docker Compose's and .NET's (configfile.go). // // Listing never decrypts: SOPS encrypts values only, so a file's keys and // metadata are read directly. A value is decrypted when it's revealed or @@ -640,6 +640,7 @@ type lazysecretsFile struct { SecretPatterns SecretPatterns `yaml:"secretPatterns"` Spring FileType `yaml:"spring"` Compose FileType `yaml:"compose"` + Dotnet FileType `yaml:"dotnet"` Sharing struct { // Key is the link to the secret that encrypts the repo's // shared env files (package share). @@ -648,7 +649,7 @@ type lazysecretsFile struct { } func (f lazysecretsFile) types() map[string]FileType { - return map[string]FileType{"spring": f.Spring, "compose": f.Compose} + return map[string]FileType{"spring": f.Spring, "compose": f.Compose, "dotnet": f.Dotnet} } // ShareKey is the sharing.key of the repo's .lazysecrets.yaml, if any. diff --git a/internal/ui/app/favourites.go b/internal/ui/app/favourites.go index 3804d54..2876467 100644 --- a/internal/ui/app/favourites.go +++ b/internal/ui/app/favourites.go @@ -145,8 +145,8 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string { } // fileIcon is a repo file's icon, in its colour: a lock for an encrypted -// one, a leaf for Spring config, a whale for compose, a file in the -// warning colour for plain text; "" for other vaults, or without icons. +// one, a leaf for Spring config, a whale for compose, .NET's for +// appsettings, a file in the warning colour for plain text; "" for other vaults, or without icons. func (m Model) fileIcon(sty styles.Styles, c provider.Container) string { if c.Source.Kind != provider.KindRepo { return "" @@ -157,6 +157,8 @@ func (m Model) fileIcon(sty styles.Styles, c provider.Container) string { icon, style = m.icons.Spring, sty.Spring case strings.HasPrefix(c.Detail, "compose"): icon, style = m.icons.Compose, sty.Compose + case strings.HasPrefix(c.Detail, "dotnet"): + icon, style = m.icons.Dotnet, sty.Dotnet case !strings.HasPrefix(c.Detail, "sops"): icon, style = m.icons.PlainFile, sty.Warning } diff --git a/internal/ui/icons/icons.go b/internal/ui/icons/icons.go index b406de1..7412ee8 100644 --- a/internal/ui/icons/icons.go +++ b/internal/ui/icons/icons.go @@ -13,6 +13,7 @@ type Set struct { PlainFile string // a plain text secrets file (.env) Spring string // a Spring Boot config file Compose string // a Docker Compose file + Dotnet string // a .NET appsettings file Note string // has a note (lazysecrets list) } @@ -29,6 +30,7 @@ var NerdFont = Set{ PlainFile: "\U000F0214", // md-file Spring: "\U000F032A", // md-leaf Compose: "\U000F0868", // md-docker + Dotnet: "\U000F0AAE", // md-dot_net Note: "\U000F09ED", } diff --git a/internal/ui/keys/keys.go b/internal/ui/keys/keys.go index 55b0f39..d1cd416 100644 --- a/internal/ui/keys/keys.go +++ b/internal/ui/keys/keys.go @@ -103,7 +103,7 @@ func (m *Map) Actions() []Action { a(Lists, "prevTab", &m.PrevTab, "prev tab", "lists", "Previous tab of the focused pane: Sources and Favourites in Sources; Secrets, Keys, Certs in Secrets."), a(Lists, "nextTab", &m.NextTab, "next tab", "lists", "Next tab of the focused pane."), a(Lists, "sourcesView", &m.SourcesView, "grouped/flat sources", "lists", "Switch the Sources pane between grouped by provider and a flat list."), - a(Lists, "toggleHidden", &m.ToggleHidden, "show/hide hidden", "lists", "Show or hide what's hidden: Spring and compose keys that don't look like secrets, example files (.env.example), and files and repos with only those. Shown, they're dimmed; Go to finds them too."), + a(Lists, "toggleHidden", &m.ToggleHidden, "show/hide hidden", "lists", "Show or hide what's hidden: config file keys (Spring, compose, .NET) that don't look like secrets, example files (.env.example), and files and repos with only those. Shown, they're dimmed; Go to finds them too."), a(Lists, "star", &m.Favourite, "star", "lists", "Star or unstar the selected subscription, vault or secret, for the account showing it. Starred items get a ★ and sort first."), a(Lists, "goTo", &m.Search, "search", "lists", "Go to anything: search every vault and secret loaded and jump to one. Again in Go to: only the source or group you're in, and back to all. On the left, the scope: All, an account (or Databricks, or Repos), or one source (○ ◐ ● for how much of it is loaded)."), a(Lists, "favourites", &m.Favourites, "favourites", "lists", "Go to, on Favourites: everything you've starred, to search and jump to."), diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index a95c121..f3066c0 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -36,6 +36,7 @@ type Styles struct { KindRepo lipgloss.Style Spring lipgloss.Style // Spring Boot config files Compose lipgloss.Style // Docker Compose files + Dotnet lipgloss.Style // .NET appsettings files // themed marks the selection colours as set by the theme, so // OnBackground leaves them. @@ -74,6 +75,7 @@ func Default() Styles { KindRepo: lipgloss.NewStyle().Foreground(lipgloss.Magenta), Spring: lipgloss.NewStyle().Foreground(lipgloss.Green), Compose: lipgloss.NewStyle().Foreground(lipgloss.Blue), + Dotnet: lipgloss.NewStyle().Foreground(lipgloss.BrightMagenta), } } diff --git a/internal/ui/styles/theme.go b/internal/ui/styles/theme.go index bb136ac..2d8d8f3 100644 --- a/internal/ui/styles/theme.go +++ b/internal/ui/styles/theme.go @@ -44,6 +44,7 @@ var Roles = map[string]string{ "repoColor": "repos and their secret files (SOPS)", "springColor": "Spring Boot config files", "composeColor": "Docker Compose files", + "dotnetColor": ".NET appsettings files", } // FromTheme returns the default styles with the theme's roles applied, and @@ -105,6 +106,8 @@ func FromTheme(theme map[string][]string) (Styles, color.Color, error) { st.Spring = v.fg(lipgloss.NewStyle()) case "composeColor": st.Compose = v.fg(lipgloss.NewStyle()) + case "dotnetColor": + st.Dotnet = v.fg(lipgloss.NewStyle()) } } return st, fg, nil