support user module overrides

parent 4ed2eb67
...@@ -8,6 +8,7 @@ user-facing configuration UI is expected to live in `ximperconf shell panel`. ...@@ -8,6 +8,7 @@ user-facing configuration UI is expected to live in `ximperconf shell panel`.
## Paths ## Paths
- User config: `~/.config/ximper-shell/panel/config.json` - User config: `~/.config/ximper-shell/panel/config.json`
- User modules: `~/.config/ximper-shell/panel/modules.json`
- System modules: `/usr/share/ximperdistro/wm/base/waybar/modules.json` - System modules: `/usr/share/ximperdistro/wm/base/waybar/modules.json`
## Commands ## Commands
...@@ -40,8 +41,11 @@ panel, floating, islands ...@@ -40,8 +41,11 @@ panel, floating, islands
## Module Resolution ## Module Resolution
The config stores logical module names. At generation time, the panel resolves The config stores logical module names. At generation time, the panel merges
them against `modules.json`. system and user modules, then resolves names against the merged registry.
User modules override system modules with the same name. Object values are
merged recursively, so a user module can override a single option without
copying the whole system module.
Examples: Examples:
......
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
...@@ -41,6 +42,9 @@ func LoadConfig(path string) (Config, error) { ...@@ -41,6 +42,9 @@ func LoadConfig(path string) (Config, error) {
} }
return cfg, err return cfg, err
} }
if len(bytes.TrimSpace(data)) == 0 {
return cfg, nil
}
if err := json.Unmarshal(data, &cfg); err != nil { if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("parse panel config: %w", err) return cfg, fmt.Errorf("parse panel config: %w", err)
} }
...@@ -73,6 +77,10 @@ func UserConfigPath() string { ...@@ -73,6 +77,10 @@ func UserConfigPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ximper-shell", "panel", "config.json") return filepath.Join(os.Getenv("HOME"), ".config", "ximper-shell", "panel", "config.json")
} }
func UserModulesPath() string {
return filepath.Join(os.Getenv("HOME"), ".config", "ximper-shell", "panel", "modules.json")
}
func RuntimeDir() string { func RuntimeDir() string {
return filepath.Join(os.TempDir(), "ximper-shell", "panel") return filepath.Join(os.TempDir(), "ximper-shell", "panel")
} }
...@@ -81,6 +89,10 @@ func RuntimeConfigPath() string { ...@@ -81,6 +89,10 @@ func RuntimeConfigPath() string {
return filepath.Join(RuntimeDir(), "config.jsonc") return filepath.Join(RuntimeDir(), "config.jsonc")
} }
func RuntimeModulesPath() string {
return filepath.Join(RuntimeDir(), "modules.jsonc")
}
func RuntimePIDPath() string { func RuntimePIDPath() string {
return filepath.Join(RuntimeDir(), "waybar.pid") return filepath.Join(RuntimeDir(), "waybar.pid")
} }
......
...@@ -19,7 +19,7 @@ func GenerateConfig(cfg Config, registry Registry) ([]byte, error) { ...@@ -19,7 +19,7 @@ func GenerateConfig(cfg Config, registry Registry) ([]byte, error) {
"fixed-center": true, "fixed-center": true,
"reload_style_on_change": true, "reload_style_on_change": true,
"include": []string{ "include": []string{
ModulesFile, RuntimeModulesPath(),
}, },
"modules-left": []string{}, "modules-left": []string{},
"modules-center": []string{}, "modules-center": []string{},
...@@ -66,6 +66,9 @@ func GenerateRuntimeConfig() ([]byte, error) { ...@@ -66,6 +66,9 @@ func GenerateRuntimeConfig() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := WriteRuntimeModules(registry); err != nil {
return nil, err
}
return GenerateConfig(cfg, registry) return GenerateConfig(cfg, registry)
} }
...@@ -74,11 +77,7 @@ func GenerateAndWriteRuntimeConfig() (string, error) { ...@@ -74,11 +77,7 @@ func GenerateAndWriteRuntimeConfig() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
path, err := WriteRuntimeConfig(data) return WriteRuntimeConfig(data)
if err != nil {
return "", err
}
return path, nil
} }
func WriteRuntimeConfig(data []byte) (string, error) { func WriteRuntimeConfig(data []byte) (string, error) {
...@@ -92,12 +91,23 @@ func WriteRuntimeConfig(data []byte) (string, error) { ...@@ -92,12 +91,23 @@ func WriteRuntimeConfig(data []byte) (string, error) {
return path, nil return path, nil
} }
func WriteRuntimeModules(registry Registry) error {
data, err := registry.Marshal()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(RuntimeModulesPath()), 0755); err != nil {
return err
}
return os.WriteFile(RuntimeModulesPath(), data, 0644)
}
func loadInputs() (Config, Registry, error) { func loadInputs() (Config, Registry, error) {
cfg, err := LoadConfig(UserConfigPath()) cfg, err := LoadConfig(UserConfigPath())
if err != nil { if err != nil {
return Config{}, Registry{}, err return Config{}, Registry{}, err
} }
registry, err := LoadRegistry(ModulesFile) registry, err := LoadMergedRegistry(ModulesFile, UserModulesPath())
if err != nil { if err != nil {
return Config{}, Registry{}, err return Config{}, Registry{}, err
} }
......
...@@ -82,7 +82,7 @@ func applyCommandSetting(cliCommand *cli.Command) { ...@@ -82,7 +82,7 @@ func applyCommandSetting(cliCommand *cli.Command) {
} }
func listModulesCommand(ctx context.Context, cmd *cli.Command) error { func listModulesCommand(ctx context.Context, cmd *cli.Command) error {
registry, err := LoadRegistry(ModulesFile) registry, err := LoadMergedRegistry(ModulesFile, UserModulesPath())
if err != nil { if err != nil {
return err return err
} }
......
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"sort" "sort"
...@@ -12,11 +14,38 @@ type Registry struct { ...@@ -12,11 +14,38 @@ type Registry struct {
Modules map[string]json.RawMessage Modules map[string]json.RawMessage
} }
func LoadMergedRegistry(systemPath, userPath string) (Registry, error) {
systemRegistry, err := LoadRegistry(systemPath)
if err != nil {
return Registry{}, err
}
userRegistry, err := LoadRegistry(userPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return systemRegistry, nil
}
return Registry{}, err
}
for name, raw := range userRegistry.Modules {
merged, err := mergeModule(systemRegistry.Modules[name], raw)
if err != nil {
return Registry{}, fmt.Errorf("merge module %q: %w", name, err)
}
systemRegistry.Modules[name] = merged
}
return systemRegistry, nil
}
func LoadRegistry(path string) (Registry, error) { func LoadRegistry(path string) (Registry, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return Registry{}, err return Registry{}, err
} }
if len(bytes.TrimSpace(data)) == 0 {
return Registry{Modules: map[string]json.RawMessage{}}, nil
}
var modules map[string]json.RawMessage var modules map[string]json.RawMessage
if err := json.Unmarshal(StripJSONC(data), &modules); err != nil { if err := json.Unmarshal(StripJSONC(data), &modules); err != nil {
...@@ -26,6 +55,55 @@ func LoadRegistry(path string) (Registry, error) { ...@@ -26,6 +55,55 @@ func LoadRegistry(path string) (Registry, error) {
return Registry{Modules: modules}, nil return Registry{Modules: modules}, nil
} }
func mergeModule(base, override json.RawMessage) (json.RawMessage, error) {
if len(base) == 0 {
return override, nil
}
var baseValue any
if err := json.Unmarshal(base, &baseValue); err != nil {
return nil, err
}
var overrideValue any
if err := json.Unmarshal(override, &overrideValue); err != nil {
return nil, err
}
merged := mergeValue(baseValue, overrideValue)
data, err := json.Marshal(merged)
if err != nil {
return nil, err
}
return data, nil
}
func mergeValue(base, override any) any {
baseMap, baseOK := base.(map[string]any)
overrideMap, overrideOK := override.(map[string]any)
if !baseOK || !overrideOK {
return override
}
for key, overrideValue := range overrideMap {
if baseValue, ok := baseMap[key]; ok {
baseMap[key] = mergeValue(baseValue, overrideValue)
} else {
baseMap[key] = overrideValue
}
}
return baseMap
}
func (r Registry) Marshal() ([]byte, error) {
data, err := json.MarshalIndent(r.Modules, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
func (r Registry) Names() []string { func (r Registry) Names() []string {
names := make([]string, 0, len(r.Modules)) names := make([]string, 0, len(r.Modules))
for name := range r.Modules { for name := range r.Modules {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment