diff --git a/view/html.go b/view/html.go
index 3f8737febd6f886d5b019477038bc819d23455e9..09ac5ea42e3f7265eaf5ef43b23385ba7eba20d3 100644
--- a/view/html.go
+++ b/view/html.go
@@ -9,6 +9,7 @@ import (
"os"
"regexp"
"strings"
+ "sync/atomic"
"time"
"charm.land/lipgloss/v2"
@@ -282,9 +283,7 @@ var nextImageID uint32 = 1000
// allocImageID returns a unique Kitty image ID.
func allocImageID() uint32 {
- id := nextImageID
- nextImageID++
- return id
+ return atomic.AddUint32(&nextImageID, 1)
}
func fetchRemoteBase64(url string) string {
diff --git a/view/html_test.go b/view/html_test.go
index 795e705baeba9e5e27aeff46c2d939f4e0b26d5e..625971b4e569f2a7be079796c109349a4377620d 100644
--- a/view/html_test.go
+++ b/view/html_test.go
@@ -5,6 +5,8 @@ import (
"os"
"regexp"
"strings"
+ "sync"
+ "sync/atomic"
"testing"
"charm.land/lipgloss/v2"
@@ -761,3 +763,44 @@ func TestRemoteImageCache_EvictsOldestWhenFull(t *testing.T) {
}
}
}
+
+func TestAllocImageID_NoRace(t *testing.T) {
+ // Reset the counter so IDs start from a known value.
+ atomic.StoreUint32(&nextImageID, 1000)
+
+ const goroutines = 100
+ const idsPerGoroutine = 100
+
+ results := make(chan uint32, goroutines*idsPerGoroutine)
+
+ var wg sync.WaitGroup
+ wg.Add(goroutines)
+ for range goroutines {
+ go func() {
+ defer wg.Done()
+ for range idsPerGoroutine {
+ results <- allocImageID()
+ }
+ }()
+ }
+
+ // Close channel once all writers are done.
+ go func() {
+ wg.Wait()
+ close(results)
+ }()
+
+ // Collect all IDs and verify uniqueness.
+ seen := make(map[uint32]bool, goroutines*idsPerGoroutine)
+ for id := range results {
+ if seen[id] {
+ t.Fatalf("duplicate image ID allocated: %d (race condition detected)", id)
+ }
+ seen[id] = true
+ }
+
+ expected := uint32(goroutines * idsPerGoroutine)
+ if uint32(len(seen)) != expected {
+ t.Errorf("expected %d unique IDs, got %d", expected, len(seen))
+ }
+}