From 26c378a8209e788bc1e5e6f879618cf7f9ee3de4 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:33:35 +0800 Subject: [PATCH] :art: Unified file listener logic (#17134) --- kernel/api/ui.go | 2 +- kernel/main.go | 1 + kernel/model/appearance.go | 186 +++++++++----------------- kernel/model/assets_watcher.go | 31 +++-- kernel/model/assets_watcher_darwin.go | 37 ++--- kernel/model/bazzar.go | 9 +- kernel/model/emojis_watcher.go | 33 +++-- kernel/model/emojis_watcher_darwin.go | 37 ++--- kernel/model/themes_watcher.go | 161 ++++++++++++++++++++++ kernel/model/themes_watcher_darwin.go | 156 +++++++++++++++++++++ 10 files changed, 462 insertions(+), 191 deletions(-) create mode 100644 kernel/model/themes_watcher.go create mode 100644 kernel/model/themes_watcher_darwin.go diff --git a/kernel/api/ui.go b/kernel/api/ui.go index dcbec4ebe..8a4d6302a 100644 --- a/kernel/api/ui.go +++ b/kernel/api/ui.go @@ -76,5 +76,5 @@ func reloadIcon(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) - model.ReloadIcon() + model.LoadIcons() } diff --git a/kernel/main.go b/kernel/main.go index c010d9f78..466b9ddb2 100644 --- a/kernel/main.go +++ b/kernel/main.go @@ -55,5 +55,6 @@ func main() { model.WatchAssets() model.WatchEmojis() + model.WatchThemes() model.HandleSignal() } diff --git a/kernel/model/appearance.go b/kernel/model/appearance.go index 2751ddd01..60da7e0f1 100644 --- a/kernel/model/appearance.go +++ b/kernel/model/appearance.go @@ -21,11 +21,9 @@ import ( "os" "path/filepath" "strings" - "sync" "time" "github.com/88250/gulu" - "github.com/fsnotify/fsnotify" "github.com/siyuan-note/filelock" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/bazaar" @@ -41,15 +39,17 @@ func InitAppearance() { return } - unloadThemes() from := filepath.Join(util.WorkingDir, "appearance") if err := filelock.Copy(from, util.AppearancePath); err != nil { logging.LogErrorf("copy appearance resources from [%s] to [%s] failed: %s", from, util.AppearancePath, err) util.ReportFileSysFatalError(err) return } - loadThemes() + loadThemes() + LoadIcons() + + Conf.m.Lock() if !containTheme(Conf.Appearance.ThemeDark, Conf.Appearance.DarkThemes) { Conf.Appearance.ThemeDark = "midnight" Conf.Appearance.ThemeJS = false @@ -58,11 +58,10 @@ func InitAppearance() { Conf.Appearance.ThemeLight = "daylight" Conf.Appearance.ThemeJS = false } - - loadIcons() if !gulu.Str.Contains(Conf.Appearance.Icon, Conf.Appearance.Icons) { Conf.Appearance.Icon = "material" } + Conf.m.Unlock() Conf.Save() @@ -78,36 +77,6 @@ func containTheme(name string, themes []*conf.AppearanceTheme) bool { return false } -var themeWatchers = sync.Map{} // [string]*fsnotify.Watcher{} - -func closeThemeWatchers() { - themeWatchers.Range(func(key, value interface{}) bool { - if err := value.(*fsnotify.Watcher).Close(); err != nil { - logging.LogErrorf("close file watcher failed: %s", err) - } - return true - }) -} - -func unloadThemes() { - if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) { - return - } - - themeDirs, err := os.ReadDir(util.ThemesPath) - if err != nil { - logging.LogErrorf("read appearance themes folder failed: %s", err) - return - } - - for _, themeDir := range themeDirs { - if !util.IsDirRegularOrSymlink(themeDir) { - continue - } - unwatchTheme(filepath.Join(util.ThemesPath, themeDir.Name())) - } -} - func loadThemes() { themeDirs, err := os.ReadDir(util.ThemesPath) if err != nil { @@ -116,9 +85,13 @@ func loadThemes() { return } - Conf.Appearance.DarkThemes = nil - Conf.Appearance.LightThemes = nil + var darkThemes, lightThemes []*conf.AppearanceTheme var daylightTheme, midnightTheme *conf.AppearanceTheme + var themeVer string + var themeJS bool + mode := Conf.Appearance.Mode + themeLight := Conf.Appearance.ThemeLight + themeDark := Conf.Appearance.ThemeDark for _, themeDir := range themeDirs { if !util.IsDirRegularOrSymlink(themeDir) { continue @@ -129,10 +102,9 @@ func loadThemes() { continue } - modes := themeConf.Modes - for _, mode := range modes { + for _, mode := range themeConf.Modes { t := &conf.AppearanceTheme{Name: name} - if "midnight" == name || "daylight" == name { + if isBuiltInTheme(name) { t.Label = name + Conf.Language(281) } else { t.Label = name @@ -156,32 +128,37 @@ func loadThemes() { } if "dark" == mode { - Conf.Appearance.DarkThemes = append(Conf.Appearance.DarkThemes, t) + darkThemes = append(darkThemes, t) } else if "light" == mode { - Conf.Appearance.LightThemes = append(Conf.Appearance.LightThemes, t) + lightThemes = append(lightThemes, t) } } - if 0 == Conf.Appearance.Mode { - if Conf.Appearance.ThemeLight == name { - Conf.Appearance.ThemeVer = themeConf.Version - Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js")) + if 0 == mode { + if themeLight == name { + themeVer = themeConf.Version + themeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js")) } } else { - if Conf.Appearance.ThemeDark == name { - Conf.Appearance.ThemeVer = themeConf.Version - Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js")) + if themeDark == name { + themeVer = themeConf.Version + themeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js")) } } - - go watchTheme(filepath.Join(util.ThemesPath, name)) } - Conf.Appearance.LightThemes = append([]*conf.AppearanceTheme{daylightTheme}, Conf.Appearance.LightThemes...) - Conf.Appearance.DarkThemes = append([]*conf.AppearanceTheme{midnightTheme}, Conf.Appearance.DarkThemes...) + lightThemes = append([]*conf.AppearanceTheme{daylightTheme}, lightThemes...) + darkThemes = append([]*conf.AppearanceTheme{midnightTheme}, darkThemes...) + + Conf.m.Lock() + Conf.Appearance.DarkThemes = darkThemes + Conf.Appearance.LightThemes = lightThemes + Conf.Appearance.ThemeVer = themeVer + Conf.Appearance.ThemeJS = themeJS + Conf.m.Unlock() } -func loadIcons() { +func LoadIcons() { iconDirs, err := os.ReadDir(util.IconsPath) if err != nil { logging.LogErrorf("read appearance icons folder failed: %s", err) @@ -189,7 +166,9 @@ func loadIcons() { return } - Conf.Appearance.Icons = nil + var icons []string + var iconVer string + currentIcon := Conf.Appearance.Icon for _, iconDir := range iconDirs { if !util.IsDirRegularOrSymlink(iconDir) { continue @@ -199,77 +178,15 @@ func loadIcons() { if err != nil || nil == iconConf { continue } - Conf.Appearance.Icons = append(Conf.Appearance.Icons, name) - if Conf.Appearance.Icon == name { - Conf.Appearance.IconVer = iconConf.Version + icons = append(icons, name) + if currentIcon == name { + iconVer = iconConf.Version } } -} - -func ReloadIcon() { - loadIcons() -} - -func unwatchTheme(folder string) { - val, _ := themeWatchers.Load(folder) - if nil != val { - themeWatcher := val.(*fsnotify.Watcher) - themeWatcher.Close() - } -} - -func watchTheme(folder string) { - val, _ := themeWatchers.Load(folder) - var themeWatcher *fsnotify.Watcher - if nil != val { - themeWatcher = val.(*fsnotify.Watcher) - themeWatcher.Close() - } - - var err error - if themeWatcher, err = fsnotify.NewWatcher(); err != nil { - logging.LogErrorf("add theme file watcher for folder [%s] failed: %s", folder, err) - return - } - themeWatchers.Store(folder, themeWatcher) - - done := make(chan bool) - go func() { - for { - select { - case event, ok := <-themeWatcher.Events: - if !ok { - return - } - - //logging.LogInfof(event.String()) - if event.Op&fsnotify.Write == fsnotify.Write && (strings.HasSuffix(event.Name, "theme.css")) { - var themeName string - if themeName = isCurrentUseTheme(event.Name); "" == themeName { - break - } - - if strings.HasSuffix(event.Name, "theme.css") { - util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{ - "theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()), - }) - break - } - } - case err, ok := <-themeWatcher.Errors: - if !ok { - return - } - logging.LogErrorf("watch theme file failed: %s", err) - } - } - }() - - //logging.LogInfof("add file watcher [%s]", folder) - if err := themeWatcher.Add(folder); err != nil { - logging.LogErrorf("add theme files watcher for folder [%s] failed: %s", folder, err) - } - <-done + Conf.m.Lock() + Conf.Appearance.Icons = icons + Conf.Appearance.IconVer = iconVer + Conf.m.Unlock() } func isCurrentUseTheme(themePath string) string { @@ -285,3 +202,22 @@ func isCurrentUseTheme(themePath string) string { } return "" } + +func broadcastRefreshThemeIfCurrent(themeCssPath string) { + if !strings.HasSuffix(themeCssPath, "theme.css") { + return + } + // 只处理主题根目录中的 theme.css + themeDir := filepath.Clean(filepath.Dir(themeCssPath)) + themesRoot := filepath.Clean(util.ThemesPath) + if themeDir != filepath.Join(themesRoot, filepath.Base(themeDir)) { + return + } + themeName := isCurrentUseTheme(themeCssPath) + if themeName == "" { + return + } + util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{ + "theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()), + }) +} diff --git a/kernel/model/assets_watcher.go b/kernel/model/assets_watcher.go index 8adff63e5..781c2d041 100644 --- a/kernel/model/assets_watcher.go +++ b/kernel/model/assets_watcher.go @@ -37,23 +37,30 @@ func WatchAssets() { return } - go func() { - watchAssets() - }() + go watchAssets() } func watchAssets() { + CloseWatchAssets() assetsDir := filepath.Join(util.DataDir, "assets") - if nil != assetsWatcher { - assetsWatcher.Close() - } var err error - if assetsWatcher, err = fsnotify.NewWatcher(); err != nil { + assetsWatcher, err = fsnotify.NewWatcher() + if err != nil { logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err) return } + if !gulu.File.IsDir(assetsDir) { + os.MkdirAll(assetsDir, 0755) + } + + if err = assetsWatcher.Add(assetsDir); err != nil { + logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err) + CloseWatchAssets() + return + } + go func() { defer logging.Recover() @@ -95,19 +102,11 @@ func watchAssets() { } } }() - - if !gulu.File.IsDir(assetsDir) { - os.MkdirAll(assetsDir, 0755) - } - - if err = assetsWatcher.Add(assetsDir); err != nil { - logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err) - } - //logging.LogInfof("added file watcher [%s]", assetsDir) } func CloseWatchAssets() { if nil != assetsWatcher { assetsWatcher.Close() + assetsWatcher = nil } } diff --git a/kernel/model/assets_watcher_darwin.go b/kernel/model/assets_watcher_darwin.go index 32e620e22..8d355af35 100644 --- a/kernel/model/assets_watcher_darwin.go +++ b/kernel/model/assets_watcher_darwin.go @@ -19,9 +19,11 @@ package model import ( + "os" "path/filepath" "time" + "github.com/88250/gulu" "github.com/radovskyb/watcher" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/cache" @@ -31,20 +33,31 @@ import ( var assetsWatcher *watcher.Watcher func WatchAssets() { - go func() { - watchAssets() - }() + if !isFileWatcherAvailable() { + return + } + + go watchAssets() } func watchAssets() { - if nil != assetsWatcher { - assetsWatcher.Close() - } - assetsWatcher = watcher.New() - + CloseWatchAssets() assetsDir := filepath.Join(util.DataDir, "assets") + assetsWatcher = watcher.New() + + if !gulu.File.IsDir(assetsDir) { + os.MkdirAll(assetsDir, 0755) + } + + if err := assetsWatcher.Add(assetsDir); err != nil { + logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err) + return + } + go func() { + defer logging.Recover() + for { select { case event, ok := <-assetsWatcher.Event: @@ -75,13 +88,6 @@ func watchAssets() { } } }() - - if err := assetsWatcher.Add(assetsDir); err != nil { - logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err) - return - } - - //logging.LogInfof("added file watcher [%s]", assetsDir) if err := assetsWatcher.Start(10 * time.Second); err != nil { logging.LogErrorf("start assets watcher for folder [%s] failed: %s", assetsDir, err) return @@ -91,5 +97,6 @@ func watchAssets() { func CloseWatchAssets() { if nil != assetsWatcher { assetsWatcher.Close() + assetsWatcher = nil } } diff --git a/kernel/model/bazzar.go b/kernel/model/bazzar.go index c873e5dc0..ac504919a 100644 --- a/kernel/model/bazzar.go +++ b/kernel/model/bazzar.go @@ -432,7 +432,7 @@ func InstalledThemes(keyword string) (ret []*bazaar.Theme) { } func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error { - closeThemeWatchers() + CloseWatchThemes() installPath := filepath.Join(util.ThemesPath, themeName) err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.ID) @@ -458,7 +458,7 @@ func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bo } func UninstallBazaarTheme(themeName string) error { - closeThemeWatchers() + CloseWatchThemes() installPath := filepath.Join(util.ThemesPath, themeName) err := bazaar.UninstallTheme(installPath) @@ -580,3 +580,8 @@ func getSearchKeywords(query string) (ret []string) { } return } + +// isBuiltInTheme 通过包名或目录名判断是否为内置主题 +func isBuiltInTheme(name string) bool { + return "daylight" == name || "midnight" == name +} diff --git a/kernel/model/emojis_watcher.go b/kernel/model/emojis_watcher.go index 8b23fd479..2974e2954 100644 --- a/kernel/model/emojis_watcher.go +++ b/kernel/model/emojis_watcher.go @@ -32,27 +32,34 @@ import ( var emojisWatcher *fsnotify.Watcher func WatchEmojis() { - if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container { + if !isFileWatcherAvailable() { return } - go func() { - watchEmojis() - }() + go watchEmojis() } func watchEmojis() { + CloseWatchEmojis() emojisDir := filepath.Join(util.DataDir, "emojis") - if nil != emojisWatcher { - emojisWatcher.Close() - } var err error - if emojisWatcher, err = fsnotify.NewWatcher(); err != nil { + emojisWatcher, err = fsnotify.NewWatcher() + if err != nil { logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err) return } + if !gulu.File.IsDir(emojisDir) { + os.MkdirAll(emojisDir, 0755) + } + + if err = emojisWatcher.Add(emojisDir); err != nil { + logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err) + CloseWatchEmojis() + return + } + go func() { defer logging.Recover() @@ -77,19 +84,11 @@ func watchEmojis() { } } }() - - if !gulu.File.IsDir(emojisDir) { - os.MkdirAll(emojisDir, 0755) - } - - if err = emojisWatcher.Add(emojisDir); err != nil { - logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err) - } - //logging.LogInfof("added file watcher [%s]", emojisDir) } func CloseWatchEmojis() { if nil != emojisWatcher { emojisWatcher.Close() + emojisWatcher = nil } } diff --git a/kernel/model/emojis_watcher_darwin.go b/kernel/model/emojis_watcher_darwin.go index 652b3c917..419180dfe 100644 --- a/kernel/model/emojis_watcher_darwin.go +++ b/kernel/model/emojis_watcher_darwin.go @@ -19,9 +19,11 @@ package model import ( + "os" "path/filepath" "time" + "github.com/88250/gulu" "github.com/radovskyb/watcher" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/util" @@ -30,20 +32,31 @@ import ( var emojisWatcher *watcher.Watcher func WatchEmojis() { - go func() { - watchEmojis() - }() + if !isFileWatcherAvailable() { + return + } + + go watchEmojis() } func watchEmojis() { - if nil != emojisWatcher { - emojisWatcher.Close() - } - emojisWatcher = watcher.New() - emojisDir := filepath.Join(util.DataDir, "emojis") + CloseWatchEmojis() + emojisWatcher = watcher.New() + + if !gulu.File.IsDir(emojisDir) { + os.MkdirAll(emojisDir, 0755) + } + + if err := emojisWatcher.Add(emojisDir); err != nil { + logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err) + return + } + go func() { + defer logging.Recover() + for { select { case _, ok := <-emojisWatcher.Event: @@ -61,13 +74,6 @@ func watchEmojis() { } } }() - - if err := emojisWatcher.Add(emojisDir); err != nil { - logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err) - return - } - - //logging.LogInfof("added file watcher [%s]", emojisDir) if err := emojisWatcher.Start(10 * time.Second); err != nil { logging.LogErrorf("start emojis watcher for folder [%s] failed: %s", emojisDir, err) return @@ -77,5 +83,6 @@ func watchEmojis() { func CloseWatchEmojis() { if nil != emojisWatcher { emojisWatcher.Close() + emojisWatcher = nil } } diff --git a/kernel/model/themes_watcher.go b/kernel/model/themes_watcher.go new file mode 100644 index 000000000..2e98dc78d --- /dev/null +++ b/kernel/model/themes_watcher.go @@ -0,0 +1,161 @@ +// SiYuan - Refactor your thinking +// Copyright (c) 2020-present, b3log.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build !darwin + +package model + +import ( + "os" + "path/filepath" + "time" + + "github.com/88250/gulu" + "github.com/fsnotify/fsnotify" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" +) + +var themesWatcher *fsnotify.Watcher + +func WatchThemes() { + if !isFileWatcherAvailable() { + return + } + + go watchThemes() +} + +func watchThemes() { + CloseWatchThemes() + themesDir := util.ThemesPath + + var err error + themesWatcher, err = fsnotify.NewWatcher() + if err != nil { + logging.LogErrorf("add themes watcher for folder [%s] failed: %s", themesDir, err) + return + } + + if !gulu.File.IsDir(themesDir) { + os.MkdirAll(themesDir, 0755) + } + + if err = themesWatcher.Add(themesDir); err != nil { + logging.LogErrorf("add themes root watcher for folder [%s] failed: %s", themesDir, err) + CloseWatchThemes() + return + } + + // 为每个子目录添加监听,以便收到 theme.css 的变更 + addThemesSubdirs(themesWatcher, themesDir) + + go func() { + defer logging.Recover() + + var ( + timer *time.Timer + lastEvent fsnotify.Event + ) + timer = time.NewTimer(100 * time.Millisecond) + <-timer.C // timer should be expired at first + + for { + select { + case event, ok := <-themesWatcher.Events: + if !ok { + return + } + + // 新目录创建时加入监听 + if event.Op&fsnotify.Create == fsnotify.Create { + if isThemesDirectSubdir(event.Name) { + if addErr := themesWatcher.Add(event.Name); addErr != nil { + logging.LogWarnf("add themes watcher for new folder [%s] failed: %s", event.Name, addErr) + } + } + } + + lastEvent = event + timer.Reset(time.Millisecond * 100) + case err, ok := <-themesWatcher.Errors: + if !ok { + return + } + logging.LogErrorf("watch themes failed: %s", err) + case <-timer.C: + handleThemesEvent(lastEvent) + } + } + }() +} + +// addThemesSubdirs 为 themes 下每个子目录添加监听 +func addThemesSubdirs(w *fsnotify.Watcher, themesDir string) { + entries, err := os.ReadDir(themesDir) + if err != nil { + logging.LogErrorf("read themes folder failed: %s", err) + return + } + for _, e := range entries { + if !util.IsDirRegularOrSymlink(e) { + continue + } + subdir := filepath.Join(themesDir, e.Name()) + if addErr := w.Add(subdir); addErr != nil { + logging.LogWarnf("add themes watcher for folder [%s] failed: %s", subdir, addErr) + } + } +} + +// isThemesDirectSubdir 判断 path 是否为 themes 下的直接子目录 +func isThemesDirectSubdir(path string) bool { + if !gulu.File.IsDir(path) { + return false + } + rel, err := filepath.Rel(util.ThemesPath, path) + if err != nil { + return false + } + if filepath.Base(path) != rel { + return false + } + entries, err := os.ReadDir(util.ThemesPath) + if err != nil { + return false + } + name := filepath.Base(path) + for _, e := range entries { + if e.Name() == name { + return util.IsDirRegularOrSymlink(e) + } + } + return false +} + +func handleThemesEvent(event fsnotify.Event) { + if event.Op&fsnotify.Write != fsnotify.Write { + return + } + broadcastRefreshThemeIfCurrent(event.Name) +} + +func CloseWatchThemes() { + if nil != themesWatcher { + themesWatcher.Close() + themesWatcher = nil + } +} diff --git a/kernel/model/themes_watcher_darwin.go b/kernel/model/themes_watcher_darwin.go new file mode 100644 index 000000000..332b96030 --- /dev/null +++ b/kernel/model/themes_watcher_darwin.go @@ -0,0 +1,156 @@ +// SiYuan - Refactor your thinking +// Copyright (c) 2020-present, b3log.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build darwin + +package model + +import ( + "os" + "path/filepath" + "strings" + "time" + + "github.com/88250/gulu" + "github.com/radovskyb/watcher" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" +) + +var themesWatcher *watcher.Watcher + +func WatchThemes() { + if !isFileWatcherAvailable() { + return + } + + go watchThemes() +} + +func watchThemes() { + CloseWatchThemes() + themesDir := util.ThemesPath + + themesWatcher = watcher.New() + + if !gulu.File.IsDir(themesDir) { + os.MkdirAll(themesDir, 0755) + } + + if err := themesWatcher.Add(themesDir); err != nil { + logging.LogErrorf("add themes watcher for folder [%s] failed: %s", themesDir, err) + return + } + + // 为每个子目录添加监听,以便收到 theme.css 的变更 + addThemesSubdirs(themesWatcher, themesDir) + + go func() { + defer logging.Recover() + + for { + select { + case event, ok := <-themesWatcher.Event: + if !ok { + return + } + + // 新目录创建时加入监听 + if watcher.Create == event.Op { + if isThemesDirectSubdir(event.Path) { + if addErr := themesWatcher.Add(event.Path); addErr != nil { + logging.LogWarnf("add themes watcher for new folder [%s] failed: %s", event.Path, addErr) + } + } + } + + handleThemesEvent(event) + case err, ok := <-themesWatcher.Error: + if !ok { + return + } + logging.LogErrorf("watch themes failed: %s", err) + case <-themesWatcher.Closed: + return + } + } + }() + + if err := themesWatcher.Start(10 * time.Second); err != nil { + logging.LogErrorf("start themes watcher for folder [%s] failed: %s", themesDir, err) + return + } +} + +// addThemesSubdirs 为 themes 下每个子目录添加监听 +func addThemesSubdirs(w *watcher.Watcher, themesDir string) { + entries, err := os.ReadDir(themesDir) + if err != nil { + logging.LogErrorf("read themes folder failed: %s", err) + return + } + for _, e := range entries { + if !util.IsDirRegularOrSymlink(e) { + continue + } + subdir := filepath.Join(themesDir, e.Name()) + if addErr := w.Add(subdir); addErr != nil { + logging.LogWarnf("add themes watcher for folder [%s] failed: %s", subdir, addErr) + } + } +} + +// isThemesDirectSubdir 判断 path 是否为 themes 下的直接子目录 +func isThemesDirectSubdir(path string) bool { + if !gulu.File.IsDir(path) { + return false + } + rel, err := filepath.Rel(util.ThemesPath, path) + if err != nil { + return false + } + if filepath.Base(path) != rel { + return false + } + entries, err := os.ReadDir(util.ThemesPath) + if err != nil { + return false + } + name := filepath.Base(path) + for _, e := range entries { + if e.Name() == name { + return util.IsDirRegularOrSymlink(e) + } + } + return false +} + +func handleThemesEvent(event watcher.Event) { + if watcher.Write != event.Op { + return + } + if !strings.HasSuffix(event.Path, "theme.css") { + return + } + broadcastRefreshThemeIfCurrent(event.Path) +} + +func CloseWatchThemes() { + if nil != themesWatcher { + themesWatcher.Close() + themesWatcher = nil + } +}