- Dynamic miller column sizing with flexbox layout - Root column (160px), middle slices (48px), active column (flex) - Auto-scroll browser to rightmost column on navigation - Offline indicator UI and IndexedDB cache integration - /api/documents endpoint for client-side document caching - Breadcrumb navigation in document content area - FTS5 search with SQLite virtual table - Client-side Mermaid and KaTeX rendering via CDN - Deep sample content (advanced-topics/raft-deep-dive) - Border removal (only search button keeps radius) - Full viewport layout with proper borders between sidebar/content
102 lines
2.0 KiB
Go
102 lines
2.0 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
)
|
|
|
|
type FileWatcher struct {
|
|
watcher *fsnotify.Watcher
|
|
rootDir string
|
|
onChange func(path string)
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewFileWatcher(rootDir string, logger *slog.Logger) (*FileWatcher, error) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create fsnotify watcher: %w", err)
|
|
}
|
|
|
|
fw := &FileWatcher{
|
|
watcher: watcher,
|
|
rootDir: rootDir,
|
|
logger: logger,
|
|
}
|
|
|
|
if err := fw.addWatches(); err != nil {
|
|
watcher.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return fw, nil
|
|
}
|
|
|
|
func (fw *FileWatcher) addWatches() error {
|
|
return filepath.Walk(fw.rootDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
if err := fw.watcher.Add(path); err != nil {
|
|
fw.logger.Warn("watch directory", "path", path, "error", err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (fw *FileWatcher) SetOnChange(fn func(path string)) {
|
|
fw.onChange = fn
|
|
}
|
|
|
|
func (fw *FileWatcher) Run(ctx context.Context) {
|
|
defer fw.watcher.Close()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case event, ok := <-fw.watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if fw.shouldProcess(event) {
|
|
if event.Op&fsnotify.Create == fsnotify.Create {
|
|
if info, err := os.Stat(event.Name); err == nil && info.IsDir() {
|
|
fw.watcher.Add(event.Name)
|
|
}
|
|
}
|
|
if fw.onChange != nil {
|
|
fw.onChange(event.Name)
|
|
}
|
|
}
|
|
case err, ok := <-fw.watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
fw.logger.Warn("fsnotify error", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fw *FileWatcher) shouldProcess(event fsnotify.Event) bool {
|
|
if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) == 0 {
|
|
return false
|
|
}
|
|
name := filepath.Base(event.Name)
|
|
if strings.HasPrefix(name, ".") || strings.HasSuffix(name, "~") {
|
|
return false
|
|
}
|
|
if filepath.Ext(event.Name) == ".md" || event.Op&fsnotify.Remove != 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|