From 9d0ef598a7ca80dd9ac2dd84df5ece1ca26ee8f3 Mon Sep 17 00:00:00 2001 From: Hadi <112569860+anotherhadi@users.noreply.github.com> Date: Sat, 30 May 2026 00:14:54 +0200 Subject: [PATCH] Per-project configuration overwrites Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com> --- README.md | 13 +++++++++++++ cmd/spilltea/main.go | 38 ++++++++++++++++++++++++-------------- cmd/spilltea/root.go | 11 +++++++++++ docs/basics.md | 12 ++++++++++++ internal/config/config.go | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b5f48e9..ed2e3b9 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - [Installation](#installation) - [Project Management](#project-management) - [Configuration](#configuration) + - [Per-project configuration](#per-project-configuration) - [CLI Flags](#cli-flags) - [Plugin System](#plugin-system) - [Vim / Neovim Integration](#vim--neovim-integration) @@ -118,6 +119,18 @@ Check the default configuration with all the options [here](./internal/config/de Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once. +### Per-project configuration + +You can override any config value on a per-project basis by placing a `config.yaml` file inside the project directory (e.g. `~/.local/share/spilltea/projects/my-project/config.yaml`). + +Only the keys present in that file are overridden; everything else falls back to the global config. + +The priority order is: + +1. Global config (`~/.config/spilltea/config.yaml`) +2. Project config (`/config.yaml`) +3. CLI flags (always win) + ## CLI Flags ``` diff --git a/cmd/spilltea/main.go b/cmd/spilltea/main.go index 1b11719..349fa01 100644 --- a/cmd/spilltea/main.go +++ b/cmd/spilltea/main.go @@ -99,21 +99,26 @@ func main() { } config.Global.Version = version - if flagPluginsDir != "" { - config.Global.App.PluginsDir = flagPluginsDir - } - if flagHost != "" { - config.Global.App.Host = flagHost - } - if flagPort != 0 { - config.Global.App.Port = flagPort - } - if flagUpstreamProxy != "" { - config.Global.App.UpstreamProxy = flagUpstreamProxy - } - if cmd.Flags().Changed("ssl-insecure") { - config.Global.App.SslInsecure = flagSslInsecure + sslInsecureChanged := cmd.Flags().Changed("ssl-insecure") + applyCLI := func(c *config.Config) { + if flagPluginsDir != "" { + c.App.PluginsDir = flagPluginsDir + } + if flagHost != "" { + c.App.Host = flagHost + } + if flagPort != 0 { + c.App.Port = flagPort + } + if flagUpstreamProxy != "" { + c.App.UpstreamProxy = flagUpstreamProxy + } + if sslInsecureChanged { + c.App.SslInsecure = flagSslInsecure + } } + applyCLI(config.Global) + config.SetCLIOverrides(applyCLI) style.Init() icons.Init(config.Global) @@ -126,6 +131,11 @@ func main() { if err != nil { return fmt.Errorf("project: %w", err) } + if err := config.MergeProjectConfig(filepath.Dir(project.Path)); err != nil { + return fmt.Errorf("project config: %w", err) + } + icons.Init(config.Global) + keys.Init(config.Global) broker := intercept.NewBroker() m := appUI.New(broker, project.Name, project.Path) final, err := tea.NewProgram(m).Run() diff --git a/cmd/spilltea/root.go b/cmd/spilltea/root.go index c1b435b..8aa5642 100644 --- a/cmd/spilltea/root.go +++ b/cmd/spilltea/root.go @@ -1,8 +1,14 @@ package main import ( + "log" + "path/filepath" + tea "charm.land/bubbletea/v2" + "github.com/anotherhadi/spilltea/internal/config" + "github.com/anotherhadi/spilltea/internal/icons" "github.com/anotherhadi/spilltea/internal/intercept" + "github.com/anotherhadi/spilltea/internal/keys" appUI "github.com/anotherhadi/spilltea/internal/ui/app" homeUI "github.com/anotherhadi/spilltea/internal/ui/home" ) @@ -34,6 +40,11 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.state == rootStateHome { if sel, ok := msg.(homeUI.ProjectSelectedMsg); ok { + if err := config.MergeProjectConfig(filepath.Dir(sel.Project.Path)); err != nil { + log.Printf("project config: %v", err) + } + icons.Init(config.Global) + keys.Init(config.Global) broker := intercept.NewBroker() app := appUI.New(broker, sel.Project.Name, sel.Project.Path) m.app = app diff --git a/docs/basics.md b/docs/basics.md index 6b0a82f..b1a1383 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -15,6 +15,18 @@ Check the default configuration with all the options [here](./internal/config/de Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once. +### Per-project configuration + +You can override any config value on a per-project basis by placing a `config.yaml` file inside the project directory (e.g. `~/.local/share/spilltea/projects/my-project/config.yaml`). + +Only the keys present in that file are overridden; everything else falls back to the global config. + +The priority order is: + +1. Global config (`~/.config/spilltea/config.yaml`) +2. Project config (`/config.yaml`) +3. CLI flags (always win) + ## CLI Flags diff --git a/internal/config/config.go b/internal/config/config.go index 0f474bc..5e086c4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -65,6 +65,43 @@ type GlobalPlugin struct { var Global *Config +var projectCLIOverrides func(*Config) + +// SetCLIOverrides stores a function that re-applies CLI flag overrides. +// It is called automatically after MergeProjectConfig so that CLI flags +// always win over both global and per-project config. +func SetCLIOverrides(fn func(*Config)) { + projectCLIOverrides = fn +} + +// MergeProjectConfig merges /config.yaml on top of the current +// Global config, then re-applies any registered CLI overrides. +// If no config.yaml exists in the project directory, this is a no-op. +func MergeProjectConfig(projectDir string) error { + projectConfigPath := filepath.Join(projectDir, "config.yaml") + if _, err := os.Stat(projectConfigPath); errors.Is(err, os.ErrNotExist) { + return nil + } + + pv := viper.New() + pv.SetConfigFile(projectConfigPath) + if err := pv.ReadInConfig(); err != nil { + return fmt.Errorf("project config: %w", err) + } + if err := viper.MergeConfigMap(pv.AllSettings()); err != nil { + return fmt.Errorf("merge project config: %w", err) + } + if err := viper.Unmarshal(Global); err != nil { + return err + } + Global.App.ProxyAuth = expandEnvValue(Global.App.ProxyAuth) + Global.App.UpstreamProxy = expandEnvValue(Global.App.UpstreamProxy) + if projectCLIOverrides != nil { + projectCLIOverrides(Global) + } + return nil +} + func Load(path string) error { var defaults map[string]any if err := yaml.Unmarshal(defaultConfig, &defaults); err != nil {