gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs | README

config.go (4194B)


      1 package config
      2 
      3 import (
      4 	"encoding/json"
      5 	"errors"
      6 	"net/url"
      7 	"os"
      8 	"path/filepath"
      9 	"runtime"
     10 	"strings"
     11 )
     12 
     13 const DefaultURL = "https://textlog.cc"
     14 
     15 type Theme string
     16 
     17 const (
     18 	ThemeAuto  Theme = "auto"
     19 	ThemeLight Theme = "light"
     20 	ThemeDark  Theme = "dark"
     21 )
     22 
     23 type FeedKind string
     24 
     25 const (
     26 	FeedForYou FeedKind = "for-you"
     27 	FeedToMe   FeedKind = "to-me"
     28 	FeedHot    FeedKind = "hot"
     29 	FeedLatest FeedKind = "latest"
     30 )
     31 
     32 type Config struct {
     33 	BaseURL string   `json:"baseUrl"`
     34 	Theme   Theme    `json:"theme"`
     35 	Token   string   `json:"token,omitempty"`
     36 	LastTab FeedKind `json:"lastTab,omitempty"`
     37 	NoColor bool     `json:"-"`
     38 }
     39 
     40 func NormalizeBaseURL(value string) (string, error) {
     41 	value = strings.TrimSpace(value)
     42 	u, err := url.Parse(value)
     43 	if err != nil || u.Scheme == "" || u.Host == "" {
     44 		return "", errors.New("Textlog URL must be a valid HTTP(S) URL")
     45 	}
     46 	u.Scheme = strings.ToLower(u.Scheme)
     47 	if u.Scheme != "http" && u.Scheme != "https" {
     48 		return "", errors.New("Textlog URL must use HTTP or HTTPS")
     49 	}
     50 	if u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.RawFragment != "" {
     51 		return "", errors.New("Textlog URL cannot include a query or fragment")
     52 	}
     53 	u.Host = strings.ToLower(u.Host)
     54 	u.Path = strings.TrimRight(u.Path, "/")
     55 	u.RawPath = strings.TrimRight(u.RawPath, "/")
     56 	return strings.TrimSuffix(u.String(), "/"), nil
     57 }
     58 
     59 func ConfigPath(env map[string]string) string {
     60 	if env == nil {
     61 		env = Environment()
     62 	}
     63 	home := env["HOME"]
     64 	if home == "" {
     65 		home, _ = os.UserHomeDir()
     66 	}
     67 	return configPathFor(runtime.GOOS, home, env)
     68 }
     69 func configPathFor(goos, home string, env map[string]string) string {
     70 	switch goos {
     71 	case "windows":
     72 		root := env["APPDATA"]
     73 		if root == "" {
     74 			root = filepath.Join(home, "AppData", "Roaming")
     75 		}
     76 		return filepath.Join(root, "textlog", "config.json")
     77 	case "darwin":
     78 		return filepath.Join(home, "Library", "Application Support", "textlog", "config.json")
     79 	default:
     80 		root := env["XDG_CONFIG_HOME"]
     81 		if root == "" {
     82 			root = filepath.Join(home, ".config")
     83 		}
     84 		return filepath.Join(root, "textlog", "config.json")
     85 	}
     86 }
     87 func validTheme(t Theme) bool { return t == ThemeAuto || t == ThemeLight || t == ThemeDark }
     88 func validTab(t FeedKind) bool {
     89 	return t == FeedForYou || t == FeedToMe || t == FeedHot || t == FeedLatest
     90 }
     91 func Load(env map[string]string) (Config, error) {
     92 	if env == nil {
     93 		env = Environment()
     94 	}
     95 	var stored Config
     96 	if data, err := os.ReadFile(ConfigPath(env)); err == nil {
     97 		var decoded Config
     98 		if json.Unmarshal(data, &decoded) == nil {
     99 			stored = decoded
    100 		}
    101 	}
    102 	base := stored.BaseURL
    103 	if base == "" {
    104 		base = DefaultURL
    105 	}
    106 	if env["TEXTLOG_URL"] != "" {
    107 		base = env["TEXTLOG_URL"]
    108 	}
    109 	var err error
    110 	base, err = NormalizeBaseURL(base)
    111 	if err != nil {
    112 		return Config{}, err
    113 	}
    114 	theme := stored.Theme
    115 	if theme == "" {
    116 		theme = ThemeAuto
    117 	}
    118 	if env["TEXTLOG_THEME"] != "" {
    119 		theme = Theme(env["TEXTLOG_THEME"])
    120 	}
    121 	_, noColor := env["NO_COLOR"]
    122 	if noColor {
    123 		theme = ThemeLight
    124 	}
    125 	if !validTheme(theme) {
    126 		theme = ThemeAuto
    127 	}
    128 	token := stored.Token
    129 	if env["TEXTLOG_TOKEN"] != "" {
    130 		token = env["TEXTLOG_TOKEN"]
    131 	}
    132 	last := stored.LastTab
    133 	if !validTab(last) {
    134 		last = ""
    135 	}
    136 	return Config{BaseURL: base, Theme: theme, Token: token, LastTab: last, NoColor: noColor}, nil
    137 }
    138 func Save(cfg Config, env map[string]string) error {
    139 	path := ConfigPath(env)
    140 	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
    141 		return err
    142 	}
    143 	data, err := json.MarshalIndent(cfg, "", "  ")
    144 	if err != nil {
    145 		return err
    146 	}
    147 	data = append(data, '\n')
    148 	tmp, err := os.CreateTemp(filepath.Dir(path), "config-*.tmp")
    149 	if err != nil {
    150 		return err
    151 	}
    152 	name := tmp.Name()
    153 	defer os.Remove(name)
    154 	if err := tmp.Chmod(0600); err != nil {
    155 		tmp.Close()
    156 		return err
    157 	}
    158 	if _, err = tmp.Write(data); err == nil {
    159 		err = tmp.Sync()
    160 	}
    161 	if closeErr := tmp.Close(); err == nil {
    162 		err = closeErr
    163 	}
    164 	if err != nil {
    165 		return err
    166 	}
    167 	if err = os.Rename(name, path); err != nil {
    168 		return err
    169 	}
    170 	return os.Chmod(path, 0600)
    171 }
    172 func Environment() map[string]string {
    173 	out := map[string]string{}
    174 	for _, item := range os.Environ() {
    175 		if k, v, ok := strings.Cut(item, "="); ok {
    176 			out[k] = v
    177 		}
    178 	}
    179 	return out
    180 }