14 // endpointChatGPT names the ChatGPT backend with the subscription login.
15 endpointChatGPT = "chatgpt"
17 // endpointAPI names the OpenAI API with an API key.
21 // Config holds the settings of config.json in the data directory.
23 Endpoint string `json:"endpoint"`
24 Compactor ModelConfig `json:"compactor"`
27 // ModelConfig names a model and its reasoning effort.
28 type ModelConfig struct {
29 Model string `json:"model"`
30 Effort string `json:"effort"`
33 // defaultConfig returns the settings that apply when config.json does not
35 func defaultConfig() Config {
37 Endpoint: endpointChatGPT,
38 Compactor: ModelConfig{
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
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) {
56 return Config{}, fmt.Errorf("read config: %w", err)
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")
67 err = config.validate()
71 return Config{}, fmt.Errorf("config.json: %w", err)
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)
83 if c.Compactor.Model == "" || c.Compactor.Effort == "" {
84 return errors.New("compactor needs a model and an effort")
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")
98 dir, err = defaultDataDir()
104 err := os.MkdirAll(dir, 0o700)
106 return "", fmt.Errorf("create data directory: %w", err)
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
120 home, err := os.UserHomeDir()
122 return "", fmt.Errorf("find data directory: set ONECHAT_DIR: %w", err)
125 return filepath.Join(home, ".local", "share", "onechat"), nil