···13131414 "github.com/spf13/cobra"
1515 "github.com/taciturnaxolotl/akami/styles"
1616+ "github.com/taciturnaxolotl/akami/utils"
1617 "github.com/taciturnaxolotl/akami/wakatime"
1718 "gopkg.in/ini.v1"
1819)
···130131 Time: float64(time.Now().Unix()),
131132}
132133134134+func getClientStuff(c *cobra.Command) (key string, url string, err error) {
135135+ configApiKey, _ := c.Flags().GetString("key")
136136+ configApiURL, _ := c.Flags().GetString("url")
137137+138138+ // If either value is missing, try to load from config file
139139+ if configApiKey == "" || configApiURL == "" {
140140+ userDir, err := os.UserHomeDir()
141141+ if err != nil {
142142+ errorTask(c, "Validating arguments")
143143+ return configApiKey, configApiURL, err
144144+ }
145145+ wakatimePath := filepath.Join(userDir, ".wakatime.cfg")
146146+147147+ cfg, err := ini.Load(wakatimePath)
148148+ if err != nil {
149149+ errorTask(c, "Validating arguments")
150150+ return configApiKey, configApiURL, errors.New("config file not found and you haven't passed all arguments")
151151+ }
152152+153153+ settings, err := cfg.GetSection("settings")
154154+ if err != nil {
155155+ errorTask(c, "Validating arguments")
156156+ return configApiKey, configApiURL, errors.New("no settings section in your config")
157157+ }
158158+159159+ // Only load from config if not provided as parameter
160160+ if configApiKey == "" {
161161+ configApiKey = settings.Key("api_key").String()
162162+ if configApiKey == "" {
163163+ errorTask(c, "Validating arguments")
164164+ return configApiKey, configApiURL, errors.New("couldn't find an api_key in your config")
165165+ }
166166+ }
167167+168168+ if configApiURL == "" {
169169+ configApiURL = settings.Key("api_url").String()
170170+ if configApiURL == "" {
171171+ errorTask(c, "Validating arguments")
172172+ return configApiKey, configApiURL, errors.New("couldn't find an api_url in your config")
173173+ }
174174+ }
175175+ }
176176+177177+ return configApiKey, configApiURL, nil
178178+}
179179+133180func Doctor(c *cobra.Command, _ []string) error {
134181 // Initialize a new context with task state
135182 c.SetContext(context.WithValue(context.Background(), "taskState", &taskState{}))
···225272 }
226273 completeTask(c, "Checking your coding stats for today")
227274228228- // Convert seconds to a formatted time string (hours, minutes, seconds)
229229- totalSeconds := duration.Data.GrandTotal.TotalSeconds
230230- hours := totalSeconds / 3600
231231- minutes := (totalSeconds % 3600) / 60
232232- seconds := totalSeconds % 60
233233-234234- formattedTime := ""
235235- if hours > 0 {
236236- formattedTime += fmt.Sprintf("%d hours, ", hours)
237237- }
238238- if minutes > 0 || hours > 0 {
239239- formattedTime += fmt.Sprintf("%d minutes, ", minutes)
240240- }
241241- formattedTime += fmt.Sprintf("%d seconds", seconds)
242242-243243- c.Printf("Sweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for %s\n\n", styles.Fancy.Render(formattedTime))
275275+ c.Printf("Sweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for %s\n\n", styles.Fancy.Render(utils.PrettyPrintTime(duration.Data.GrandTotal.TotalSeconds)))
244276245277 printTask(c, "Sending test heartbeat")
246278···262294263295 printTask(c, "Validating arguments")
264296265265- configApiKey, _ := c.Flags().GetString("key")
266266- configApiURL, _ := c.Flags().GetString("url")
267267-268268- // If either value is missing, try to load from config file
269269- if configApiKey == "" || configApiURL == "" {
270270- userDir, err := os.UserHomeDir()
271271- if err != nil {
272272- errorTask(c, "Validating arguments")
273273- return err
274274- }
275275- wakatimePath := filepath.Join(userDir, ".wakatime.cfg")
276276-277277- cfg, err := ini.Load(wakatimePath)
278278- if err != nil {
279279- errorTask(c, "Validating arguments")
280280- return errors.New("config file not found and you haven't passed all arguments")
281281- }
282282-283283- settings, err := cfg.GetSection("settings")
284284- if err != nil {
285285- errorTask(c, "Validating arguments")
286286- return errors.New("no settings section in your config")
287287- }
288288-289289- // Only load from config if not provided as parameter
290290- if configApiKey == "" {
291291- configApiKey = settings.Key("api_key").String()
292292- if configApiKey == "" {
293293- errorTask(c, "Validating arguments")
294294- return errors.New("couldn't find an api_key in your config")
295295- }
296296- }
297297-298298- if configApiURL == "" {
299299- configApiURL = settings.Key("api_url").String()
300300- if configApiURL == "" {
301301- errorTask(c, "Validating arguments")
302302- return errors.New("couldn't find an api_url in your config")
303303- }
304304- }
305305- }
297297+ api_key, api_url, err := getClientStuff(c)
306298307299 completeTask(c, "Arguments look fine!")
308300309301 printTask(c, "Loading api client")
310302311311- client := wakatime.NewClientWithOptions(configApiKey, configApiURL)
312312- _, err := client.GetStatusBar()
303303+ client := wakatime.NewClientWithOptions(api_key, api_url)
304304+ _, err = client.GetStatusBar()
313305 if err != nil {
314306 errorTask(c, "Loading api client")
315307 return err
···317309318310 completeTask(c, "Loading api client")
319311320320- c.Println("Sending a test heartbeat to", styles.Muted.Render(configApiURL))
312312+ c.Println("Sending a test heartbeat to", styles.Muted.Render(api_url))
321313322314 printTask(c, "Sending test heartbeat")
323315···334326335327 return nil
336328}
329329+330330+func Status(c *cobra.Command, args []string) error {
331331+ // Initialize a new context with task state
332332+ c.SetContext(context.WithValue(context.Background(), "taskState", &taskState{}))
333333+334334+ printTask(c, "Validating arguments")
335335+336336+ api_key, api_url, err := getClientStuff(c)
337337+338338+ completeTask(c, "Arguments look fine!")
339339+340340+ printTask(c, "Loading api client")
341341+342342+ client := wakatime.NewClientWithOptions(api_key, api_url)
343343+ status, err := client.GetStatusBar()
344344+ if err != nil {
345345+ errorTask(c, "Loading api client")
346346+ return err
347347+ }
348348+349349+ completeTask(c, "Loading api client")
350350+351351+ c.Printf("\nLooks like you have coded today for %s today!\n", styles.Fancy.Render(utils.PrettyPrintTime(status.Data.GrandTotal.TotalSeconds)))
352352+353353+ summary, err := client.GetLast7Days()
354354+ if err != nil {
355355+ return err
356356+ }
357357+358358+ c.Printf("You have averaged %s over the last 7 days\n\n", styles.Fancy.Render(utils.PrettyPrintTime(int(summary.Data.DailyAverage))))
359359+360360+ // Display top 5 projects with progress bars
361361+ if len(summary.Data.Projects) > 0 {
362362+ c.Println(styles.Fancy.Render("Top Projects:"))
363363+364364+ // Determine how many projects to show (up to 5)
365365+ count := min(5, len(summary.Data.Projects))
366366+367367+ // Find the longest project name for formatting
368368+ longestName := 0
369369+ longestTime := 0
370370+371371+ for i := range count {
372372+ project := summary.Data.Projects[i]
373373+ if len(project.Name) > longestName {
374374+ longestName = len(project.Name)
375375+ }
376376+377377+ timeStr := utils.PrettyPrintTime(int(project.TotalSeconds))
378378+ if len(timeStr) > longestTime {
379379+ longestTime = len(timeStr)
380380+ }
381381+ }
382382+383383+ // Display each project with a bar
384384+ for i := range count {
385385+ project := summary.Data.Projects[i]
386386+387387+ // Format the project name and time with padding
388388+ paddedName := fmt.Sprintf("%-*s", longestName+2, project.Name)
389389+ timeStr := utils.PrettyPrintTime(int(project.TotalSeconds))
390390+ paddedTime := fmt.Sprintf("%-*s", longestTime+2, timeStr)
391391+392392+ // Create the progress bar
393393+ barWidth := 25
394394+ bar := ""
395395+ percentage := project.Percent
396396+ for j := range barWidth {
397397+ if float64(j) < percentage/(100/float64(barWidth)) {
398398+ bar += "█"
399399+ } else {
400400+ bar += "░"
401401+ }
402402+ }
403403+404404+ // Use different styles for different components
405405+ styledName := styles.Fancy.Render(paddedName)
406406+ styledTime := styles.Muted.Render(paddedTime)
407407+ styledBar := styles.Success.Render(bar)
408408+ styledPercent := styles.Warn.Render(fmt.Sprintf("%.2f%%", percentage))
409409+410410+ // Print the formatted line
411411+ c.Printf(" %s %s %s %s\n", styledName, styledTime, styledBar, styledPercent)
412412+ }
413413+414414+ c.Println()
415415+ }
416416+417417+ // Display top 5 languages with progress bars
418418+ if len(summary.Data.Languages) > 0 {
419419+ c.Println(styles.Fancy.Render("Top Languages:"))
420420+421421+ // Determine how many languages to show (up to 5)
422422+ count := min(5, len(summary.Data.Languages))
423423+424424+ // Find the longest language name for formatting
425425+ longestName := 0
426426+ longestTime := 0
427427+428428+ for i := range count {
429429+ language := summary.Data.Languages[i]
430430+ if len(language.Name) > longestName {
431431+ longestName = len(language.Name)
432432+ }
433433+434434+ timeStr := utils.PrettyPrintTime(int(language.TotalSeconds))
435435+ if len(timeStr) > longestTime {
436436+ longestTime = len(timeStr)
437437+ }
438438+ }
439439+440440+ // Display each language with a bar
441441+ for i := range count {
442442+ language := summary.Data.Languages[i]
443443+444444+ // Format the language name and time with padding
445445+ paddedName := fmt.Sprintf("%-*s", longestName+2, language.Name)
446446+ timeStr := utils.PrettyPrintTime(int(language.TotalSeconds))
447447+ paddedTime := fmt.Sprintf("%-*s", longestTime+2, timeStr)
448448+449449+ // Create the progress bar
450450+ barWidth := 25
451451+ bar := ""
452452+ percentage := language.Percent
453453+ for j := range barWidth {
454454+ if float64(j) < percentage/(100/float64(barWidth)) {
455455+ bar += "█"
456456+ } else {
457457+ bar += "░"
458458+ }
459459+ }
460460+461461+ // Use different styles for different components
462462+ styledName := styles.Fancy.Render(paddedName)
463463+ styledTime := styles.Muted.Render(paddedTime)
464464+ styledBar := styles.Success.Render(bar)
465465+ styledPercent := styles.Warn.Render(fmt.Sprintf("%.2f%%", percentage))
466466+467467+ // Print the formatted line
468468+ c.Printf(" %s %s %s %s\n", styledName, styledTime, styledBar, styledPercent)
469469+ }
470470+471471+ c.Println()
472472+ }
473473+474474+ return nil
475475+}
+13-5
main.go
···3030 Use: "doc",
3131 Short: "diagnose potential hackatime issues",
3232 RunE: handler.Doctor,
3333+ Args: cobra.NoArgs,
3334 })
34353535- cmdTest := &cobra.Command{
3636+ cmd.AddCommand(&cobra.Command{
3637 Use: "test",
3738 Short: "send a test heartbeat to hackatime or whatever api url you provide",
3839 RunE: handler.TestHeartbeat,
3940 Args: cobra.NoArgs,
4040- }
4141- cmdTest.Flags().StringP("url", "u", "", "The base url for the hackatime client")
4242- cmdTest.Flags().StringP("key", "k", "", "API key to use for authentication")
4343- cmd.AddCommand(cmdTest)
4141+ })
4242+4343+ cmd.AddCommand(&cobra.Command{
4444+ Use: "status",
4545+ Short: "get your hackatime stats",
4646+ RunE: handler.Status,
4747+ Args: cobra.NoArgs,
4848+ })
4949+5050+ cmd.PersistentFlags().StringP("url", "u", "", "The base url for the hackatime client")
5151+ cmd.PersistentFlags().StringP("key", "k", "", "API key to use for authentication")
44524553 // this is where we get the fancy fang magic ✨
4654 if err := fang.Execute(
···212212213213 return durationResp, nil
214214}
215215+216216+// Last7DaysResponse represents the response from the WakaTime Last 7 Days API endpoint.
217217+// This contains detailed information about a user's coding activity over the past 7 days.
218218+type Last7DaysResponse struct {
219219+ // Data contains coding statistics for the last 7 days
220220+ Data struct {
221221+ // TotalSeconds is the total time spent coding in seconds
222222+ TotalSeconds float64 `json:"total_seconds"`
223223+ // HumanReadableTotal is the human-readable representation of the total coding time
224224+ HumanReadableTotal string `json:"human_readable_total"`
225225+ // DailyAverage is the average time spent coding per day in seconds
226226+ DailyAverage float64 `json:"daily_average"`
227227+ // HumanReadableDailyAverage is the human-readable representation of the daily average
228228+ HumanReadableDailyAverage string `json:"human_readable_daily_average"`
229229+ // Languages is a list of programming languages used with statistics
230230+ Languages []struct {
231231+ // Name is the programming language name
232232+ Name string `json:"name"`
233233+ // TotalSeconds is the time spent coding in this language in seconds
234234+ TotalSeconds float64 `json:"total_seconds"`
235235+ // Percent is the percentage of time spent in this language
236236+ Percent float64 `json:"percent"`
237237+ // Text is the human-readable representation of time spent in this language
238238+ Text string `json:"text"`
239239+ } `json:"languages"`
240240+ // Editors is a list of editors used with statistics
241241+ Editors []struct {
242242+ // Name is the editor name
243243+ Name string `json:"name"`
244244+ // TotalSeconds is the time spent using this editor in seconds
245245+ TotalSeconds float64 `json:"total_seconds"`
246246+ // Percent is the percentage of time spent using this editor
247247+ Percent float64 `json:"percent"`
248248+ // Text is the human-readable representation of time spent using this editor
249249+ Text string `json:"text"`
250250+ } `json:"editors"`
251251+ // Projects is a list of projects worked on with statistics
252252+ Projects []struct {
253253+ // Name is the project name
254254+ Name string `json:"name"`
255255+ // TotalSeconds is the time spent on this project in seconds
256256+ TotalSeconds float64 `json:"total_seconds"`
257257+ // Percent is the percentage of time spent on this project
258258+ Percent float64 `json:"percent"`
259259+ // Text is the human-readable representation of time spent on this project
260260+ Text string `json:"text"`
261261+ } `json:"projects"`
262262+ } `json:"data"`
263263+}
264264+265265+// GetLast7Days retrieves a user's coding activity summary for the past 7 days from the WakaTime API.
266266+// It returns a Last7DaysResponse and an error if the request fails or returns a non-success status code.
267267+func (c *Client) GetLast7Days() (Last7DaysResponse, error) {
268268+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/users/current/stats/last_7_days", c.APIURL), nil)
269269+ if err != nil {
270270+ return Last7DaysResponse{}, fmt.Errorf("%w: %v", ErrCreatingRequest, err)
271271+ }
272272+273273+ req.Header.Set("Accept", "application/json")
274274+ req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(c.APIKey)))
275275+276276+ resp, err := c.HTTPClient.Do(req)
277277+ if err != nil {
278278+ return Last7DaysResponse{}, fmt.Errorf("%w: %v", ErrSendingRequest, err)
279279+ }
280280+ defer resp.Body.Close()
281281+282282+ // Read the response body for potential error messages
283283+ var respBody bytes.Buffer
284284+ _, err = respBody.ReadFrom(resp.Body)
285285+ if err != nil {
286286+ return Last7DaysResponse{}, fmt.Errorf("failed to read response body: %v", err)
287287+ }
288288+289289+ respContent := respBody.String()
290290+291291+ if resp.StatusCode == http.StatusUnauthorized {
292292+ return Last7DaysResponse{}, fmt.Errorf("%w: %s", ErrUnauthorized, respContent)
293293+ } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
294294+ return Last7DaysResponse{}, fmt.Errorf("%w: status code %d, response: %s", ErrInvalidStatusCode, resp.StatusCode, respContent)
295295+ }
296296+297297+ var statsResp Last7DaysResponse
298298+ if err := json.Unmarshal(respBody.Bytes(), &statsResp); err != nil {
299299+ return Last7DaysResponse{}, fmt.Errorf("%w: %v, response: %s", ErrDecodingResponse, err, respContent)
300300+ }
301301+302302+ return statsResp, nil
303303+}