v0.28.0 3cb3770

update.go RawHistoryFolder

1package coordinator
2
3import (
4	"context"
5	"fmt"
6	"sync"
7
8	"github.com/bnema/puregotk/v4/glib"
9
10	"github.com/bnema/dumber/internal/application/usecase"
11	"github.com/bnema/dumber/internal/domain/entity"
12	"github.com/bnema/dumber/internal/logging"
13	"github.com/bnema/dumber/internal/ui/component"
14)
15
16// UpdateCoordinator handles update checking and notification.
17type UpdateCoordinator struct {
18	checkUC         *usecase.CheckUpdateUseCase
19	applyUC         *usecase.ApplyUpdateUseCase
20	toaster         *component.Toaster
21	enableOnStartup bool
22	autoDownload    bool
23	mu              sync.RWMutex // Protects status and lastInfo
24	status          entity.UpdateStatus
25	lastInfo        *usecase.CheckUpdateOutput
26}
27
28// NewUpdateCoordinator creates a new update coordinator.
29// enableOnStartup controls whether an update check runs at startup.
30// autoDownload controls whether updates are downloaded automatically in the background.
31func NewUpdateCoordinator(
32	checkUC *usecase.CheckUpdateUseCase,
33	applyUC *usecase.ApplyUpdateUseCase,
34	toaster *component.Toaster,
35	enableOnStartup bool,
36	autoDownload bool,
37) *UpdateCoordinator {
38	return &UpdateCoordinator{
39		checkUC:         checkUC,
40		applyUC:         applyUC,
41		toaster:         toaster,
42		enableOnStartup: enableOnStartup,
43		autoDownload:    autoDownload,
44		status:          entity.UpdateStatusUnknown,
45	}
46}
47
48// CheckOnStartup triggers an update check if enabled via the enableOnStartup constructor flag.
49func (c *UpdateCoordinator) CheckOnStartup(ctx context.Context) {
50	log := logging.FromContext(ctx)
51
52	if !c.enableOnStartup {
53		log.Debug().Msg("update check on startup disabled")
54		return
55	}
56
57	go c.checkAsync(ctx)
58}
59
60func (c *UpdateCoordinator) checkAsync(ctx context.Context) {
61	log := logging.FromContext(ctx)
62
63	result, err := c.checkUC.Execute(ctx, usecase.CheckUpdateInput{})
64	if err != nil {
65		log.Warn().Err(err).Msg("background update check failed")
66		c.mu.Lock()
67		c.status = entity.UpdateStatusFailed
68		c.mu.Unlock()
69		return
70	}
71
72	c.mu.Lock()
73	c.lastInfo = result
74	if !result.UpdateAvailable {
75		c.status = entity.UpdateStatusUpToDate
76		c.mu.Unlock()
77		log.Debug().
78			Str("version", result.CurrentVersion).
79			Msg("already on latest version")
80		return
81	}
82	c.status = entity.UpdateStatusAvailable
83	c.mu.Unlock()
84	log.Info().
85		Str("current", result.CurrentVersion).
86		Str("latest", result.LatestVersion).
87		Bool("can_auto_update", result.CanAutoUpdate).
88		Msg("update available")
89
90	c.showUpdateNotification(ctx, result)
91
92	if c.autoDownload && result.CanAutoUpdate {
93		go c.downloadAsync(ctx, result.DownloadURL)
94	}
95}
96
97func (c *UpdateCoordinator) showUpdateNotification(ctx context.Context, result *usecase.CheckUpdateOutput) {
98	var msg string
99	switch {
100	case result.CanAutoUpdate && c.autoDownload:
101		msg = fmt.Sprintf("Downloading update %s...", result.LatestVersion)
102	default:
103		msg = fmt.Sprintf("Update %s available", result.LatestVersion)
104	}
105
106	cb := glib.SourceFunc(func(_ uintptr) bool {
107		c.toaster.Show(ctx, msg, component.ToastInfo)
108		return false
109	})
110	glib.IdleAdd(&cb, 0)
111}
112
113func (c *UpdateCoordinator) downloadAsync(ctx context.Context, downloadURL string) {
114	log := logging.FromContext(ctx)
115
116	c.mu.Lock()
117	c.status = entity.UpdateStatusDownloading
118	c.mu.Unlock()
119
120	result, err := c.applyUC.Execute(ctx, usecase.ApplyUpdateInput{
121		DownloadURL: downloadURL,
122	})
123	if err != nil {
124		log.Error().Err(err).Msg("update download failed")
125		c.mu.Lock()
126		c.status = entity.UpdateStatusFailed
127		c.mu.Unlock()
128		c.showToast(ctx, "Update download failed", component.ToastError)
129		return
130	}
131
132	c.mu.Lock()
133	if result.Status == entity.UpdateStatusReady {
134		c.status = entity.UpdateStatusReady
135		c.mu.Unlock()
136		log.Info().Msg("update ready, will apply on exit")
137		c.showToast(ctx, "Update ready - applies on exit", component.ToastSuccess)
138	} else {
139		c.status = entity.UpdateStatusFailed
140		c.mu.Unlock()
141		log.Warn().Str("message", result.Message).Msg("update staging failed")
142		c.showToast(ctx, result.Message, component.ToastWarning)
143	}
144}
145
146func (c *UpdateCoordinator) showToast(ctx context.Context, msg string, level component.ToastLevel) {
147	cb := glib.SourceFunc(func(_ uintptr) bool {
148		c.toaster.Show(ctx, msg, level)
149		return false
150	})
151	glib.IdleAdd(&cb, 0)
152}
153
154// FinalizeOnExit applies a staged update on process exit.
155func (c *UpdateCoordinator) FinalizeOnExit(ctx context.Context) error {
156	if c.applyUC == nil {
157		return nil
158	}
159	return c.applyUC.FinalizeOnExit(ctx)
160}
161
162// Status returns the current update status.
163func (c *UpdateCoordinator) Status() entity.UpdateStatus {
164	c.mu.RLock()
165	defer c.mu.RUnlock()
166	return c.status
167}
168
169// LastCheckResult returns the most recent update check result.
170func (c *UpdateCoordinator) LastCheckResult() *usecase.CheckUpdateOutput {
171	c.mu.RLock()
172	defer c.mu.RUnlock()
173	return c.lastInfo
174}
175
176// HasPendingUpdate returns true if a downloaded update is waiting to be applied.
177func (c *UpdateCoordinator) HasPendingUpdate(ctx context.Context) bool {
178	if c.applyUC == nil {
179		return false
180	}
181	return c.applyUC.HasPendingUpdate(ctx)
182}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close