Files
diple/config.go
2026-07-28 09:52:23 +02:00

137 lines
3.7 KiB
Go

package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/BurntSushi/toml"
)
type configDuration struct {
time.Duration
}
func (d *configDuration) UnmarshalText(text []byte) error {
value, err := time.ParseDuration(string(text))
if err != nil {
return err
}
d.Duration = value
return nil
}
type Config struct {
Theme string `toml:"theme"`
RefreshInterval configDuration `toml:"refresh_interval"`
Repository string `toml:"repository"`
ShowAll bool `toml:"show_all"`
Limit int `toml:"limit"`
Endpoint string `toml:"endpoint"`
Display DisplayConfig `toml:"display"`
Paths PathConfig `toml:"paths"`
}
type DisplayConfig struct {
FoldResolved bool `toml:"fold_resolved"`
ThreadListWidthPercent int `toml:"thread_list_width_percent"`
}
type PathConfig struct {
Scroll bool `toml:"scroll"`
ScrollInterval configDuration `toml:"scroll_interval"`
}
func defaultConfig() Config {
return Config{
Theme: "dark",
RefreshInterval: configDuration{10 * time.Second},
Limit: 50,
Endpoint: "https://api.github.com/graphql",
Display: DisplayConfig{
FoldResolved: true,
ThreadListWidthPercent: 33,
},
Paths: PathConfig{
Scroll: false,
ScrollInterval: configDuration{350 * time.Millisecond},
},
}
}
func configPath() (string, error) {
if path := os.Getenv("GH_THREADS_CONFIG"); path != "" {
return path, nil
}
if base := os.Getenv("XDG_CONFIG_HOME"); base != "" {
return filepath.Join(base, "gh-threads", "config.toml"), nil
}
base, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("find user config directory: %w", err)
}
preferred := filepath.Join(base, "gh-threads", "config.toml")
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("find home directory: %w", err)
}
fallback := filepath.Join(home, ".config", "gh-threads", "config.toml")
return existingConfigPath(preferred, fallback), nil
}
func existingConfigPath(preferred, fallback string) string {
if _, err := os.Stat(preferred); err == nil || !errors.Is(err, os.ErrNotExist) {
return preferred
}
if _, err := os.Stat(fallback); err == nil {
return fallback
}
return preferred
}
func loadConfig(path string, required bool) (Config, error) {
config := defaultConfig()
metadata, err := toml.DecodeFile(path, &config)
if err != nil {
if !required && errors.Is(err, os.ErrNotExist) {
return config, nil
}
return Config{}, fmt.Errorf("read config %s: %w", path, err)
}
if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
keys := make([]string, len(undecoded))
for i, key := range undecoded {
keys[i] = key.String()
}
return Config{}, fmt.Errorf("config %s contains unknown settings: %s", path, strings.Join(keys, ", "))
}
return config, nil
}
func validateConfig(config Config) error {
switch config.Theme {
case "dark", "light":
default:
return fmt.Errorf("theme must be dark or light")
}
if config.RefreshInterval.Duration < 2*time.Second {
return fmt.Errorf("refresh_interval must be at least 2s")
}
if config.Limit < 1 || config.Limit > 100 {
return fmt.Errorf("limit must be between 1 and 100")
}
if config.Paths.ScrollInterval.Duration < 50*time.Millisecond {
return fmt.Errorf("paths.scroll_interval must be at least 50ms")
}
if config.Display.ThreadListWidthPercent < 20 || config.Display.ThreadListWidthPercent > 60 {
return fmt.Errorf("display.thread_list_width_percent must be between 20 and 60")
}
if config.ShowAll && config.Repository == "" {
return fmt.Errorf("show_all requires repository")
}
return nil
}