This commit is contained in:
Hadi
2026-08-27 23:14:56 +02:00
commit e9c2a59ee5
41 changed files with 3680 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
package config
import (
_ "embed"
)
//go:embed default_config.yaml
var defaultConfig []byte
type Config struct {
Version string `mapstructure:"-"`
App struct {
ShowFlags bool `mapstructure:"show_flags"`
FocusSearchOnLaunch bool `mapstructure:"focus_search_on_launch"`
Default struct {
Tor bool `mapstructure:"tor"`
P2P bool `mapstructure:"p2p"`
SecureCore bool `mapstructure:"securecore"`
Free bool `mapstructure:"free"`
Prefer string `mapstructure:"prefer"`
} `mapstructure:"default"`
Favorites []string `mapstructure:"favorites"`
} `mapstructure:"app"`
Keybindings Keybindings `mapstructure:"keybindings"`
}
var Global *Config
+43
View File
@@ -0,0 +1,43 @@
app:
show_flags: true
focus_search_on_launch: false
default:
tor: false # Enable Tor over VPN
p2p: false # Only show P2P-optimized servers
securecore: false # Use an entry node and an exit node
free: false # Show only free servers
prefer: "fastest" # 'fastest' or 'random'
# Country code (e.g. "CH"), country name (e.g. "Switzerland"), or a
# specific server ID (e.g. "CH#20"). Case-insensitive.
favorites: []
keybindings:
global:
quit: "q,ctrl+c"
escape: "esc,ctrl+c"
help: "?"
up: "up,k"
down: "down,j"
left: "left,h"
right: "right,l"
goto_top: "g"
goto_bottom: "G,end"
home:
open_settings: "s"
connect: "enter,c"
disconnect: "d"
goto_favorite: "f"
search: "/,ctrl+f"
refresh: "r"
toggle_tor: "t"
toggle_securecore: "S"
toggle_p2p: "p"
toggle_free: "F"
toggle_prefer: "P"
settings:
toggle_setting: "space"
previous_option: "left,h"
next_option: "right,l"
+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, "proton-vpn-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)
}
}
}
+44
View File
@@ -0,0 +1,44 @@
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:"close" help:"none"`
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"`
Left key.Binding `mapstructure:"left" desc:"left" help:"full"`
Right key.Binding `mapstructure:"right" desc:"right" 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 HomeKeys struct {
OpenSettings key.Binding `mapstructure:"open_settings" desc:"settings" help:"full"`
Connect key.Binding `mapstructure:"connect" desc:"connect" help:"short"`
Disconnect key.Binding `mapstructure:"disconnect" desc:"disconnect" help:"short"`
GotoFavorite key.Binding `mapstructure:"goto_favorite" desc:"go to favorites" help:"full"`
Search key.Binding `mapstructure:"search" desc:"search" help:"short"`
Refresh key.Binding `mapstructure:"refresh" desc:"refresh" help:"full"`
ToggleTor key.Binding `mapstructure:"toggle_tor" desc:"toggle tor" help:"full"`
ToggleSecureCore key.Binding `mapstructure:"toggle_securecore" desc:"toggle secure core" help:"full"`
ToggleP2P key.Binding `mapstructure:"toggle_p2p" desc:"toggle p2p" help:"full"`
ToggleFree key.Binding `mapstructure:"toggle_free" desc:"toggle free" help:"full"`
TogglePrefer key.Binding `mapstructure:"toggle_prefer" desc:"fastest/random" help:"full"`
}
type SettingsKeys struct {
PreviousOption key.Binding `mapstructure:"previous_option" desc:"decrease" help:"full"`
NextOption key.Binding `mapstructure:"next_option" desc:"increase" help:"full"`
ToggleSetting key.Binding `mapstructure:"toggle_setting" desc:"toggle" help:"short"`
}
type Keybindings struct {
Global GlobalKeys `mapstructure:"global"`
Home HomeKeys `mapstructure:"home"`
Settings SettingsKeys `mapstructure:"settings"`
}