mirror of
https://github.com/anotherhadi/proton-vpn-tui.git
synced 2026-10-05 19:28:28 +02:00
init
This commit is contained in:
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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"`
|
||||
}
|
||||
Reference in New Issue
Block a user