From 320bb5c34a8521aa653acc8ca5a5a6d573e753c7 Mon Sep 17 00:00:00 2001 From: Abner Coimbre Date: Wed, 22 Oct 2025 00:58:20 -0700 Subject: [PATCH] Add progress indicator/animation --- main.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 1d1417b..477f0d6 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,6 @@ // Meetup Invite 2000 // @author: Abner Coimbre -// Copyright (c) 2024 Handmade Cities +// Copyright (c) Handmade Cities LLC // Based off News Blaster 2000 from the Handmade Network: https://git.handmade.network/hmn/newsblaster2000 @@ -12,7 +12,7 @@ // Jack Punter (@TarriestPython) // // Emotional Support -// Cucui Ganon Rosario +// Cucui Ganon Rosario: https://www.youtube.com/watch?v=bvCsTca0uBc // ========================================================================= package main @@ -306,14 +306,18 @@ func blastMail(cfg *Config, logFile string, trackingFile string, audience []stri if len(group) == cfg.BatchSize { results, err := sendMail(cfg, group, subject, body) if err != nil { - fmt.Printf("Error while sending mail: %v\n", err) return } sentToAddresses = append(sentToAddresses, group...) os.WriteFile(trackingFile, []byte(strings.Join(sentToAddresses, "\n")), 0666) + var failed []string for i, res := range results { log.WriteString(fmt.Sprintf("%s: %s\n", group[i], res.Message)) + if res.ErrorCode != 0 { + failed = append(failed, group[i]) + } } + printBatchSummary(group, failed) newReaderCount += len(group) group = group[0:0] } @@ -322,14 +326,18 @@ func blastMail(cfg *Config, logFile string, trackingFile string, audience []stri if len(group) > 0 { results, err := sendMail(cfg, group, subject, body) if err != nil { - fmt.Printf("Error while sending mail: %v\n", err) return } sentToAddresses = append(sentToAddresses, group...) os.WriteFile(trackingFile, []byte(strings.Join(sentToAddresses, "\n")), 0666) + var failed []string for i, res := range results { log.WriteString(fmt.Sprintf("%s: %s\n", group[i], res.Message)) + if res.ErrorCode != 0 { + failed = append(failed, group[i]) + } } + printBatchSummary(group, failed) newReaderCount += len(group) } @@ -369,8 +377,47 @@ type PostmarkBatchResult struct { Message string `json:"Message"` } +func printBatchSummary(sent, failed []string) { + if len(failed) == 0 { + fmt.Printf("\033[32m✅ Sent %d messages\033[0m\n", len(sent)) + } else { + successCount := len(sent) - len(failed) + if successCount < 0 { + successCount = 0 + } + fmt.Printf("\033[31m❌ Sent %d, failed %d\033[0m\n", successCount, len(failed)) + for _, f := range failed { + fmt.Printf(" - %s\n", f) + } + } +} + func sendMail(cfg *Config, recipients []string, subject, html string) ([]PostmarkBatchResult, error) { - fmt.Printf("Sending batch [%d recipients]...", len(recipients)) + /* Start a tiny spinner while the HTTP call runs. + * Uses \r to update the same line. Stops as soon as the network call returns. + */ + + stop := make(chan struct{}) + done := make(chan struct{}) + go func() { + frames := []rune{'|', '/', '-', '\\'} + i := 0 + for { + select { + case <-stop: + close(done) + return + default: + fmt.Printf("\rSending batch [%d recipients]... %c", len(recipients), frames[i%len(frames)]) + i++ + time.Sleep(120 * time.Millisecond) + } + } + }() + + /* + * Prepare and make the network call + */ from := cfg.Postmark.SenderEmail if cfg.Postmark.SenderName != "" { @@ -400,13 +447,21 @@ func sendMail(cfg *Config, recipients []string, subject, html string) ([]Postmar req, err := http.NewRequest(http.MethodPost, "https://api.postmarkapp.com/email/batchWithTemplates", bytes.NewReader(reqBody)) if err != nil { + // stop spinner before returning + close(stop) + <-done + fmt.Printf("\r") return nil, err } + req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Postmark-Server-Token", cfg.Postmark.ServerToken) res, err := postmarkClient.Do(req) if err != nil { + close(stop) + <-done + fmt.Printf("\r\033[31m❌ Sending batch [%d recipients]... error: %v\033[0m\n", len(recipients), err) return nil, err } @@ -414,17 +469,30 @@ func sendMail(cfg *Config, recipients []string, subject, html string) ([]Postmar resBody, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { + close(stop) + <-done + fmt.Printf("\r") return nil, err } + if res.StatusCode != 200 { + close(stop) + <-done + fmt.Printf("\r\033[31m❌ Bad response from postmark: %d\033[0m\n", res.StatusCode) return nil, fmt.Errorf("Bad response from postmark: %d", res.StatusCode) } + err = json.Unmarshal(resBody, &results) if err != nil { - fmt.Printf("Batch sent successfully, but failed to parse response from postmark.\n") + close(stop) + <-done + fmt.Printf("\r\033[33m⚠️ Batch sent but failed to parse Postmark response\033[0m\n") return nil, nil } - fmt.Printf("Done.\n") + + close(stop) + <-done + fmt.Printf("\r\033[32m✅ Emailed batch of %d recipients\033[0m\n", len(recipients)) return results, nil }