support user module overrides

parent 4ed2eb67
......@@ -8,6 +8,7 @@ user-facing configuration UI is expected to live in `ximperconf shell panel`.
## Paths
- 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`
## Commands
......@@ -40,8 +41,11 @@ panel, floating, islands
## Module Resolution
The config stores logical module names. At generation time, the panel resolves
them against `modules.json`.
The config stores logical module names. At generation time, the panel merges
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:
......
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
......@@ -41,6 +42,9 @@ func LoadConfig(path string) (Config, error) {
}
return cfg, err
}
if len(bytes.TrimSpace(data)) == 0 {
return cfg, nil
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("parse panel config: %w", err)
}
......@@ -73,6 +77,10 @@ func UserConfigPath() string {
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 {
return filepath.Join(os.TempDir(), "ximper-shell", "panel")
}
......@@ -81,6 +89,10 @@ func RuntimeConfigPath() string {
return filepath.Join(RuntimeDir(), "config.jsonc")
}
func RuntimeModulesPath() string {
return filepath.Join(RuntimeDir(), "modules.jsonc")
}
func RuntimePIDPath() string {
return filepath.Join(RuntimeDir(), "waybar.pid")
}
......
......@@ -19,7 +19,7 @@ func GenerateConfig(cfg Config, registry Registry) ([]byte, error) {
"fixed-center": true,
"reload_style_on_change": true,
"include": []string{
ModulesFile,
RuntimeModulesPath(),
},
"modules-left": []string{},
"modules-center": []string{},
......@@ -66,6 +66,9 @@ func GenerateRuntimeConfig() ([]byte, error) {
if err != nil {
return nil, err
}
if err := WriteRuntimeModules(registry); err != nil {
return nil, err
}
return GenerateConfig(cfg, registry)
}
......@@ -74,11 +77,7 @@ func GenerateAndWriteRuntimeConfig() (string, error) {
if err != nil {
return "", err
}
path, err := WriteRuntimeConfig(data)
if err != nil {
return "", err
}
return path, nil
return WriteRuntimeConfig(data)
}
func WriteRuntimeConfig(data []byte) (string, error) {
......@@ -92,12 +91,23 @@ func WriteRuntimeConfig(data []byte) (string, error) {
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) {
cfg, err := LoadConfig(UserConfigPath())
if err != nil {
return Config{}, Registry{}, err
}
registry, err := LoadRegistry(ModulesFile)
registry, err := LoadMergedRegistry(ModulesFile, UserModulesPath())
if err != nil {
return Config{}, Registry{}, err
}
......
......@@ -82,7 +82,7 @@ func applyCommandSetting(cliCommand *cli.Command) {
}
func listModulesCommand(ctx context.Context, cmd *cli.Command) error {
registry, err := LoadRegistry(ModulesFile)
registry, err := LoadMergedRegistry(ModulesFile, UserModulesPath())
if err != nil {
return err
}
......
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"sort"
......@@ -12,11 +14,38 @@ type Registry struct {
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) {
data, err := os.ReadFile(path)
if err != nil {
return Registry{}, err
}
if len(bytes.TrimSpace(data)) == 0 {
return Registry{Modules: map[string]json.RawMessage{}}, nil
}
var modules map[string]json.RawMessage
if err := json.Unmarshal(StripJSONC(data), &modules); err != nil {
......@@ -26,6 +55,55 @@ func LoadRegistry(path string) (Registry, error) {
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 {
names := make([]string, 0, len(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