mirror of
https://github.com/anotherhadi/default-creds-tui.git
synced 2026-10-05 10:58:25 +02:00
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user