···11package handler
2233import (
44+ "context"
45 "errors"
56 "fmt"
77+ "math/rand"
68 "os"
79 "path/filepath"
810 "runtime"
···1517 "gopkg.in/ini.v1"
1618)
17192020+// Task status indicators
2121+var spinnerChars = []string{"[|]", "[/]", "[-]", "[\\]"}
2222+var TaskCompleted = "[*]"
2323+2424+// taskState holds shared state for the currently running task
2525+type taskState struct {
2626+ cancel context.CancelFunc
2727+ message string
2828+}
2929+3030+// printTask prints a task with a spinning animation
3131+func printTask(c *cobra.Command, message string) {
3232+ // Create a cancellable context for this spinner
3333+ ctx, cancel := context.WithCancel(c.Context())
3434+3535+ // Store cancel function so we can stop the spinner later
3636+ if taskCtx, ok := c.Context().Value("taskState").(*taskState); ok {
3737+ // Cancel any previously running spinner first
3838+ if taskCtx.cancel != nil {
3939+ taskCtx.cancel()
4040+ // Small delay to ensure previous spinner is stopped
4141+ time.Sleep(10 * time.Millisecond)
4242+ }
4343+ taskCtx.message = message
4444+ taskCtx.cancel = cancel
4545+ } else {
4646+ // First task, create the state and store it
4747+ state := &taskState{
4848+ message: message,
4949+ cancel: cancel,
5050+ }
5151+ c.SetContext(context.WithValue(c.Context(), "taskState", state))
5252+ }
5353+5454+ // Start spinner in background
5555+ go func() {
5656+ ticker := time.NewTicker(100 * time.Millisecond)
5757+ defer ticker.Stop()
5858+ i := 0
5959+ for {
6060+ select {
6161+ case <-ctx.Done():
6262+ return
6363+ case <-ticker.C:
6464+ // Clear line and print spinner with current character
6565+ spinner := styles.Muted.Render(spinnerChars[i%len(spinnerChars)])
6666+ c.Printf("\r\033[K%s %s", spinner, message)
6767+ i++
6868+ }
6969+ }
7070+ }()
7171+7272+ // Add a small random delay between 200-400ms to make spinner animation visible
7373+ randomDelay := 200 + time.Duration(rand.Intn(201)) // 300-500ms
7474+ time.Sleep(randomDelay * time.Millisecond)
7575+}
7676+7777+// completeTask marks a task as completed
7878+func completeTask(c *cobra.Command, message string) {
7979+ // Cancel spinner
8080+ if state, ok := c.Context().Value("taskState").(*taskState); ok && state.cancel != nil {
8181+ state.cancel()
8282+ // Small delay to ensure spinner is stopped
8383+ time.Sleep(10 * time.Millisecond)
8484+ }
8585+8686+ // Clear line and display success message
8787+ c.Printf("\r\033[K%s %s\n", styles.Success.Render(TaskCompleted), message)
8888+}
8989+9090+// errorTask marks a task as failed
9191+func errorTask(c *cobra.Command, message string) {
9292+ // Cancel spinner
9393+ if state, ok := c.Context().Value("taskState").(*taskState); ok && state.cancel != nil {
9494+ state.cancel()
9595+ // Small delay to ensure spinner is stopped
9696+ time.Sleep(10 * time.Millisecond)
9797+ }
9898+9999+ // Clear line and display error message
100100+ c.Printf("\r\033[K%s %s\n", styles.Bad.Render("[ ! ]"), message)
101101+}
102102+103103+// warnTask marks a task as a warning
104104+func warnTask(c *cobra.Command, message string) {
105105+ // Cancel spinner
106106+ if state, ok := c.Context().Value("taskState").(*taskState); ok && state.cancel != nil {
107107+ state.cancel()
108108+ // Small delay to ensure spinner is stopped
109109+ time.Sleep(10 * time.Millisecond)
110110+ }
111111+112112+ // Clear line and display warning message
113113+ c.Printf("\r\033[K%s %s\n", styles.Warn.Render("[?]"), message)
114114+}
115115+18116func Doctor() *cobra.Command {
1919- return &cobra.Command{
117117+ cmd := &cobra.Command{
20118 Use: "doc",
21119 Short: "diagnose potential hackatime issues",
22120 RunE: func(c *cobra.Command, _ []string) error {
121121+ // Initialize a new context with task state
122122+ c.SetContext(context.WithValue(context.Background(), "taskState", &taskState{}))
123123+23124 // check our os
125125+ printTask(c, "Checking operating system")
126126+24127 os_name := runtime.GOOS
2512826129 user_dir, err := os.UserHomeDir()
27130 if err != nil {
131131+ errorTask(c, "Checking operating system")
28132 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")
29133 }
30134 hackatime_path := filepath.Join(user_dir, ".wakatime.cfg")
3113532136 if os_name != "linux" && os_name != "darwin" && os_name != "windows" {
137137+ errorTask(c, "Checking operating system")
33138 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?")
34139 }
140140+ completeTask(c, "Checking operating system")
141141+142142+ 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))
351433636- 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")
144144+ printTask(c, "Checking wakatime config file")
3714538146 rawCfg, err := os.ReadFile(hackatime_path)
39147 if errors.Is(err, os.ErrNotExist) {
148148+ errorTask(c, "Checking wakatime config file")
40149 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")
41150 }
4215143152 cfg, err := ini.Load(rawCfg)
44153 if err != nil {
154154+ errorTask(c, "Checking wakatime config file")
45155 return errors.New(err.Error())
46156 }
4715748158 settings, err := cfg.GetSection("settings")
49159 if err != nil {
160160+ errorTask(c, "Checking wakatime config file")
50161 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())
51162 }
163163+ completeTask(c, "Checking wakatime config file")
164164+165165+ printTask(c, "Verifying API credentials")
5216653167 api_key := settings.Key("api_key").String()
54168 api_url := settings.Key("api_url").String()
55169 if api_key == "" {
170170+ errorTask(c, "Verifying API credentials")
56171 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?")
57172 }
58173 if api_url == "" {
174174+ errorTask(c, "Verifying API credentials")
59175 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?")
60176 }
177177+ completeTask(c, "Verifying API credentials")
178178+179179+ printTask(c, "Validating API URL")
6118062181 correctApiUrl := "https://hackatime.hackclub.com/api/hackatime/v1"
63182 if api_url != correctApiUrl {
···66185 _, err := client.GetStatusBar()
6718668187 if !errors.Is(err, wakatime.ErrUnauthorized) {
188188+ errorTask(c, "Validating API URL")
69189 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")
70190 } else {
191191+ errorTask(c, "Validating API URL")
71192 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")
72193 }
73194 }
7474- 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")
195195+ warnTask(c, "Validating API URL")
196196+ 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))
197197+ } else {
198198+ completeTask(c, "Validating API URL")
75199 }
7620077201 client := wakatime.NewClientWithOptions(api_key, api_url)
7878- c.Println("\nChecking your coding stats for today...")
202202+ printTask(c, "Checking your coding stats for today")
203203+79204 duration, err := client.GetStatusBar()
80205 if err != nil {
206206+ errorTask(c, "Checking your coding stats for today")
81207 if errors.Is(err, wakatime.ErrUnauthorized) {
82208 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") + "?")
83209 }
8421085211 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()))
86212 }
213213+ completeTask(c, "Checking your coding stats for today")
214214+215215+ // Add small delay to make the spinner animation visible
8721688217 // Convert seconds to a formatted time string (hours, minutes, seconds)
89218 totalSeconds := duration.Data.GrandTotal.TotalSeconds
···100229 }
101230 formattedTime += fmt.Sprintf("%d seconds", seconds)
102231103103- c.Println("\nSweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for", styles.Fancy.Render(formattedTime))
232232+ c.Printf("Sweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for %s\n\n", styles.Fancy.Render(formattedTime))
104233105105- c.Println("\nSending one quick heartbeat to make sure everything is ship shape and then you should be good to go!")
234234+ printTask(c, "Sending test heartbeat")
106235107236 err = client.SendHeartbeat(wakatime.Heartbeat{
108237 Branch: "main",
···119248 Time: float64(time.Now().Unix()),
120249 })
121250 if err != nil {
251251+ errorTask(c, "Sending test heartbeat")
122252 return errors.New("oh dear; looks like something went wrong when sending that heartbeat. " + styles.Bad.Render("Full error: \""+strings.TrimSpace(err.Error())+"\""))
123253 }
254254+ completeTask(c, "Sending test heartbeat")
124255125125- c.Println("\nš„³ it worked! you are good to go! Happy coding š")
256256+ c.Println("š„³ it worked! you are good to go! Happy coding š")
126257127258 return nil
128259 },
129260 }
261261+ return cmd
130262}