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 }