🌷 the cutsie hackatime helper

feat: add status command

dunkirk.sh 18c5657b 25ab5c9b

verified
Changed files
+323 -65
handler
utils
wakatime
+199 -60
handler/main.go
···
"github.com/spf13/cobra"
"github.com/taciturnaxolotl/akami/styles"
+
"github.com/taciturnaxolotl/akami/utils"
"github.com/taciturnaxolotl/akami/wakatime"
"gopkg.in/ini.v1"
)
···
Time: float64(time.Now().Unix()),
}
+
func getClientStuff(c *cobra.Command) (key string, url string, err error) {
+
configApiKey, _ := c.Flags().GetString("key")
+
configApiURL, _ := c.Flags().GetString("url")
+
+
// If either value is missing, try to load from config file
+
if configApiKey == "" || configApiURL == "" {
+
userDir, err := os.UserHomeDir()
+
if err != nil {
+
errorTask(c, "Validating arguments")
+
return configApiKey, configApiURL, err
+
}
+
wakatimePath := filepath.Join(userDir, ".wakatime.cfg")
+
+
cfg, err := ini.Load(wakatimePath)
+
if err != nil {
+
errorTask(c, "Validating arguments")
+
return configApiKey, configApiURL, errors.New("config file not found and you haven't passed all arguments")
+
}
+
+
settings, err := cfg.GetSection("settings")
+
if err != nil {
+
errorTask(c, "Validating arguments")
+
return configApiKey, configApiURL, errors.New("no settings section in your config")
+
}
+
+
// Only load from config if not provided as parameter
+
if configApiKey == "" {
+
configApiKey = settings.Key("api_key").String()
+
if configApiKey == "" {
+
errorTask(c, "Validating arguments")
+
return configApiKey, configApiURL, errors.New("couldn't find an api_key in your config")
+
}
+
}
+
+
if configApiURL == "" {
+
configApiURL = settings.Key("api_url").String()
+
if configApiURL == "" {
+
errorTask(c, "Validating arguments")
+
return configApiKey, configApiURL, errors.New("couldn't find an api_url in your config")
+
}
+
}
+
}
+
+
return configApiKey, configApiURL, nil
+
}
+
func Doctor(c *cobra.Command, _ []string) error {
// Initialize a new context with task state
c.SetContext(context.WithValue(context.Background(), "taskState", &taskState{}))
···
}
completeTask(c, "Checking your coding stats for today")
-
// Convert seconds to a formatted time string (hours, minutes, seconds)
-
totalSeconds := duration.Data.GrandTotal.TotalSeconds
-
hours := totalSeconds / 3600
-
minutes := (totalSeconds % 3600) / 60
-
seconds := totalSeconds % 60
-
-
formattedTime := ""
-
if hours > 0 {
-
formattedTime += fmt.Sprintf("%d hours, ", hours)
-
}
-
if minutes > 0 || hours > 0 {
-
formattedTime += fmt.Sprintf("%d minutes, ", minutes)
-
}
-
formattedTime += fmt.Sprintf("%d seconds", seconds)
-
-
c.Printf("Sweet!!! Looks like your hackatime is configured properly! Looks like you have coded today for %s\n\n", styles.Fancy.Render(formattedTime))
+
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)))
printTask(c, "Sending test heartbeat")
···
printTask(c, "Validating arguments")
-
configApiKey, _ := c.Flags().GetString("key")
-
configApiURL, _ := c.Flags().GetString("url")
-
-
// If either value is missing, try to load from config file
-
if configApiKey == "" || configApiURL == "" {
-
userDir, err := os.UserHomeDir()
-
if err != nil {
-
errorTask(c, "Validating arguments")
-
return err
-
}
-
wakatimePath := filepath.Join(userDir, ".wakatime.cfg")
-
-
cfg, err := ini.Load(wakatimePath)
-
if err != nil {
-
errorTask(c, "Validating arguments")
-
return errors.New("config file not found and you haven't passed all arguments")
-
}
-
-
settings, err := cfg.GetSection("settings")
-
if err != nil {
-
errorTask(c, "Validating arguments")
-
return errors.New("no settings section in your config")
-
}
-
-
// Only load from config if not provided as parameter
-
if configApiKey == "" {
-
configApiKey = settings.Key("api_key").String()
-
if configApiKey == "" {
-
errorTask(c, "Validating arguments")
-
return errors.New("couldn't find an api_key in your config")
-
}
-
}
-
-
if configApiURL == "" {
-
configApiURL = settings.Key("api_url").String()
-
if configApiURL == "" {
-
errorTask(c, "Validating arguments")
-
return errors.New("couldn't find an api_url in your config")
-
}
-
}
-
}
+
api_key, api_url, err := getClientStuff(c)
completeTask(c, "Arguments look fine!")
printTask(c, "Loading api client")
-
client := wakatime.NewClientWithOptions(configApiKey, configApiURL)
-
_, err := client.GetStatusBar()
+
client := wakatime.NewClientWithOptions(api_key, api_url)
+
_, err = client.GetStatusBar()
if err != nil {
errorTask(c, "Loading api client")
return err
···
completeTask(c, "Loading api client")
-
c.Println("Sending a test heartbeat to", styles.Muted.Render(configApiURL))
+
c.Println("Sending a test heartbeat to", styles.Muted.Render(api_url))
printTask(c, "Sending test heartbeat")
···
return nil
}
+
+
func Status(c *cobra.Command, args []string) error {
+
// Initialize a new context with task state
+
c.SetContext(context.WithValue(context.Background(), "taskState", &taskState{}))
+
+
printTask(c, "Validating arguments")
+
+
api_key, api_url, err := getClientStuff(c)
+
+
completeTask(c, "Arguments look fine!")
+
+
printTask(c, "Loading api client")
+
+
client := wakatime.NewClientWithOptions(api_key, api_url)
+
status, err := client.GetStatusBar()
+
if err != nil {
+
errorTask(c, "Loading api client")
+
return err
+
}
+
+
completeTask(c, "Loading api client")
+
+
c.Printf("\nLooks like you have coded today for %s today!\n", styles.Fancy.Render(utils.PrettyPrintTime(status.Data.GrandTotal.TotalSeconds)))
+
+
summary, err := client.GetLast7Days()
+
if err != nil {
+
return err
+
}
+
+
c.Printf("You have averaged %s over the last 7 days\n\n", styles.Fancy.Render(utils.PrettyPrintTime(int(summary.Data.DailyAverage))))
+
+
// Display top 5 projects with progress bars
+
if len(summary.Data.Projects) > 0 {
+
c.Println(styles.Fancy.Render("Top Projects:"))
+
+
// Determine how many projects to show (up to 5)
+
count := min(5, len(summary.Data.Projects))
+
+
// Find the longest project name for formatting
+
longestName := 0
+
longestTime := 0
+
+
for i := range count {
+
project := summary.Data.Projects[i]
+
if len(project.Name) > longestName {
+
longestName = len(project.Name)
+
}
+
+
timeStr := utils.PrettyPrintTime(int(project.TotalSeconds))
+
if len(timeStr) > longestTime {
+
longestTime = len(timeStr)
+
}
+
}
+
+
// Display each project with a bar
+
for i := range count {
+
project := summary.Data.Projects[i]
+
+
// Format the project name and time with padding
+
paddedName := fmt.Sprintf("%-*s", longestName+2, project.Name)
+
timeStr := utils.PrettyPrintTime(int(project.TotalSeconds))
+
paddedTime := fmt.Sprintf("%-*s", longestTime+2, timeStr)
+
+
// Create the progress bar
+
barWidth := 25
+
bar := ""
+
percentage := project.Percent
+
for j := range barWidth {
+
if float64(j) < percentage/(100/float64(barWidth)) {
+
bar += "█"
+
} else {
+
bar += "░"
+
}
+
}
+
+
// Use different styles for different components
+
styledName := styles.Fancy.Render(paddedName)
+
styledTime := styles.Muted.Render(paddedTime)
+
styledBar := styles.Success.Render(bar)
+
styledPercent := styles.Warn.Render(fmt.Sprintf("%.2f%%", percentage))
+
+
// Print the formatted line
+
c.Printf(" %s %s %s %s\n", styledName, styledTime, styledBar, styledPercent)
+
}
+
+
c.Println()
+
}
+
+
// Display top 5 languages with progress bars
+
if len(summary.Data.Languages) > 0 {
+
c.Println(styles.Fancy.Render("Top Languages:"))
+
+
// Determine how many languages to show (up to 5)
+
count := min(5, len(summary.Data.Languages))
+
+
// Find the longest language name for formatting
+
longestName := 0
+
longestTime := 0
+
+
for i := range count {
+
language := summary.Data.Languages[i]
+
if len(language.Name) > longestName {
+
longestName = len(language.Name)
+
}
+
+
timeStr := utils.PrettyPrintTime(int(language.TotalSeconds))
+
if len(timeStr) > longestTime {
+
longestTime = len(timeStr)
+
}
+
}
+
+
// Display each language with a bar
+
for i := range count {
+
language := summary.Data.Languages[i]
+
+
// Format the language name and time with padding
+
paddedName := fmt.Sprintf("%-*s", longestName+2, language.Name)
+
timeStr := utils.PrettyPrintTime(int(language.TotalSeconds))
+
paddedTime := fmt.Sprintf("%-*s", longestTime+2, timeStr)
+
+
// Create the progress bar
+
barWidth := 25
+
bar := ""
+
percentage := language.Percent
+
for j := range barWidth {
+
if float64(j) < percentage/(100/float64(barWidth)) {
+
bar += "█"
+
} else {
+
bar += "░"
+
}
+
}
+
+
// Use different styles for different components
+
styledName := styles.Fancy.Render(paddedName)
+
styledTime := styles.Muted.Render(paddedTime)
+
styledBar := styles.Success.Render(bar)
+
styledPercent := styles.Warn.Render(fmt.Sprintf("%.2f%%", percentage))
+
+
// Print the formatted line
+
c.Printf(" %s %s %s %s\n", styledName, styledTime, styledBar, styledPercent)
+
}
+
+
c.Println()
+
}
+
+
return nil
+
}
+13 -5
main.go
···
Use: "doc",
Short: "diagnose potential hackatime issues",
RunE: handler.Doctor,
+
Args: cobra.NoArgs,
})
-
cmdTest := &cobra.Command{
+
cmd.AddCommand(&cobra.Command{
Use: "test",
Short: "send a test heartbeat to hackatime or whatever api url you provide",
RunE: handler.TestHeartbeat,
Args: cobra.NoArgs,
-
}
-
cmdTest.Flags().StringP("url", "u", "", "The base url for the hackatime client")
-
cmdTest.Flags().StringP("key", "k", "", "API key to use for authentication")
-
cmd.AddCommand(cmdTest)
+
})
+
+
cmd.AddCommand(&cobra.Command{
+
Use: "status",
+
Short: "get your hackatime stats",
+
RunE: handler.Status,
+
Args: cobra.NoArgs,
+
})
+
+
cmd.PersistentFlags().StringP("url", "u", "", "The base url for the hackatime client")
+
cmd.PersistentFlags().StringP("key", "k", "", "API key to use for authentication")
// this is where we get the fancy fang magic ✨
if err := fang.Execute(
+22
utils/main.go
···
+
package utils
+
+
import (
+
"fmt"
+
)
+
+
func PrettyPrintTime(totalSeconds int) string {
+
hours := totalSeconds / 3600
+
minutes := (totalSeconds % 3600) / 60
+
seconds := totalSeconds % 60
+
+
formattedTime := ""
+
if hours > 0 {
+
formattedTime += fmt.Sprintf("%d hours, ", hours)
+
}
+
if minutes > 0 || hours > 0 {
+
formattedTime += fmt.Sprintf("%d minutes, ", minutes)
+
}
+
formattedTime += fmt.Sprintf("%d seconds", seconds)
+
+
return formattedTime
+
}
+89
wakatime/main.go
···
return durationResp, nil
}
+
+
// Last7DaysResponse represents the response from the WakaTime Last 7 Days API endpoint.
+
// This contains detailed information about a user's coding activity over the past 7 days.
+
type Last7DaysResponse struct {
+
// Data contains coding statistics for the last 7 days
+
Data struct {
+
// TotalSeconds is the total time spent coding in seconds
+
TotalSeconds float64 `json:"total_seconds"`
+
// HumanReadableTotal is the human-readable representation of the total coding time
+
HumanReadableTotal string `json:"human_readable_total"`
+
// DailyAverage is the average time spent coding per day in seconds
+
DailyAverage float64 `json:"daily_average"`
+
// HumanReadableDailyAverage is the human-readable representation of the daily average
+
HumanReadableDailyAverage string `json:"human_readable_daily_average"`
+
// Languages is a list of programming languages used with statistics
+
Languages []struct {
+
// Name is the programming language name
+
Name string `json:"name"`
+
// TotalSeconds is the time spent coding in this language in seconds
+
TotalSeconds float64 `json:"total_seconds"`
+
// Percent is the percentage of time spent in this language
+
Percent float64 `json:"percent"`
+
// Text is the human-readable representation of time spent in this language
+
Text string `json:"text"`
+
} `json:"languages"`
+
// Editors is a list of editors used with statistics
+
Editors []struct {
+
// Name is the editor name
+
Name string `json:"name"`
+
// TotalSeconds is the time spent using this editor in seconds
+
TotalSeconds float64 `json:"total_seconds"`
+
// Percent is the percentage of time spent using this editor
+
Percent float64 `json:"percent"`
+
// Text is the human-readable representation of time spent using this editor
+
Text string `json:"text"`
+
} `json:"editors"`
+
// Projects is a list of projects worked on with statistics
+
Projects []struct {
+
// Name is the project name
+
Name string `json:"name"`
+
// TotalSeconds is the time spent on this project in seconds
+
TotalSeconds float64 `json:"total_seconds"`
+
// Percent is the percentage of time spent on this project
+
Percent float64 `json:"percent"`
+
// Text is the human-readable representation of time spent on this project
+
Text string `json:"text"`
+
} `json:"projects"`
+
} `json:"data"`
+
}
+
+
// GetLast7Days retrieves a user's coding activity summary for the past 7 days from the WakaTime API.
+
// It returns a Last7DaysResponse and an error if the request fails or returns a non-success status code.
+
func (c *Client) GetLast7Days() (Last7DaysResponse, error) {
+
req, err := http.NewRequest("GET", fmt.Sprintf("%s/users/current/stats/last_7_days", c.APIURL), nil)
+
if err != nil {
+
return Last7DaysResponse{}, fmt.Errorf("%w: %v", ErrCreatingRequest, err)
+
}
+
+
req.Header.Set("Accept", "application/json")
+
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(c.APIKey)))
+
+
resp, err := c.HTTPClient.Do(req)
+
if err != nil {
+
return Last7DaysResponse{}, fmt.Errorf("%w: %v", ErrSendingRequest, err)
+
}
+
defer resp.Body.Close()
+
+
// Read the response body for potential error messages
+
var respBody bytes.Buffer
+
_, err = respBody.ReadFrom(resp.Body)
+
if err != nil {
+
return Last7DaysResponse{}, fmt.Errorf("failed to read response body: %v", err)
+
}
+
+
respContent := respBody.String()
+
+
if resp.StatusCode == http.StatusUnauthorized {
+
return Last7DaysResponse{}, fmt.Errorf("%w: %s", ErrUnauthorized, respContent)
+
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+
return Last7DaysResponse{}, fmt.Errorf("%w: status code %d, response: %s", ErrInvalidStatusCode, resp.StatusCode, respContent)
+
}
+
+
var statsResp Last7DaysResponse
+
if err := json.Unmarshal(respBody.Bytes(), &statsResp); err != nil {
+
return Last7DaysResponse{}, fmt.Errorf("%w: %v, response: %s", ErrDecodingResponse, err, respContent)
+
}
+
+
return statsResp, nil
+
}