]> git.giacomolli.uk - onechat/blob - config.go
Send the end of the view in one part with the task
[onechat] / config.go
1 package main
2
3 import (
4         "bytes"
5         "encoding/json"
6         "errors"
7         "fmt"
8         "io"
9         "os"
10         "path/filepath"
11 )
12
13 const (
14         // endpointChatGPT names the ChatGPT backend with the subscription login.
15         endpointChatGPT = "chatgpt"
16
17         // endpointAPI names the OpenAI API with an API key.
18         endpointAPI = "api"
19 )
20
21 // Config holds the settings of config.json in the data directory.
22 type Config struct {
23         Endpoint  string      `json:"endpoint"`
24         Compactor ModelConfig `json:"compactor"`
25 }
26
27 // ModelConfig names a model and its reasoning effort.
28 type ModelConfig struct {
29         Model  string `json:"model"`
30         Effort string `json:"effort"`
31 }
32
33 // defaultConfig returns the settings that apply when config.json does not
34 // set them.
35 func defaultConfig() Config {
36         return Config{
37                 Endpoint: endpointChatGPT,
38                 Compactor: ModelConfig{
39                         Model:  "gpt-6.1-sol",
40                         Effort: "high",
41                 },
42         }
43 }
44
45 // loadConfig reads config.json in dir over the defaults. A missing file gives
46 // the defaults. An unknown field is an error, so a typo does not become a
47 // silent default.
48 func loadConfig(dir string) (Config, error) {
49         config := defaultConfig()
50         data, err := os.ReadFile(filepath.Join(dir, "config.json"))
51         if errors.Is(err, os.ErrNotExist) {
52                 return config, nil
53         }
54
55         if err != nil {
56                 return Config{}, fmt.Errorf("read config: %w", err)
57         }
58
59         decoder := json.NewDecoder(bytes.NewReader(data))
60         decoder.DisallowUnknownFields()
61         err = decoder.Decode(&config)
62         if err == nil && !errors.Is(decoder.Decode(&struct{}{}), io.EOF) {
63                 err = errors.New("data after the JSON object")
64         }
65
66         if err == nil {
67                 err = config.validate()
68         }
69
70         if err != nil {
71                 return Config{}, fmt.Errorf("config.json: %w", err)
72         }
73
74         return config, nil
75 }
76
77 // validate makes sure that each setting has a valid value.
78 func (c Config) validate() error {
79         if c.Endpoint != endpointChatGPT && c.Endpoint != endpointAPI {
80                 return fmt.Errorf("endpoint %q is not %q or %q", c.Endpoint, endpointChatGPT, endpointAPI)
81         }
82
83         if c.Compactor.Model == "" || c.Compactor.Effort == "" {
84                 return errors.New("compactor needs a model and an effort")
85         }
86
87         return nil
88 }
89
90 // dataDir returns the data directory and creates it with mode 0700 when it
91 // does not exist. It is ONECHAT_DIR when that is set. Otherwise it is
92 // $XDG_DATA_HOME/onechat when XDG_DATA_HOME is an absolute path, or else
93 // ~/.local/share/onechat.
94 func dataDir() (string, error) {
95         dir := os.Getenv("ONECHAT_DIR")
96         if dir == "" {
97                 var err error
98                 dir, err = defaultDataDir()
99                 if err != nil {
100                         return "", err
101                 }
102         }
103
104         err := os.MkdirAll(dir, 0o700)
105         if err != nil {
106                 return "", fmt.Errorf("create data directory: %w", err)
107         }
108
109         return dir, nil
110 }
111
112 // defaultDataDir returns the data directory for an unset ONECHAT_DIR. The XDG
113 // specification says to ignore a relative XDG_DATA_HOME.
114 func defaultDataDir() (string, error) {
115         xdg := os.Getenv("XDG_DATA_HOME")
116         if filepath.IsAbs(xdg) {
117                 return filepath.Join(xdg, "onechat"), nil
118         }
119
120         home, err := os.UserHomeDir()
121         if err != nil {
122                 return "", fmt.Errorf("find data directory: set ONECHAT_DIR: %w", err)
123         }
124
125         return filepath.Join(home, ".local", "share", "onechat"), nil
126 }