Skip to content

Commit 61f226e

Browse files
authored
Merge pull request #128 from markusressel/feature/improve-responsiveness-when-selecting-file-browser-entries
Feature/improve responsiveness when selecting file browser entries
2 parents ad73001 + b8b682c commit 61f226e

1 file changed

Lines changed: 102 additions & 22 deletions

File tree

internal/ui/snapshot_browser/snapshot_browser.go

Lines changed: 102 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"slices"
77
"sort"
88
"strings"
9+
"sync/atomic"
910
"time"
1011
"zfs-file-history/internal/data"
1112
"zfs-file-history/internal/data/diff_state"
@@ -40,6 +41,9 @@ type SnapshotBrowserComponent struct {
4041
selectedSnapshotMemory *uiutil.SelectionMemory[data.SnapshotBrowserEntry]
4142

4243
isRestoringSelection bool
44+
45+
diffCancel context.CancelFunc
46+
diffSeq atomic.Uint64
4347
}
4448

4549
type snapshotLoadResult struct {
@@ -270,39 +274,115 @@ func (snapshotBrowser *SnapshotBrowserComponent) updateCurrentSnapshotEntries(qu
270274
}
271275

272276
func (snapshotBrowser *SnapshotBrowserComponent) updateTableEntries() {
273-
newEntries := snapshotBrowser.computeTableEntries(snapshotBrowser.currentSnapshots)
274-
snapshotBrowser.tableContainer.SetData(newEntries)
275-
snapshotBrowser.updateTableTitle()
277+
snapshotBrowser.startAsyncDiffCalculation()
276278
}
277279

278-
func (snapshotBrowser *SnapshotBrowserComponent) Focus() {
279-
snapshotBrowser.application.SetFocus(snapshotBrowser.container)
280+
func (snapshotBrowser *SnapshotBrowserComponent) cancelDiffCalculation() {
281+
if snapshotBrowser.diffCancel != nil {
282+
snapshotBrowser.diffCancel()
283+
snapshotBrowser.diffCancel = nil
284+
}
280285
}
281286

282-
func (snapshotBrowser *SnapshotBrowserComponent) HasFocus() bool {
283-
return snapshotBrowser.container.HasFocus()
284-
}
287+
func (snapshotBrowser *SnapshotBrowserComponent) startAsyncDiffCalculation() {
288+
snapshotBrowser.cancelDiffCalculation()
285289

286-
func (snapshotBrowser *SnapshotBrowserComponent) computeTableEntries(snapshots []*zfs.Snapshot) []*data.SnapshotBrowserEntry {
287-
result := []*data.SnapshotBrowserEntry{}
290+
snapshots := snapshotBrowser.currentSnapshots
291+
fileEntry := snapshotBrowser.currentFileEntry
288292

289-
if snapshotBrowser.hostDataset == nil {
290-
return result
293+
// If no snapshots, clear the table instantly
294+
if len(snapshots) == 0 {
295+
snapshotBrowser.tableContainer.SetData([]*data.SnapshotBrowserEntry{})
296+
snapshotBrowser.updateTableTitle()
297+
return
291298
}
292299

293-
for _, snapshot := range snapshots {
294-
diffState := diff_state.Unknown
295-
if snapshotBrowser.currentFileEntry != nil {
296-
filePath := snapshotBrowser.currentFileEntry.GetRealPath()
297-
diffState = snapshot.DetermineDiffState(filePath)
300+
// Create cancellable context and increment sequence
301+
ctx, cancel := context.WithCancel(context.Background())
302+
snapshotBrowser.diffCancel = cancel
303+
seq := snapshotBrowser.diffSeq.Add(1)
304+
305+
// Step 1: Instantly render table with Unknown ("N/A") states
306+
initialEntries := make([]*data.SnapshotBrowserEntry, len(snapshots))
307+
for i, snap := range snapshots {
308+
initialEntries[i] = &data.SnapshotBrowserEntry{
309+
Snapshot: snap,
310+
DiffState: diff_state.Unknown,
298311
}
299-
result = append(result, &data.SnapshotBrowserEntry{
300-
Snapshot: snapshot,
301-
DiffState: diffState,
302-
})
303312
}
313+
snapshotBrowser.tableContainer.SetData(initialEntries)
314+
snapshotBrowser.updateTableTitle()
315+
316+
// Get the sorted entries from the table container (determines screen top-to-bottom layout)
317+
sortedEntries := snapshotBrowser.tableContainer.GetEntries()
318+
319+
// Step 2: Compute actual diffs in background goroutine, processing them in sorted order
320+
go func() {
321+
filePath := ""
322+
if fileEntry != nil {
323+
filePath = fileEntry.GetRealPath()
324+
}
325+
326+
// Keep a local working copy in the exact sorted order
327+
localEntries := make([]*data.SnapshotBrowserEntry, len(sortedEntries))
328+
for i, entry := range sortedEntries {
329+
localEntries[i] = &data.SnapshotBrowserEntry{
330+
Snapshot: entry.Snapshot,
331+
DiffState: diff_state.Unknown,
332+
}
333+
}
334+
335+
// Closure to safely push local updates to UI thread
336+
updateUI := func() {
337+
entriesCopy := make([]*data.SnapshotBrowserEntry, len(localEntries))
338+
for i, entry := range localEntries {
339+
entriesCopy[i] = &data.SnapshotBrowserEntry{
340+
Snapshot: entry.Snapshot,
341+
DiffState: entry.DiffState,
342+
}
343+
}
344+
345+
snapshotBrowser.application.QueueUpdateDraw(func() {
346+
// Discard update if a newer selection calculation has started
347+
if seq != snapshotBrowser.diffSeq.Load() {
348+
return
349+
}
350+
snapshotBrowser.tableContainer.SetData(entriesCopy)
351+
snapshotBrowser.updateTableTitle()
352+
})
353+
}
354+
355+
for i, entry := range localEntries {
356+
// Abort immediately if user changed selection and cancelled context
357+
if ctx.Err() != nil {
358+
return
359+
}
360+
361+
if filePath != "" {
362+
entry.DiffState = entry.Snapshot.DetermineDiffState(filePath)
363+
}
364+
365+
// Incremental Redraw Logic:
366+
// - First 5 entries: Redraw after each (gives instant feedback on visible entries)
367+
// - Remaining entries: Batch update every 10 entries (optimizes UI rendering)
368+
// - Final entry: Always push the final update
369+
isFirstFew := i < 5
370+
isBatchEnd := (i+1)%10 == 0
371+
isLast := i == len(localEntries)-1
304372

305-
return result
373+
if isFirstFew || isBatchEnd || isLast {
374+
updateUI()
375+
}
376+
}
377+
}()
378+
}
379+
380+
func (snapshotBrowser *SnapshotBrowserComponent) Focus() {
381+
snapshotBrowser.application.SetFocus(snapshotBrowser.container)
382+
}
383+
384+
func (snapshotBrowser *SnapshotBrowserComponent) HasFocus() bool {
385+
return snapshotBrowser.container.HasFocus()
306386
}
307387

308388
func (snapshotBrowser *SnapshotBrowserComponent) updateTableTitle() {

0 commit comments

Comments
 (0)