Signed-off-by: Hadi <[email protected]>
This commit is contained in:
Hadi
2026-08-27 00:05:08 +02:00
commit 624a723b16
29 changed files with 1989 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
package config
import (
_ "embed"
)
//go:embed default_config.yaml
var defaultConfig []byte
type Config struct {
Version string `mapstructure:"-"`
App struct {
Mode string `mapstructure:"mode"`
ApiURL string `mapstructure:"api_url"`
FocusSearchOnLaunch bool `mapstructure:"focus_search_on_launch"`
PrintOnEnter bool `mapstructure:"print_on_enter"`
} `mapstructure:"app"`
Keybindings Keybindings `mapstructure:"keybindings"`
}
var Global *Config
+36
View File
@@ -0,0 +1,36 @@
app:
# Where credentials come from: "auto", "online" or "local".
# - auto: use the local data directory if present, otherwise the API
# - online: always query the default-creds API
# - local: always read from the local data directory
mode: "auto"
# Base URL of the default-creds API (used in "online"/"auto" mode).
api_url: "https://default-creds.hadi.icu"
# Focus the search bar when the app starts.
focus_search_on_launch: true
# When true, pressing enter on a result prints it to stdout and exits
print_on_enter: false
keybindings:
global:
quit: "ctrl+c,q"
escape: "esc"
help: "?"
up: "up,k"
down: "down,j"
goto_top: "home,g"
goto_bottom: "end,G"
search:
focus: "/"
clear: "ctrl+u"
results:
copy_user: "u"
copy_pass: "p"
enter: "enter"
print_and_quit: "P"
refresh: "ctrl+r"
+78
View File
@@ -0,0 +1,78 @@
package config
import (
_ "embed"
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"reflect"
"github.com/go-viper/mapstructure/v2"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
func DefaultPath() string {
dir, err := os.UserConfigDir()
if err != nil {
dir = "."
}
return filepath.Join(dir, "default-creds-tui", "config.yaml")
}
func Load(path string) error {
var defaults map[string]any
if err := yaml.Unmarshal(defaultConfig, &defaults); err != nil {
return fmt.Errorf("default config: %w", err)
}
for k, v := range flatten("", defaults) {
viper.SetDefault(k, v)
}
viper.SetConfigType("yaml")
viper.SetConfigFile(path)
if err := viper.ReadInConfig(); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
}
Global = &Config{}
hook := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
stringToKeyBindingHook,
mapstructure.StringToTimeDurationHookFunc(),
))
if err := viper.Unmarshal(Global, hook); err != nil {
return err
}
fillHelp(reflect.ValueOf(&Global.Keybindings))
return nil
}
func WriteDefaultConfig(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return fmt.Errorf("create config dir: %w", err)
}
if err := os.WriteFile(path, defaultConfig, 0o600); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}
func flatten(prefix string, m map[string]any) map[string]any {
out := make(map[string]any)
for k, v := range m {
key := k
if prefix != "" {
key = prefix + "." + k
}
if nested, ok := v.(map[string]any); ok {
maps.Copy(out, flatten(key, nested))
} else {
out[key] = v
}
}
return out
}
+113
View File
@@ -0,0 +1,113 @@
package config
import (
"fmt"
"reflect"
"slices"
"strings"
"charm.land/bubbles/v2/key"
)
func ShortHelp(keymap any) []key.Binding {
return bindingsWithHelp(keymap, "short")
}
func pageFullHelp(keymap any) []key.Binding {
return bindingsWithHelp(keymap, "short", "full")
}
func bindingsWithHelp(keymap any, levels ...string) []key.Binding {
v := reflect.ValueOf(keymap)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
t := v.Type()
var out []key.Binding
for i := range v.NumField() {
field := t.Field(i)
if field.Type != bindingType {
continue
}
if slices.Contains(levels, field.Tag.Get("help")) {
out = append(out, v.Field(i).Interface().(key.Binding))
}
}
return out
}
type HelpKeyMap struct {
pages []any
}
func Help(pages ...any) HelpKeyMap {
return HelpKeyMap{pages: pages}
}
func (h HelpKeyMap) ShortHelp() []key.Binding {
var out []key.Binding
for _, p := range h.pages {
out = append(out, ShortHelp(p)...)
}
return out
}
func (h HelpKeyMap) FullHelp() [][]key.Binding {
cols := make([][]key.Binding, 0, len(h.pages))
for _, p := range h.pages {
if col := pageFullHelp(p); len(col) > 0 {
cols = append(cols, col)
}
}
return cols
}
var bindingType = reflect.TypeFor[key.Binding]()
var validHelpLevels = map[string]bool{"none": true, "short": true, "full": true}
func stringToKeyBindingHook(from reflect.Type, to reflect.Type, data any) (any, error) {
if to != bindingType || from.Kind() != reflect.String {
return data, nil
}
var keys []string
for part := range strings.SplitSeq(data.(string), ",") {
if k := strings.TrimSpace(part); k != "" {
keys = append(keys, k)
}
}
return key.NewBinding(key.WithKeys(keys...)), nil
}
func fillHelp(v reflect.Value) {
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return
}
t := v.Type()
for i := range t.NumField() {
field := t.Field(i)
fv := v.Field(i)
if field.Type == bindingType {
b := fv.Addr().Interface().(*key.Binding)
b.SetHelp(strings.Join(b.Keys(), ","), field.Tag.Get("desc"))
if level := field.Tag.Get("help"); !validHelpLevels[level] {
panic(fmt.Sprintf(
"config: %s.%s has invalid `help` tag %q, want \"none\", \"short\" or \"full\"",
t.Name(), field.Name, level,
))
}
continue
}
if field.Type.Kind() == reflect.Struct {
fillHelp(fv)
}
}
}
+34
View File
@@ -0,0 +1,34 @@
package config
import "charm.land/bubbles/v2/key"
type GlobalKeys struct {
Quit key.Binding `mapstructure:"quit" desc:"quit" help:"short"`
Escape key.Binding `mapstructure:"escape" desc:"back" help:"short"`
Help key.Binding `mapstructure:"help" desc:"help" help:"none"`
Up key.Binding `mapstructure:"up" desc:"up" help:"full"`
Down key.Binding `mapstructure:"down" desc:"down" help:"full"`
GotoTop key.Binding `mapstructure:"goto_top" desc:"go to top" help:"full"`
GotoBottom key.Binding `mapstructure:"goto_bottom" desc:"go to bottom" help:"full"`
}
type SearchKeys struct {
Focus key.Binding `mapstructure:"focus" desc:"search" help:"short"`
Clear key.Binding `mapstructure:"clear" desc:"clear search" help:"full"`
}
type ResultsKeys struct {
CopyUser key.Binding `mapstructure:"copy_user" desc:"copy user" help:"short"`
CopyPass key.Binding `mapstructure:"copy_pass" desc:"copy pass" help:"short"`
Enter key.Binding `mapstructure:"enter" desc:"select" help:"short"`
PrintAndQuit key.Binding `mapstructure:"print_and_quit" desc:"print and quit" help:"full"`
Refresh key.Binding `mapstructure:"refresh" desc:"refresh" help:"full"`
}
type Keybindings struct {
Global GlobalKeys `mapstructure:"global"`
Search SearchKeys `mapstructure:"search"`
Results ResultsKeys `mapstructure:"results"`
}