🌷 the cutsie hackatime helper

feat: add task system

dunkirk.sh 6cd4382d b35aaa79

verified
+141 -7
+139 -7
handler/main.go
··· 1 1 package handler 2 2 3 3 import ( 4 + "context" 4 5 "errors" 5 6 "fmt" 7 + "math/rand" 6 8 "os" 7 9 "path/filepath" 8 10 "runtime" ··· 15 17 "gopkg.in/ini.v1" 16 18 ) 17 19 20 + // Task status indicators 21 + var spinnerChars = []string{"[|]", "[/]", "[-]", "[\\]"} 22 + var TaskCompleted = "[*]" 23 + 24 + // taskState holds shared state for the currently running task 25 + type taskState struct { 26 + cancel context.CancelFunc 27 + message string 28 + } 29 + 30 + // printTask prints a task with a spinning animation 31 + func printTask(c *cobra.Command, message string) { 32 + // Create a cancellable context for this spinner 33 + ctx, cancel := context.WithCancel(c.Context()) 34 + 35 + // Store cancel function so we can stop the spinner later 36 + if taskCtx, ok := c.Context().Value("taskState").(*taskState); ok { 37 + // Cancel any previously running spinner first 38 + if taskCtx.cancel != nil { 39 + taskCtx.cancel() 40 + // Small delay to ensure previous spinner is stopped 41 + time.Sleep(10 * time.Millisecond) 42 + } 43 + taskCtx.message = message 44 + taskCtx.cancel = cancel 45 + } else { 46 + // First task, create the state and store it 47 + state := &taskState{ 48 + message: message, 49 + cancel: cancel, 50 + } 51 + c.SetContext(context.WithValue(c.Context(), "taskState", state)) 52 + } 53 + 54 + // Start spinner in background 55 + go func() { 56 + ticker := time.NewTicker(100 * time.Millisecond) 57 + defer ticker.Stop() 58 + i := 0 59 + for { 60 + select { 61 + case <-ctx.Done(): 62 + return 63 + case <-ticker.C: 64 + // Clear line and print spinner with current character 65 + spinner := styles.Muted.Render(spinnerChars[i%len(spinnerChars)]) 66 + c.Printf("\r\033[K%s %s", spinner, message) 67 + i++ 68 + } 69 + } 70 + }() 71 + 72 + // Add a small random delay between 200-400ms to make spinner animation visible 73 + randomDelay := 200 + time.Duration(rand.Intn(201)) // 300-500ms 74 + time.Sleep(randomDelay * time.Millisecond) 75 + } 76 + 77 + // completeTask marks a task as completed 78 + func completeTask(c *cobra.Command, message string) { 79 + // Cancel spinner 80 + if state, ok := c.Context().Value("taskState").(*taskState); ok && state.cancel != nil { 81 + state.cancel() 82 + // Small delay to ensure spinner is stopped 83 + time.Sleep(10 * time.Millisecond) 84 + } 85 + 86 + // Clear line and display success message 87 + c.Printf("\r\033[K%s %s\n", styles.Success.Render(TaskCompleted), message) 88 + } 89 + 90 + // errorTask marks a task as failed 91 + func errorTask(c *cobra.Command, message string) { 92 + // Cancel spinner 93 + if state, ok := c.Context().Value("taskState").(*taskState); ok && state.cancel != nil { 94 + state.cancel() 95 + // Small delay to ensure spinner is stopped 96 + time.Sleep(10 * time.Millisecond) 97 + } 98 + 99 + // Clear line and display error message 100 + c.Printf("\r\033[K%s %s\n", styles.Bad.Render("[ ! ]"), message) 101 + } 102 + 103 + // warnTask marks a task as a warning 104 + func warnTask(c *cobra.Command, message string) { 105 + // Cancel spinner 106 + if state, ok := c.Context().Value("taskState").(*taskState); ok && state.cancel != nil { 107 + state.cancel() 108 + // Small delay to ensure spinner is stopped 109 + time.Sleep(10 * time.Millisecond) 110 + } 111 + 112 + // Clear line and display warning message 113 + c.Printf("\r\033[K%s %s\n", styles.Warn.Render("[?]"), message) 114 + } 115 + 18 116 func Doctor() *cobra.Command { 19 - return &cobra.Command{ 117 + cmd := &cobra.Command{ 20 118 Use: "doc", 21 119 Short: "diagnose potential hackatime issues", 22 120 RunE: func(c *cobra.Command, _ []string) error { 121 + // Initialize a new context with task state 122 + c.SetContext(context.WithValue(context.Background(), "taskState", &taskState{})) 123 + 23 124 // check our os 125 + printTask(c, "Checking operating system") 126 + 24 127 os_name := runtime.GOOS 25 128 26 129 user_dir, err := os.UserHomeDir() 27 130 if err != nil { 131 + errorTask(c, "Checking operating system") 28 132 return errors.New("somehow your user doesn't exist? fairly sure this should never happen; plz report this to @krn on slack or via email at me@dunkirk.sh") 29 133 } 30 134 hackatime_path := filepath.Join(user_dir, ".wakatime.cfg") 31 135 32 136 if os_name != "linux" && os_name != "darwin" && os_name != "windows" { 137 + errorTask(c, "Checking operating system") 33 138 return errors.New("hmm you don't seem to be running a recognized os? you are listed as running " + styles.Fancy.Render(os_name) + "; can you plz report this to @krn on slack or via email at me@dunkirk.sh?") 34 139 } 140 + completeTask(c, "Checking operating system") 141 + 142 + c.Printf("Looks like you are running %s so lets take a look at %s for your config\n\n", styles.Fancy.Render(os_name), styles.Muted.Render(hackatime_path)) 35 143 36 - c.Println("Looks like you are running", styles.Fancy.Render(os_name), "so lets take a look at", styles.Muted.Render(hackatime_path), "for your config") 144 + printTask(c, "Checking wakatime config file") 37 145 38 146 rawCfg, err := os.ReadFile(hackatime_path) 39 147 if errors.Is(err, os.ErrNotExist) { 148 + errorTask(c, "Checking wakatime config file") 40 149 return errors.New("you don't have a wakatime config file! go check " + styles.Muted.Render("https://hackatime.hackclub.com/my/wakatime_setup") + " for the instructions and then try this again") 41 150 } 42 151 43 152 cfg, err := ini.Load(rawCfg) 44 153 if err != nil { 154 + errorTask(c, "Checking wakatime config file") 45 155 return errors.New(err.Error()) 46 156 } 47 157 48 158 settings, err := cfg.GetSection("settings") 49 159 if err != nil { 160 + errorTask(c, "Checking wakatime config file") 50 161 return errors.New("wow! your config file seems to be messed up and doesn't have a settings heading; can you follow the instructions at " + styles.Muted.Render("https://hackatime.hackclub.com/my/wakatime_setup") + " to regenerate it?\n\nThe raw error we got was: " + err.Error()) 51 162 } 163 + completeTask(c, "Checking wakatime config file") 164 + 165 + printTask(c, "Verifying API credentials") 52 166 53 167 api_key := settings.Key("api_key").String() 54 168 api_url := settings.Key("api_url").String() 55 169 if api_key == "" { 170 + errorTask(c, "Verifying API credentials") 56 171 return errors.New("hmm šŸ¤” looks like you don't have an api_key in your config file? are you sure you have followed the setup instructions at " + styles.Muted.Render("https://hackatime.hackclub.com/my/wakatime_setup") + " correctly?") 57 172 } 58 173 if api_url == "" { 174 + errorTask(c, "Verifying API credentials") 59 175 return errors.New("hmm šŸ¤” looks like you don't have an api_url in your config file? are you sure you have followed the setup instructions at " + styles.Muted.Render("https://hackatime.hackclub.com/my/wakatime_setup") + " correctly?") 60 176 } 177 + completeTask(c, "Verifying API credentials") 178 + 179 + printTask(c, "Validating API URL") 61 180 62 181 correctApiUrl := "https://hackatime.hackclub.com/api/hackatime/v1" 63 182 if api_url != correctApiUrl { ··· 66 185 _, err := client.GetStatusBar() 67 186 68 187 if !errors.Is(err, wakatime.ErrUnauthorized) { 188 + errorTask(c, "Validating API URL") 69 189 return errors.New("turns out you were connected to wakatime.com instead of hackatime; since your key seems to work if you would like to keep syncing data to wakatime.com as well as to hackatime you can either setup a realy serve like " + styles.Muted.Render("https://github.com/JasonLovesDoggo/multitime") + " or you can wait for " + styles.Muted.Render("https://github.com/hackclub/hackatime/issues/85") + " to get merged in hackatime and have it synced there :)\n\nIf you want to import your wakatime.com data into hackatime then you can use hackatime v1 temporarily to connect your wakatime account and import (in settings under integrations at " + styles.Muted.Render("https://waka.hackclub.com") + ") and then click the import from hackatime v1 button at " + styles.Muted.Render("https://hackatime.hackclub.com/my/settings") + ".\n\n If you have more questions feel free to reach out to me (hackatime v1 creator) on slack (at @krn) or via email at me@dunkirk.sh") 70 190 } else { 191 + errorTask(c, "Validating API URL") 71 192 return errors.New("turns out your config is connected to the wrong api url and is trying to use wakatime.com to sync time but you don't have a working api key from them. Go to " + styles.Muted.Render("https://hackatime.hackclub.com/my/wakatime_setup") + " to run the setup script and fix your config file") 72 193 } 73 194 } 74 - c.Println("\nYour api url", styles.Muted.Render(api_url), "doesn't match the expected url of", styles.Muted.Render(correctApiUrl), "however if you are using a custom forwarder or are sure you know what you are doing then you are probably fine") 195 + warnTask(c, "Validating API URL") 196 + c.Printf("\nYour api url %s doesn't match the expected url of %s however if you are using a custom forwarder or are sure you know what you are doing then you are probably fine\n\n", styles.Muted.Render(api_url), styles.Muted.Render(correctApiUrl)) 197 + } else { 198 + completeTask(c, "Validating API URL") 75 199 } 76 200 77 201 client := wakatime.NewClientWithOptions(api_key, api_url) 78 - c.Println("\nChecking your coding stats for today...") 202 + printTask(c, "Checking your coding stats for today") 203 + 79 204 duration, err := client.GetStatusBar() 80 205 if err != nil { 206 + errorTask(c, "Checking your coding stats for today") 81 207 if errors.Is(err, wakatime.ErrUnauthorized) { 82 208 return errors.New("Your config file looks mostly correct and you have the correct api url but when we tested your api_key it looks like it is invalid? Can you double check if the key in your config file is the same as at " + styles.Muted.Render("https://hackatime.hackclub.com/my/wakatime_setup") + "?") 83 209 } 84 210 85 211 return errors.New("Something weird happened with the hackatime api; if the error doesn't make sense then please contact @krn on slack or via email at me@dunkirk.sh\n\n" + styles.Bad.Render("Full error: "+err.Error())) 86 212 } 213 + completeTask(c, "Checking your coding stats for today") 214 + 215 + // Add small delay to make the spinner animation visible 87 216 88 217 // Convert seconds to a formatted time string (hours, minutes, seconds) 89 218 totalSeconds := duration.Data.GrandTotal.TotalSeconds ··· 100 229 } 101 230 formattedTime += fmt.Sprintf("%d seconds", seconds) 102 231 103 - c.Println("\nSweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for", styles.Fancy.Render(formattedTime)) 232 + c.Printf("Sweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for %s\n\n", styles.Fancy.Render(formattedTime)) 104 233 105 - c.Println("\nSending one quick heartbeat to make sure everything is ship shape and then you should be good to go!") 234 + printTask(c, "Sending test heartbeat") 106 235 107 236 err = client.SendHeartbeat(wakatime.Heartbeat{ 108 237 Branch: "main", ··· 119 248 Time: float64(time.Now().Unix()), 120 249 }) 121 250 if err != nil { 251 + errorTask(c, "Sending test heartbeat") 122 252 return errors.New("oh dear; looks like something went wrong when sending that heartbeat. " + styles.Bad.Render("Full error: \""+strings.TrimSpace(err.Error())+"\"")) 123 253 } 254 + completeTask(c, "Sending test heartbeat") 124 255 125 - c.Println("\n🄳 it worked! you are good to go! Happy coding šŸ‘‹") 256 + c.Println("🄳 it worked! you are good to go! Happy coding šŸ‘‹") 126 257 127 258 return nil 128 259 }, 129 260 } 261 + return cmd 130 262 }
+2
styles/main.go
··· 5 5 var Fancy = lipgloss.NewStyle().Foreground(lipgloss.Magenta).Bold(true).Italic(true) 6 6 var Muted = lipgloss.NewStyle().Foreground(lipgloss.BrightBlue).Italic(true) 7 7 var Bad = lipgloss.NewStyle().Foreground(lipgloss.BrightRed).Bold(true) 8 + var Success = lipgloss.NewStyle().Foreground(lipgloss.Green).Bold(true) 9 + var Warn = lipgloss.NewStyle().Foreground(lipgloss.Yellow).Bold(true)