From 842ce63eda42a5dfa1599bf69b5ea22b332a954d Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 2 Apr 2026 22:21:22 +0300 Subject: task 004: replace time.Sleep goroutine synchronization with proper primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/gui/app.go | 18 ++++++++---------- internal/gui/audio_player.go | 14 +++++--------- internal/gui/navigation.go | 28 +++++++++++++++++++++------- 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) } -- cgit v1.2.3