Skip to content

Commit e41f9aa

Browse files
fix(macOS): Rate-limit UI tuning loop and revert double-buffering (#139)
1 parent 841346a commit e41f9aa

4 files changed

Lines changed: 34 additions & 117 deletions

File tree

pkg/curation/zz_generated_provider_map.go

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/wallpaper/double_buffer_test.go

Lines changed: 0 additions & 67 deletions
This file was deleted.

pkg/wallpaper/monitor_controller.go

Lines changed: 20 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"math/rand"
77
"os"
88
"path/filepath"
9-
"runtime"
10-
"strings"
119
"sync"
1210
"time"
1311

@@ -546,27 +544,6 @@ func (mc *MonitorController) applyImage(img provider.Image) {
546544
}
547545
}
548546

549-
// nextDerivativePath returns the next file path for the double buffering system on macOS.
550-
// For non-macOS systems, it returns the current path.
551-
func nextDerivativePath(currentPath string, targetOS string) string {
552-
if targetOS != "darwin" {
553-
return currentPath
554-
}
555-
dir := filepath.Dir(currentPath)
556-
base := filepath.Base(currentPath)
557-
ext := filepath.Ext(base)
558-
nameWithoutExt := strings.TrimSuffix(base, ext)
559-
560-
if strings.HasSuffix(nameWithoutExt, "_A") {
561-
nameWithoutExt = strings.TrimSuffix(nameWithoutExt, "_A") + "_B"
562-
} else if strings.HasSuffix(nameWithoutExt, "_B") {
563-
nameWithoutExt = strings.TrimSuffix(nameWithoutExt, "_B") + "_A"
564-
} else {
565-
nameWithoutExt = nameWithoutExt + "_A"
566-
}
567-
return filepath.Join(dir, nameWithoutExt+ext)
568-
}
569-
570547
// reprocessWithTuning updates the tuning options on the current image, re-runs FitImage,
571548
// and sets the resulting derivative as the wallpaper.
572549
func (mc *MonitorController) reprocessWithTuning(opts provider.TuningOptions) {
@@ -622,14 +599,28 @@ func (mc *MonitorController) reprocessWithTuning(opts provider.TuningOptions) {
622599
// 4. Re-run FitImage with new tuning
623600
width, height := mc.Monitor.Rect.Dx(), mc.Monitor.Rect.Dy()
624601
ctx := context.Background()
602+
virtualFramed := false
603+
ctx = context.WithValue(ctx, provider.VirtualFramedKey, &virtualFramed)
604+
625605
processedImg, err := mc.processor.FitImage(ctx, srcImg, width, height, opts)
626606
if err != nil {
627607
log.Printf("[ERROR] [Monitor %d] FitImage failed with tuning %v: %v", mc.ID, opts, err)
628608
return
629609
}
630610

631-
// 5. Determine derivative path and overwrite
632611
resKey = fmt.Sprintf("%dx%d", width, height)
612+
613+
// Keep the UI sync state fresh by tracking if VirtualFramer actually framed it
614+
if img.ProcessingFlags == nil {
615+
img.ProcessingFlags = make(map[string]bool)
616+
}
617+
if virtualFramed {
618+
img.ProcessingFlags["VirtualFramed:"+resKey] = true
619+
} else {
620+
delete(img.ProcessingFlags, "VirtualFramed:"+resKey)
621+
}
622+
623+
// 5. Determine derivative path and overwrite
633624
derivPath := ""
634625
if p, ok := img.DerivativePaths[resKey]; ok {
635626
derivPath = p
@@ -645,33 +636,18 @@ func (mc *MonitorController) reprocessWithTuning(opts provider.TuningOptions) {
645636
// inode. On macOS/APFS, imaging.Save → os.Create truncates in-place (same
646637
// inode), and the OS serves stale cached image data. Deleting first ensures
647638
// os.Create allocates a new inode, bypassing all file-level caching.
648-
newDerivPath := nextDerivativePath(derivPath, runtime.GOOS)
639+
os.Remove(derivPath) // Ignore error — file may not exist yet
649640

650-
if err := imaging.Save(processedImg, newDerivPath); err != nil {
641+
if err := imaging.Save(processedImg, derivPath); err != nil {
651642
log.Printf("[ERROR] [Monitor %d] Failed to save derivative: %v", mc.ID, err)
652643
return
653644
}
654-
655-
if newDerivPath != derivPath {
656-
// Safely clean up the old buffer file after successful save
657-
os.Remove(derivPath)
658-
659-
// Update in-memory state and persistent store with new path
660-
if img.DerivativePaths == nil {
661-
img.DerivativePaths = make(map[string]string)
662-
}
663-
img.DerivativePaths[resKey] = newDerivPath
664-
mc.State.CurrentImage = img
665-
mc.Store.SetDerivativePath(img.ID, resKey, newDerivPath)
666-
} else {
667-
// Standard modification time bump for other OSes (if needed by their cache)
668-
now := time.Now()
669-
_ = os.Chtimes(newDerivPath, now, now)
670-
}
645+
now := time.Now()
646+
_ = os.Chtimes(derivPath, now, now)
671647

672648
// 6. Set wallpaper
673649
mc.State.CurrentImage = img
674-
if err := mc.os.SetWallpaper(newDerivPath, mc.ID); err != nil {
650+
if err := mc.os.SetWallpaper(derivPath, mc.ID); err != nil {
675651
log.Printf("[ERROR] [Monitor %d] Failed to set wallpaper: %v", mc.ID, err)
676652
}
677653

pkg/wallpaper/tune_popup.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func (wp *Plugin) showTuneImagePopup(monitorID int) {
108108
}
109109
}
110110

111+
start := time.Now()
111112
wp.SetTuningOptions(monitorID, opts)
112113

113114
// Wait for completion (with timeout to prevent hanging)
@@ -117,6 +118,13 @@ func (wp *Plugin) showTuneImagePopup(monitorID int) {
117118
log.Printf("[WARN] Tuning reprocessing timed out for monitor %d", monitorID)
118119
}
119120

121+
// Enforce a minimum delay to prevent macOS wallpaper daemon choke and UI spam
122+
// macOS NSWorkspace can drop or queue transitions if called too rapidly.
123+
elapsed := time.Since(start)
124+
if elapsed < 500*time.Millisecond {
125+
time.Sleep(500*time.Millisecond - elapsed)
126+
}
127+
120128
if onDone != nil {
121129
onDone()
122130
}

0 commit comments

Comments
 (0)