@@ -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+ }
0 commit comments