Skip to content

Commit b6ca7df

Browse files
committed
Prettier update progress tracker using bubbletea
1 parent e264d7f commit b6ca7df

4 files changed

Lines changed: 179 additions & 2 deletions

File tree

cmd/aocli/aocli.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ func main() {
7676
Help(args)
7777
return
7878
} else if args[1] == "update" {
79-
Update()
79+
// Update()
80+
RunUpdateModel()
8081
return
8182
} else if args[1] == "leaderboard" {
8283
Leaderboard(args)

cmd/aocli/updater.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
"strings"
1111

1212
"dalton.dog/aocgo/internal/styles"
13+
"github.com/charmbracelet/bubbles/spinner"
14+
tea "github.com/charmbracelet/bubbletea"
15+
"github.com/charmbracelet/lipgloss"
1316
"github.com/charmbracelet/log"
1417
"golang.org/x/mod/semver"
1518
)
@@ -146,3 +149,174 @@ func Update() {
146149

147150
logger.Infof("Updated successfully to version %v", release.TagName)
148151
}
152+
153+
type initMsg int
154+
type doneMsg string
155+
type urlMsg struct {
156+
assetURL string
157+
version string
158+
}
159+
160+
type fileMsg struct {
161+
curFile string
162+
tmpFile *os.File
163+
}
164+
165+
type errMsg struct{ err error }
166+
167+
type updateModel struct {
168+
spinner spinner.Model
169+
status string
170+
done bool
171+
err error
172+
173+
version string
174+
assetURL string
175+
curFile string
176+
tmpFile *os.File
177+
}
178+
179+
func RunUpdateModel() {
180+
s := spinner.New()
181+
s.Spinner = spinner.Dot
182+
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(styles.UpdateSpinnerColor))
183+
184+
model := updateModel{
185+
spinner: s,
186+
status: "Starting up!",
187+
done: false,
188+
}
189+
190+
p := tea.NewProgram(model)
191+
192+
if _, err := p.Run(); err != nil {
193+
log.Fatal(err)
194+
}
195+
196+
}
197+
198+
func (m updateModel) Init() tea.Cmd {
199+
initCmd := func() tea.Msg { return initMsg(1) }
200+
201+
return tea.Batch(initCmd, m.spinner.Tick)
202+
}
203+
204+
func (m updateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
205+
var cmds []tea.Cmd
206+
switch msg := msg.(type) {
207+
case initMsg:
208+
m.status = "Querying for latest release"
209+
cmds = append(cmds, func() tea.Msg {
210+
release, err := getLatestRelease()
211+
if err != nil {
212+
return errMsg{err}
213+
}
214+
215+
var assetURL string
216+
for _, asset := range release.Assets {
217+
if asset.Name == fmt.Sprintf("aocli-%v-%v", runtime.GOOS, runtime.GOARCH) {
218+
assetURL = asset.DownloadURL
219+
} else if asset.Name == fmt.Sprintf("aocli-%v-%v.exe", runtime.GOOS, runtime.GOARCH) {
220+
assetURL = asset.DownloadURL
221+
}
222+
}
223+
224+
if assetURL == "" {
225+
return errMsg{err}
226+
}
227+
228+
return urlMsg{assetURL: assetURL, version: release.TagName}
229+
})
230+
231+
case urlMsg:
232+
m.status = "Downloading latest release to temp file"
233+
url := msg.assetURL
234+
m.version = msg.version
235+
236+
cmds = append(cmds, urlCmd(url))
237+
238+
case fileMsg:
239+
m.status = "Replacing current file with new version, then cleaning up"
240+
cur := msg.curFile
241+
tmp := msg.tmpFile
242+
243+
cmds = append(cmds, fileCmd(cur, tmp))
244+
case doneMsg:
245+
m.done = true
246+
m.status = fmt.Sprintf("Successfully updated to version %s", m.version)
247+
return m, tea.Quit
248+
}
249+
250+
var cmd tea.Cmd
251+
m.spinner, cmd = m.spinner.Update(msg)
252+
cmds = append(cmds, cmd)
253+
254+
return m, tea.Batch(cmds...)
255+
}
256+
257+
func (m updateModel) View() string {
258+
var symbol string
259+
var status string
260+
261+
if m.err != nil {
262+
symbol = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Render("")
263+
status = m.err.Error()
264+
} else if m.done {
265+
symbol = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00")).Render("󰸞")
266+
status = m.status
267+
} else {
268+
symbol = m.spinner.View()
269+
status = m.status
270+
}
271+
272+
return fmt.Sprintf("\n %s %s\n", symbol, status)
273+
}
274+
275+
func urlCmd(assetURL string) tea.Cmd {
276+
return func() tea.Msg {
277+
resp, err := http.Get(assetURL)
278+
if err != nil {
279+
return errMsg{err}
280+
}
281+
defer resp.Body.Close()
282+
283+
curExec, err := os.Executable()
284+
if err != nil {
285+
return errMsg{err}
286+
}
287+
288+
tmpFile, err := os.CreateTemp("", "aocli-update-")
289+
if err != nil {
290+
return errMsg{err}
291+
}
292+
293+
// Write the downloaded content to the temporary file
294+
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
295+
return errMsg{err}
296+
}
297+
298+
return fileMsg{curFile: curExec, tmpFile: tmpFile}
299+
}
300+
}
301+
302+
func fileCmd(curFile string, tmpFile *os.File) tea.Cmd {
303+
return func() tea.Msg {
304+
// Close the file to flush the content
305+
if err := tmpFile.Close(); err != nil {
306+
return errMsg{err}
307+
}
308+
309+
// Make the temp file executable
310+
if err := os.Chmod(tmpFile.Name(), 0700); err != nil {
311+
return errMsg{err}
312+
}
313+
314+
// Replace the current executable with the new one
315+
if err := os.Rename(tmpFile.Name(), curFile); err != nil {
316+
return errMsg{err}
317+
}
318+
319+
os.Remove(tmpFile.Name())
320+
return doneMsg(1)
321+
}
322+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/charmbracelet/bubbletea v0.26.6
1414
github.com/charmbracelet/lipgloss v0.12.1
1515
github.com/charmbracelet/log v0.4.0
16+
github.com/evertras/bubble-table v0.16.1
1617
go.etcd.io/bbolt v1.3.10
1718
golang.org/x/mod v0.20.0
1819
golang.org/x/term v0.23.0
@@ -28,7 +29,6 @@ require (
2829
github.com/charmbracelet/x/term v0.1.1 // indirect
2930
github.com/charmbracelet/x/windows v0.1.2 // indirect
3031
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
31-
github.com/evertras/bubble-table v0.16.1 // indirect
3232
github.com/go-logfmt/logfmt v0.6.0 // indirect
3333
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
3434
github.com/mattn/go-isatty v0.0.20 // indirect

internal/styles/styles.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ var (
2222
starColor = lipgloss.AdaptiveColor{Light: "#F1FA8C", Dark: "#F1FA8C"}
2323
linkColor = lipgloss.AdaptiveColor{Light: "#8BE9FD", Dark: "#8BE9FD"}
2424
codeColor = lipgloss.AdaptiveColor{Light: "#FAC3D5", Dark: "#FAC3D5"}
25+
26+
UpdateSpinnerColor = lipgloss.Color("#FB25A0")
2527
)
2628

2729
var (

0 commit comments

Comments
 (0)