summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-02 22:21:22 +0300
committerPaul Buetow <paul@buetow.org>2026-04-02 22:21:22 +0300
commit842ce63eda42a5dfa1599bf69b5ea22b332a954d (patch)
treedd95130457d654ee2bc363b345c8d7f0d501379a
parent373a10660bdd1faf27d22073380e5e897870ab12 (diff)
task 004: replace time.Sleep goroutine synchronization with proper primitives
audio_player.go: use time.AfterFunc instead of goroutine+sleep for the 100ms auto-play delay — no goroutine is blocked waiting. app.go (setupUI, setupTooltips): replace goroutine+sleep with time.AfterFunc for both 500ms tooltip initialization delays. navigation.go: replace fixed 500ms sleep in the delete-cleanup goroutine with a ticker+select loop that polls hasActiveOperations(). The goroutine now proceeds as soon as all in-flight operations for the deleted word finish (or after a 5-second safety timeout), rather than waiting a fixed interval that may be too short or too long. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--internal/gui/app.go18
-rw-r--r--internal/gui/audio_player.go14
-rw-r--r--internal/gui/navigation.go28
3 files changed, 34 insertions, 26 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go
index 666c763..4f8311e 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -555,9 +555,9 @@ func (a *Application) setupUI() {
// Now that tooltip layer is created, set all tooltips
a.setupTooltips()
- // Set tooltips for export, archive and help buttons with a delay
- go func() {
- time.Sleep(500 * time.Millisecond)
+ // Set tooltips for export, archive and help buttons after the tooltip layer
+ // has had time to initialize. AfterFunc avoids blocking a goroutine.
+ time.AfterFunc(500*time.Millisecond, func() {
fyne.Do(func() {
if exportButton != nil {
exportButton.SetToolTip("Export to Anki (x)")
@@ -569,7 +569,7 @@ func (a *Application) setupUI() {
helpButton.SetToolTip("Show hotkeys (?)")
}
})
- }()
+ })
a.window.SetOnClosed(func() {
// Stop file check ticker
@@ -1962,12 +1962,10 @@ func (a *Application) clearUI() {
a.setActionButtonsEnabled(false)
}
-// setupTooltips sets up all tooltips after the tooltip layer has been created
+// setupTooltips sets up all tooltips after the tooltip layer has been created.
+// AfterFunc fires after the tooltip layer is initialized without blocking a goroutine.
func (a *Application) setupTooltips() {
- // Use a goroutine with a delay to ensure the tooltip layer is fully initialized
- go func() {
- time.Sleep(500 * time.Millisecond)
-
+ time.AfterFunc(500*time.Millisecond, func() {
fyne.Do(func() {
// Navigation button tooltips
if a.submitButton != nil {
@@ -2014,7 +2012,7 @@ func (a *Application) setupTooltips() {
a.audioPlayer.stopButton.SetToolTip("Stop audio")
}
})
- }()
+ })
}
// processNextInQueue processes the next word in the queue
diff --git a/internal/gui/audio_player.go b/internal/gui/audio_player.go
index eb96430..1f599d9 100644
--- a/internal/gui/audio_player.go
+++ b/internal/gui/audio_player.go
@@ -156,16 +156,12 @@ func (p *AudioPlayer) setAudioFileInternal(audioFile string, allowAutoPlay bool)
statusText := fmt.Sprintf("Audio: %s%s", filepath.Base(audioFile), p.voiceInfo)
p.statusLabel.SetText(statusText)
- // Auto-play if enabled and allowed
+ // Auto-play if enabled and allowed. AfterFunc fires the callback after
+ // the UI has had a chance to render without blocking a goroutine.
if allowAutoPlay && p.autoPlayEnabled != nil && *p.autoPlayEnabled {
- // Small delay to ensure UI is ready
- go func() {
- // Wait a tiny bit for UI to be ready
- time.Sleep(100 * time.Millisecond)
- fyne.Do(func() {
- p.onPlay()
- })
- }()
+ time.AfterFunc(100*time.Millisecond, func() {
+ fyne.Do(p.onPlay)
+ })
}
} else {
p.Clear()
diff --git a/internal/gui/navigation.go b/internal/gui/navigation.go
index 1cd6089..206da00 100644
--- a/internal/gui/navigation.go
+++ b/internal/gui/navigation.go
@@ -827,19 +827,33 @@ func (a *Application) deleteCurrentWord() {
a.deleteButton.Enable()
}
- // Start a cleanup goroutine to remove directory after any pending operations complete
+ // Start a cleanup goroutine to guard against directory recreation by racing
+ // in-flight operations. Instead of a fixed sleep, poll hasActiveOperations
+ // so the cleanup runs as soon as all operations for this word complete.
go func() {
- // Wait a bit for any ongoing operations to notice cancellation
- time.Sleep(500 * time.Millisecond)
+ ticker := time.NewTicker(50 * time.Millisecond)
+ defer ticker.Stop()
+ timeout := time.NewTimer(5 * time.Second)
+ defer timeout.Stop()
- // Check if the directory was somehow recreated (by a racing operation)
+ // Wait until all active operations for this word finish or timeout elapses.
+ for {
+ select {
+ case <-timeout.C:
+ // Proceed even if some operations are still pending.
+ case <-ticker.C:
+ if a.hasActiveOperations(deletedWord) {
+ continue
+ }
+ }
+ break
+ }
+
+ // Check if a racing operation recreated the directory.
recreatedDir := a.findCardDirectory(deletedWord)
if recreatedDir != "" {
- // Directory was recreated, try to delete it again
timestamp := time.Now().Format("20060102_150405")
trashWordDir := filepath.Join(trashDir, fmt.Sprintf("%s_%s_cleanup", filepath.Base(recreatedDir), timestamp))
-
- // Move to trash again
if err := os.Rename(recreatedDir, trashWordDir); err == nil {
fmt.Printf("Cleanup: moved recreated directory for '%s' to trash\n", deletedWord)
}