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
+ }
+}