fix: protect the dirty flag with an atomic.Bool

SetDirty runs on a Wails call goroutine while the GTK close handler
reads the flag, with no synchronisation: an edit landing while a save
finished could make the close confirmation unreliable. The spellMu
comment already documents the goroutine-per-call convention; the flag
that guards against losing work deserves the same care.
This commit is contained in:
Kharec committed 2026-10-15 17:43:56 +02:00
1 parent 643adffb12
commit c0a52a9040
2 files changed
+13 -4

No files matched your search

+5
View File
@@ -7,6 +7,11 @@ version is in the `VERSION` file.
## [Unreleased] ## [Unreleased]
### Fixed
- The unsaved-changes marker is now updated atomically: an edit landing while a save
finished could make the close confirmation unreliable.
## [1.4] - 2026-10-15 ## [1.4] - 2026-10-15
### Fixed ### Fixed
+8 -4
View File
@@ -10,6 +10,7 @@ import (
"slices" "slices"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
@@ -24,8 +25,11 @@ import (
// App is bound to the frontend: every exported method is callable from JS. // App is bound to the frontend: every exported method is callable from JS.
type App struct { type App struct {
ctx context.Context ctx context.Context
dirty bool // dirty decides whether closing the window asks for confirmation. It is
// written from Wails call goroutines (SetDirty) and read by the GTK
// close handler: atomic, like spellMu protects the checker.
dirty atomic.Bool
initialPath string initialPath string
// Wails runs each call on its own goroutine: spelling calls share one // Wails runs each call on its own goroutine: spelling calls share one
@@ -74,7 +78,7 @@ func (a *App) startup(ctx context.Context) {
// beforeClose keeps the window open while there are unsaved changes and // beforeClose keeps the window open while there are unsaved changes and
// lets the frontend ask; it quits again once the user has decided. // lets the frontend ask; it quits again once the user has decided.
func (a *App) beforeClose(ctx context.Context) bool { func (a *App) beforeClose(ctx context.Context) bool {
if !a.dirty { if !a.dirty.Load() {
return false return false
} }
runtime.EventsEmit(a.ctx, "confirm-close") runtime.EventsEmit(a.ctx, "confirm-close")
@@ -201,7 +205,7 @@ func (a *App) RevisionSet(n int) screenplay.Revision {
return r return r
} }
func (a *App) SetDirty(dirty bool) { a.dirty = dirty } func (a *App) SetDirty(dirty bool) { a.dirty.Store(dirty) }
// dataDir is the XDG data directory (~/.local/share). // dataDir is the XDG data directory (~/.local/share).
func dataDir() string { func dataDir() string {