🎨 Unified file listener logic (#17134)

This commit is contained in:
Jeffrey Chen 2026-03-04 22:33:35 +08:00 committed by GitHub
parent ba733eedfd
commit 26c378a820
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 462 additions and 191 deletions

View file

@ -76,5 +76,5 @@ func reloadIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
model.ReloadIcon()
model.LoadIcons()
}

View file

@ -55,5 +55,6 @@ func main() {
model.WatchAssets()
model.WatchEmojis()
model.WatchThemes()
model.HandleSignal()
}

View file

@ -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()),
})
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
//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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
//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
}
}