diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-17 21:39:01 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-17 21:39:01 +0300 |
| commit | ad99f0a6a65bb1d327feb933d5349fc067c881f2 (patch) | |
| tree | f8e66b81639e134e4898e000ff34e8782db57fa0 /internal | |
| parent | 454451105ad3522d2ac3d22136eedee4a4d034af (diff) | |
feat: Support XDG config home
This change implements support for the XDG Base Directory Specification for the configuration file.
The configuration file is now read from `$XDG_CONFIG_HOME/hexai/config.json` if the `XDG_CONFIG_HOME` environment variable is set.
If it is not set, it falls back to the previous location, `$HOME/.config/hexai/config.json`.
This change also includes:
- A fix for a bug in the test suite where a test was failing due to an environment variable being set.
- Updates to the documentation to reflect the new configuration file location.
- A version bump to 0.1.0.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/appconfig/config.go | 160 | ||||
| -rw-r--r-- | internal/hexailsp/run_test.go | 4 | ||||
| -rw-r--r-- | internal/version.go | 1 |
3 files changed, 94 insertions, 71 deletions
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go index c0f28d2..6b8df4a 100644 --- a/internal/appconfig/config.go +++ b/internal/appconfig/config.go @@ -29,75 +29,95 @@ type App struct { CopilotModel string `json:"copilot_model"` } -// Load reads configuration from ~/.config/hexai/config.json and merges with defaults. +// Load reads configuration from a file and merges with defaults. +// It respects the XDG Base Directory Specification. func Load(logger *log.Logger) App { - cfg := App{ - MaxTokens: 4000, - ContextMode: "always-full", - ContextWindowLines: 120, - MaxContextTokens: 4000, - LogPreviewLimit: 100, - NoDiskIO: true, - } - home, err := os.UserHomeDir() - if err != nil { - return cfg - } - path := filepath.Join(home, ".config", "hexai", "config.json") - f, err := os.Open(path) - if err != nil { - return cfg - } - defer f.Close() - dec := json.NewDecoder(f) - var fileCfg App - if err := dec.Decode(&fileCfg); err != nil { - if logger != nil { - logger.Printf("invalid config file %s: %v", path, err) - } - return cfg - } - // Merge: file overrides defaults when provided - if fileCfg.MaxTokens > 0 { - cfg.MaxTokens = fileCfg.MaxTokens - } - if strings.TrimSpace(fileCfg.ContextMode) != "" { - cfg.ContextMode = fileCfg.ContextMode - } - if fileCfg.ContextWindowLines > 0 { - cfg.ContextWindowLines = fileCfg.ContextWindowLines - } - if fileCfg.MaxContextTokens > 0 { - cfg.MaxContextTokens = fileCfg.MaxContextTokens - } - if fileCfg.LogPreviewLimit >= 0 { - cfg.LogPreviewLimit = fileCfg.LogPreviewLimit - } - cfg.NoDiskIO = fileCfg.NoDiskIO - if len(fileCfg.TriggerCharacters) > 0 { - cfg.TriggerCharacters = append([]string{}, fileCfg.TriggerCharacters...) - } - if strings.TrimSpace(fileCfg.Provider) != "" { - cfg.Provider = fileCfg.Provider - } - // Provider-specific options - if strings.TrimSpace(fileCfg.OpenAIBaseURL) != "" { - cfg.OpenAIBaseURL = fileCfg.OpenAIBaseURL - } - if strings.TrimSpace(fileCfg.OpenAIModel) != "" { - cfg.OpenAIModel = fileCfg.OpenAIModel - } - if strings.TrimSpace(fileCfg.OllamaBaseURL) != "" { - cfg.OllamaBaseURL = fileCfg.OllamaBaseURL - } - if strings.TrimSpace(fileCfg.OllamaModel) != "" { - cfg.OllamaModel = fileCfg.OllamaModel - } - if strings.TrimSpace(fileCfg.CopilotBaseURL) != "" { - cfg.CopilotBaseURL = fileCfg.CopilotBaseURL - } - if strings.TrimSpace(fileCfg.CopilotModel) != "" { - cfg.CopilotModel = fileCfg.CopilotModel - } - return cfg + cfg := App{ + MaxTokens: 4000, + ContextMode: "always-full", + ContextWindowLines: 120, + MaxContextTokens: 4000, + LogPreviewLimit: 100, + NoDiskIO: true, + } + + if logger == nil { + return cfg // Return defaults if no logger is provided (e.g. in tests) + } + + var configPath string + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { + configPath = filepath.Join(xdgConfigHome, "hexai", "config.json") + } else { + home, err := os.UserHomeDir() + if err != nil { + if logger != nil { + logger.Printf("cannot find user home directory: %v", err) + } + return cfg // Return defaults if home dir is not found + } + configPath = filepath.Join(home, ".config", "hexai", "config.json") + } + + f, err := os.Open(configPath) + if err != nil { + if !os.IsNotExist(err) && logger != nil { + logger.Printf("cannot open config file %s: %v", configPath, err) + } + return cfg // Return defaults if file doesn't exist or can't be opened + } + defer f.Close() + + dec := json.NewDecoder(f) + var fileCfg App + if err := dec.Decode(&fileCfg); err != nil { + if logger != nil { + logger.Printf("invalid config file %s: %v", configPath, err) + } + return cfg // Return defaults on decoding error + } + + // Merge: file overrides defaults when provided + if fileCfg.MaxTokens > 0 { + cfg.MaxTokens = fileCfg.MaxTokens + } + if strings.TrimSpace(fileCfg.ContextMode) != "" { + cfg.ContextMode = fileCfg.ContextMode + } + if fileCfg.ContextWindowLines > 0 { + cfg.ContextWindowLines = fileCfg.ContextWindowLines + } + if fileCfg.MaxContextTokens > 0 { + cfg.MaxContextTokens = fileCfg.MaxContextTokens + } + if fileCfg.LogPreviewLimit >= 0 { + cfg.LogPreviewLimit = fileCfg.LogPreviewLimit + } + cfg.NoDiskIO = fileCfg.NoDiskIO + if len(fileCfg.TriggerCharacters) > 0 { + cfg.TriggerCharacters = append([]string{}, fileCfg.TriggerCharacters...) + } + if strings.TrimSpace(fileCfg.Provider) != "" { + cfg.Provider = fileCfg.Provider + } + // Provider-specific options + if strings.TrimSpace(fileCfg.OpenAIBaseURL) != "" { + cfg.OpenAIBaseURL = fileCfg.OpenAIBaseURL + } + if strings.TrimSpace(fileCfg.OpenAIModel) != "" { + cfg.OpenAIModel = fileCfg.OpenAIModel + } + if strings.TrimSpace(fileCfg.OllamaBaseURL) != "" { + cfg.OllamaBaseURL = fileCfg.OllamaBaseURL + } + if strings.TrimSpace(fileCfg.OllamaModel) != "" { + cfg.OllamaModel = fileCfg.OllamaModel + } + if strings.TrimSpace(fileCfg.CopilotBaseURL) != "" { + cfg.CopilotBaseURL = fileCfg.CopilotBaseURL + } + if strings.TrimSpace(fileCfg.CopilotModel) != "" { + cfg.CopilotModel = fileCfg.CopilotModel + } + return cfg } diff --git a/internal/hexailsp/run_test.go b/internal/hexailsp/run_test.go index 2c0fcaf..923f408 100644 --- a/internal/hexailsp/run_test.go +++ b/internal/hexailsp/run_test.go @@ -24,6 +24,10 @@ type fakeServer struct{ func (f *fakeServer) Run() error { f.ran = true; return nil } func TestRunWithFactory_UsesDefaultsAndCallsServer(t *testing.T) { + old := os.Getenv("OPENAI_API_KEY") + t.Cleanup(func(){ _ = os.Setenv("OPENAI_API_KEY", old) }) + _ = os.Setenv("OPENAI_API_KEY", "") + var stderr bytes.Buffer logger := log.New(&stderr, "hexai-lsp ", 0) cfg := appconfig.Load(nil) // defaults diff --git a/internal/version.go b/internal/version.go index a6b1527..d86e60b 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,5 +1,4 @@ // Summary: Hexai semantic version identifier used by CLI and LSP binaries. -// Not yet reviewed by a human package internal const Version = "0.1.0" |
