package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" ) const ( // endpointChatGPT names the ChatGPT backend with the subscription login. endpointChatGPT = "chatgpt" // endpointAPI names the OpenAI API with an API key. endpointAPI = "api" ) // Config holds the settings of config.json in the data directory. type Config struct { Endpoint string `json:"endpoint"` Compactor ModelConfig `json:"compactor"` } // ModelConfig names a model and its reasoning effort. type ModelConfig struct { Model string `json:"model"` Effort string `json:"effort"` } // defaultConfig returns the settings that apply when config.json does not // set them. func defaultConfig() Config { return Config{ Endpoint: endpointChatGPT, Compactor: ModelConfig{ Model: "gpt-6.1-sol", Effort: "high", }, } } // loadConfig reads config.json in dir over the defaults. A missing file gives // the defaults. An unknown field is an error, so a typo does not become a // silent default. func loadConfig(dir string) (Config, error) { config := defaultConfig() data, err := os.ReadFile(filepath.Join(dir, "config.json")) if errors.Is(err, os.ErrNotExist) { return config, nil } if err != nil { return Config{}, fmt.Errorf("read config: %w", err) } decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() err = decoder.Decode(&config) if err == nil && !errors.Is(decoder.Decode(&struct{}{}), io.EOF) { err = errors.New("data after the JSON object") } if err == nil { err = config.validate() } if err != nil { return Config{}, fmt.Errorf("config.json: %w", err) } return config, nil } // validate makes sure that each setting has a valid value. func (c Config) validate() error { if c.Endpoint != endpointChatGPT && c.Endpoint != endpointAPI { return fmt.Errorf("endpoint %q is not %q or %q", c.Endpoint, endpointChatGPT, endpointAPI) } if c.Compactor.Model == "" || c.Compactor.Effort == "" { return errors.New("compactor needs a model and an effort") } return nil } // dataDir returns the data directory and creates it with mode 0700 when it // does not exist. It is ONECHAT_DIR when that is set. Otherwise it is // $XDG_DATA_HOME/onechat when XDG_DATA_HOME is an absolute path, or else // ~/.local/share/onechat. func dataDir() (string, error) { dir := os.Getenv("ONECHAT_DIR") if dir == "" { var err error dir, err = defaultDataDir() if err != nil { return "", err } } err := os.MkdirAll(dir, 0o700) if err != nil { return "", fmt.Errorf("create data directory: %w", err) } return dir, nil } // defaultDataDir returns the data directory for an unset ONECHAT_DIR. The XDG // specification says to ignore a relative XDG_DATA_HOME. func defaultDataDir() (string, error) { xdg := os.Getenv("XDG_DATA_HOME") if filepath.IsAbs(xdg) { return filepath.Join(xdg, "onechat"), nil } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("find data directory: set ONECHAT_DIR: %w", err) } return filepath.Join(home, ".local", "share", "onechat"), nil }