2024-06-22 09:41:49 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-06-20 08:59:41 +02:00
|
|
|
package wconfig
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2024-07-25 05:34:22 +02:00
|
|
|
"errors"
|
2024-06-20 08:59:41 +02:00
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2024-07-25 05:34:22 +02:00
|
|
|
"strings"
|
2024-06-20 08:59:41 +02:00
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
|
|
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
|
|
|
)
|
|
|
|
|
|
|
|
const configDir = "config"
|
|
|
|
|
|
|
|
var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir)
|
2024-07-25 05:34:22 +02:00
|
|
|
var termThemesDirAbsPath = filepath.Join(configDirAbsPath, termThemesDir)
|
2024-06-20 08:59:41 +02:00
|
|
|
|
|
|
|
var instance *Watcher
|
|
|
|
var once sync.Once
|
|
|
|
|
|
|
|
type Watcher struct {
|
2024-07-25 05:34:22 +02:00
|
|
|
initialized bool
|
|
|
|
watcher *fsnotify.Watcher
|
|
|
|
mutex sync.Mutex
|
|
|
|
settingsData SettingsConfigType
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type WatcherUpdate struct {
|
2024-07-25 05:34:22 +02:00
|
|
|
Settings SettingsConfigType `json:"settings"`
|
|
|
|
Error string `json:"error"`
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
func LoadFullSettings() (*SettingsConfigType, error) {
|
|
|
|
// first load settings.json
|
|
|
|
// then load themes
|
|
|
|
// then apply defaults
|
|
|
|
settings, err := readFileContents[SettingsConfigType](settingsAbsPath, false)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
themes, err := readThemes()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if settings.TermThemes == nil {
|
|
|
|
settings.TermThemes = make(map[string]TermThemeType)
|
|
|
|
}
|
|
|
|
for k, v := range themes {
|
|
|
|
settings.TermThemes[k] = v
|
|
|
|
}
|
|
|
|
applyDefaultSettings(settings)
|
|
|
|
return settings, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func readThemes() (map[string]TermThemeType, error) {
|
|
|
|
files, err := os.ReadDir(termThemesDirAbsPath)
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error reading themes directory: %v", err)
|
|
|
|
}
|
|
|
|
themes := make(map[string]TermThemeType)
|
|
|
|
for _, file := range files {
|
|
|
|
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
|
|
|
|
log.Printf("reading theme file %s\n", file.Name())
|
|
|
|
theme, err := readFileContents[TermThemeType](filepath.Join(termThemesDirAbsPath, file.Name()), true)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("error reading theme file %s: %v", file.Name(), err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if theme == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
themeName := getThemeName(file.Name())
|
|
|
|
themes[themeName] = *theme
|
|
|
|
}
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
2024-07-25 05:34:22 +02:00
|
|
|
return themes, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func readFileContents[T any](filePath string, nilOnNotExist bool) (*T, error) {
|
|
|
|
var content T
|
2024-06-20 08:59:41 +02:00
|
|
|
data, err := os.ReadFile(filePath)
|
2024-07-25 05:34:22 +02:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
if nilOnNotExist {
|
|
|
|
return nil, nil
|
|
|
|
} else {
|
|
|
|
return &content, nil
|
|
|
|
}
|
|
|
|
}
|
2024-06-20 08:59:41 +02:00
|
|
|
if err != nil {
|
2024-07-25 05:34:22 +02:00
|
|
|
log.Printf("could not read file %s: %v", filePath, err)
|
2024-06-20 08:59:41 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &content); err != nil {
|
2024-07-25 05:34:22 +02:00
|
|
|
log.Printf("could not unmarshal file %s: %v", filePath, err)
|
2024-06-20 08:59:41 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &content, nil
|
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
func isInDirectory(fileName, directory string) bool {
|
|
|
|
rel, err := filepath.Rel(directory, fileName)
|
|
|
|
return err == nil && !strings.HasPrefix(rel, "..")
|
|
|
|
}
|
|
|
|
|
2024-06-20 08:59:41 +02:00
|
|
|
// GetWatcher returns the singleton instance of the Watcher
|
|
|
|
func GetWatcher() *Watcher {
|
|
|
|
once.Do(func() {
|
|
|
|
watcher, err := fsnotify.NewWatcher()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to create file watcher: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
instance = &Watcher{watcher: watcher}
|
2024-07-25 05:34:22 +02:00
|
|
|
if err := instance.addSettingsFile(settingsAbsPath); err != nil {
|
|
|
|
log.Printf("failed to add path %s to watcher: %v", settingsAbsPath, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := instance.addTermThemesDir(termThemesDirAbsPath); err != nil {
|
|
|
|
log.Printf("failed to add terminal themes path %s to watcher: %v", termThemesDirAbsPath, err)
|
2024-06-20 08:59:41 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return instance
|
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
func (w *Watcher) addSettingsFile(filePath string) error {
|
2024-06-20 08:59:41 +02:00
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
dir := filepath.Dir(filePath)
|
|
|
|
err := os.MkdirAll(dir, 0751)
|
2024-06-20 08:59:41 +02:00
|
|
|
if err != nil {
|
2024-07-25 05:34:22 +02:00
|
|
|
return fmt.Errorf("error creating config directory: %v", err)
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
w.watcher.Add(filePath)
|
|
|
|
log.Printf("started config watcher: %v\n", filePath)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) addTermThemesDir(dir string) error {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
_, err := os.Stat(dir)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
if err := os.MkdirAll(dir, 0751); err != nil {
|
|
|
|
return fmt.Errorf("error creating themes directory: %v", err)
|
|
|
|
}
|
|
|
|
} else if err != nil {
|
|
|
|
return fmt.Errorf("error accessing themes directory: %v", err)
|
|
|
|
}
|
|
|
|
if err := w.watcher.Add(dir); err != nil {
|
|
|
|
return fmt.Errorf("error adding themes directory to watcher: %v", err)
|
|
|
|
}
|
|
|
|
log.Printf("started termthemes watcher: %v\n", dir)
|
2024-06-20 08:59:41 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) Start() {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
log.Printf("starting file watcher\n")
|
|
|
|
w.initialized = true
|
|
|
|
w.sendInitialValues()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case event, ok := <-w.watcher.Events:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.handleEvent(event)
|
|
|
|
case err, ok := <-w.watcher.Errors:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
log.Println("watcher error:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// for initial values, exit on first error
|
|
|
|
func (w *Watcher) sendInitialValues() error {
|
2024-07-25 05:34:22 +02:00
|
|
|
settings, err := LoadFullSettings()
|
|
|
|
if err != nil {
|
2024-06-20 08:59:41 +02:00
|
|
|
return err
|
|
|
|
}
|
2024-07-25 05:34:22 +02:00
|
|
|
w.settingsData = *settings
|
2024-06-20 08:59:41 +02:00
|
|
|
message := WatcherUpdate{
|
2024-07-25 05:34:22 +02:00
|
|
|
Settings: w.settingsData,
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
2024-07-25 05:34:22 +02:00
|
|
|
w.broadcast(message)
|
2024-06-20 08:59:41 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) Close() {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
if w.watcher != nil {
|
|
|
|
w.watcher.Close()
|
|
|
|
w.watcher = nil
|
|
|
|
log.Println("file watcher closed")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) broadcast(message WatcherUpdate) {
|
|
|
|
// send to frontend
|
|
|
|
eventbus.SendEvent(eventbus.WSEventType{
|
2024-06-25 04:04:08 +02:00
|
|
|
EventType: eventbus.WSEvent_Config,
|
2024-06-20 08:59:41 +02:00
|
|
|
Data: message,
|
|
|
|
})
|
|
|
|
|
|
|
|
if message.Error != "" {
|
2024-07-25 05:34:22 +02:00
|
|
|
log.Printf("watcher: error processing update: %v. error: %s", message.Settings, message.Error)
|
2024-06-20 08:59:41 +02:00
|
|
|
} else {
|
2024-07-25 05:34:22 +02:00
|
|
|
log.Printf("watcher: update: %v", message.Settings)
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) GetSettingsConfig() SettingsConfigType {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
return w.settingsData
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
fileName := filepath.ToSlash(event.Name)
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
if isInDirectory(fileName, termThemesDirAbsPath) {
|
|
|
|
w.handleTermThemesEvent(event, fileName)
|
|
|
|
} else if filepath.Base(fileName) == filepath.Base(settingsAbsPath) {
|
|
|
|
w.handleSettingsFileEvent(event, fileName)
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
2024-07-25 05:34:22 +02:00
|
|
|
}
|
2024-06-20 08:59:41 +02:00
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
func (w *Watcher) handleTermThemesEvent(event fsnotify.Event, fileName string) {
|
|
|
|
settings, err := LoadFullSettings()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("error loading settings after term-themes event: %v", err)
|
|
|
|
return
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
2024-07-25 05:34:22 +02:00
|
|
|
w.settingsData = *settings
|
|
|
|
w.broadcast(WatcherUpdate{Settings: w.settingsData})
|
|
|
|
}
|
2024-06-20 08:59:41 +02:00
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
func (w *Watcher) handleSettingsFileEvent(event fsnotify.Event, fileName string) {
|
|
|
|
settings, err := LoadFullSettings()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("error loading settings after settings file event: %v", err)
|
|
|
|
return
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
2024-07-25 05:34:22 +02:00
|
|
|
w.settingsData = *settings
|
|
|
|
w.broadcast(WatcherUpdate{Settings: w.settingsData})
|
|
|
|
}
|
|
|
|
|
|
|
|
func getThemeName(fileName string) string {
|
|
|
|
return strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName))
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
|
|
|
|
current := w.GetSettingsConfig()
|
|
|
|
current.Widgets = append(current.Widgets, newWidget)
|
|
|
|
update, err := json.Marshal(current)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
os.MkdirAll(filepath.Dir(settingsFile), 0751)
|
|
|
|
return os.WriteFile(settingsFile, update, 0644)
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) RmWidget(idx uint) error {
|
|
|
|
current := w.GetSettingsConfig().Widgets
|
|
|
|
truncated := append(current[:idx], current[idx+1:]...)
|
|
|
|
update, err := json.Marshal(truncated)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
os.MkdirAll(filepath.Dir(settingsFile), 0751)
|
|
|
|
return os.WriteFile(settingsFile, update, 0644)
|
2024-06-20 08:59:41 +02:00
|
|
|
}
|