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	toastFn         func(ctx context.Context, msg string, level component.ToastLevel)
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	showToast func(ctx context.Context, msg string, level component.ToastLevel),
35	enableOnStartup bool,
36	autoDownload bool,
37) *UpdateCoordinator {
38	return &UpdateCoordinator{
39		checkUC:         checkUC,
40		applyUC:         applyUC,
41		toastFn:         showToast,
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		if c.toastFn != nil {
108			c.toastFn(ctx, msg, component.ToastInfo)
109		}
110		return false
111	})
112	glib.IdleAdd(&cb, 0)
113}
114
115func (c *UpdateCoordinator) downloadAsync(ctx context.Context, downloadURL string) {
116	log := logging.FromContext(ctx)
117
118	c.mu.Lock()
119	c.status = entity.UpdateStatusDownloading
120	c.mu.Unlock()
121
122	result, err := c.applyUC.Execute(ctx, usecase.ApplyUpdateInput{
123		DownloadURL: downloadURL,
124	})
125	if err != nil {
126		log.Error().Err(err).Msg("update download failed")
127		c.mu.Lock()
128		c.status = entity.UpdateStatusFailed
129		c.mu.Unlock()
130		c.showToast(ctx, "Update download failed", component.ToastError)
131		return
132	}
133
134	c.mu.Lock()
135	if result.Status == entity.UpdateStatusReady {
136		c.status = entity.UpdateStatusReady
137		c.mu.Unlock()
138		log.Info().Msg("update ready, will apply on exit")
139		c.showToast(ctx, "Update ready - applies on exit", component.ToastSuccess)
140	} else {
141		c.status = entity.UpdateStatusFailed
142		c.mu.Unlock()
143		log.Warn().Str("message", result.Message).Msg("update staging failed")
144		c.showToast(ctx, result.Message, component.ToastWarning)
145	}
146}
147
148func (c *UpdateCoordinator) showToast(ctx context.Context, msg string, level component.ToastLevel) {
149	cb := glib.SourceFunc(func(_ uintptr) bool {
150		if c.toastFn != nil {
151			c.toastFn(ctx, msg, level)
152		}
153		return false
154	})
155	glib.IdleAdd(&cb, 0)
156}
157
158// FinalizeOnExit applies a staged update on process exit.
159func (c *UpdateCoordinator) FinalizeOnExit(ctx context.Context) error {
160	if c.applyUC == nil {
161		return nil
162	}
163	return c.applyUC.FinalizeOnExit(ctx)
164}
165
166// Status returns the current update status.
167func (c *UpdateCoordinator) Status() entity.UpdateStatus {
168	c.mu.RLock()
169	defer c.mu.RUnlock()
170	return c.status
171}
172
173// LastCheckResult returns the most recent update check result.
174func (c *UpdateCoordinator) LastCheckResult() *usecase.CheckUpdateOutput {
175	c.mu.RLock()
176	defer c.mu.RUnlock()
177	return c.lastInfo
178}
179
180// HasPendingUpdate returns true if a downloaded update is waiting to be applied.
181func (c *UpdateCoordinator) HasPendingUpdate(ctx context.Context) bool {
182	if c.applyUC == nil {
183		return false
184	}
185	return c.applyUC.HasPendingUpdate(ctx)
186}

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