diff --git a/README.md b/README.md
index 8e531f6..59a2712 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,6 @@
-![.github/workflows/release.yaml](https://github.com/Faagerholm/clockify-cli/workflows/.github/workflows/release.yaml/badge.svg?branch=v1.1&event=release)
-[![CLI Best Practices](https://bestpractices.coreinfrastructure.org/projects/4331/badge)](https://bestpractices.coreinfrastructure.org/projects/4331)
-
-
-
# clockify-cli
Integrate your clocking with your favorite CLI.
-### Install:
-
-```bash
-wget https://raw.githubusercontent.com/Faagerholm/clockify-cli/master/install.sh && ./install.sh
-```
-
### Usage:
```
clockify-cli [flags]
@@ -35,13 +24,4 @@ Flags:
--config string config file (default is $HOME/.clockify-cli/config.yaml) (default "./config.yaml")
-h, --help help for clockify-cli
--viper use Viper for configuration (default true)
-```
-
-### Contributing:
-
-Please open an issue if there is something that is not working or you would like to be added to this project.
-
-### External API:
-
-Clockify has an API that this project heavily depends on. The API can be accessed by any user that has generated an API key from their user settings page.
-More information about the API can be found here: https://clockify.me/developers-api
+```
\ No newline at end of file
diff --git a/cmd/entry.go b/cmd/entry.go
deleted file mode 100644
index a0acd95..0000000
--- a/cmd/entry.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package cmd
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "time"
-
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-var startActivityCmd = &cobra.Command{
- Use: "start [project_id]",
- Short: "start timer for project. Use 'default' flag to use default project id.",
- Long: `Start timer for project. User 'default' flag to use default project id.
- You can set your default project with clockify-cli projects.
- If the flag and project id is omitted, and the default is set. Thw default will be used!`,
- Run: func(cmd *cobra.Command, args []string) {
- key := viper.Get("API-KEY").(string)
- workspace := viper.Get("WORKSPACE")
-
- loc, _ := time.LoadLocation("UTC")
- cur_time := time.Now().In(loc)
-
- start_time := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02dZ", cur_time.Year(), cur_time.Month(), cur_time.Day(),
- cur_time.Hour(), cur_time.Minute(), cur_time.Second())
-
- project := ""
- if len(args) == 0 && viper.IsSet("default-project") {
- project = viper.Get("default-project").(string)
- } else if len(args) > 0 && len(args[0]) == 23 {
- project = args[0]
- } else if len(args) > 0 && (args[0] == "d" || args[0] == "default") {
- project = viper.Get("default-project").(string)
- } else {
- fmt.Println("Could not parse arguments. Check 'start --help' for more information.")
- return
- }
-
- reqBody, err := json.Marshal(map[string]string{
- "start": start_time,
- "projectId": project,
- })
-
- if err != nil {
- log.Fatal(err)
- }
- client := &http.Client{}
- req, _ := http.NewRequest("POST", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/time-entries", workspace), bytes.NewBuffer(reqBody))
- req.Header.Set("X-API-KEY", key)
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
-
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- } else {
- log.Println("Project started")
- }
- defer resp.Body.Close()
- },
-}
-
-var stopActivityCmd = &cobra.Command{
- Use: "stop",
- Short: "Stops an active timer.",
- Run: func(cmd *cobra.Command, args []string) {
- key := viper.Get("API-KEY").(string)
- workspace := viper.Get("WORKSPACE")
- user := viper.Get("USER-ID")
-
- loc, _ := time.LoadLocation("UTC")
- cur_time := time.Now().In(loc)
-
- end_time := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02dZ", cur_time.Year(), cur_time.Month(), cur_time.Day(),
- cur_time.Hour(), cur_time.Minute(), cur_time.Second())
-
- reqBody, err := json.Marshal(map[string]string{
- "end": end_time,
- })
-
- if err != nil {
- log.Fatal(err)
- }
- client := &http.Client{}
- req, _ := http.NewRequest("PATCH", fmt.Sprintf("https://test.clockify.me/api/v1/workspaces/%s/user/%s/time-entries", workspace, user), bytes.NewBuffer(reqBody))
- req.Header.Set("X-API-KEY", key)
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
-
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- } else {
- log.Println("Project stopped")
- }
- defer resp.Body.Close()
-
- },
-}
-
-func getProjects() {
-
-}
diff --git a/cmd/project.go b/cmd/project.go
deleted file mode 100644
index 133df08..0000000
--- a/cmd/project.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package cmd
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "strconv"
- "strings"
-
- "text/tabwriter"
-
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-type project struct {
- Name string `json:"name"`
- Id string `json:"id"`
-}
-
-var projectsCmd = &cobra.Command{
- Use: "projects",
- Short: "Select default workspace project",
- Long: `Display all workspace projects and
- select the default project to use when starting a timer`,
- Run: func(cmd *cobra.Command, args []string) {
- key := viper.Get("API-KEY").(string)
- workspace := viper.Get("WORKSPACE")
- client := &http.Client{}
- req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/projects", workspace), nil)
- req.Header.Set("X-API-KEY", key)
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
-
- body, err := ioutil.ReadAll(resp.Body)
- results := []project{}
- jsonErr := json.Unmarshal(body, &results)
- if jsonErr != nil {
- log.Fatal(jsonErr)
- }
-
- writer := new(tabwriter.Writer)
- writer.Init(os.Stdout, 12, 8, 12, '\t', 0)
-
- for i, r := range results {
- if i > 0 && i%3 == 0 {
- fmt.Fprintf(writer, "\n")
- }
- fmt.Fprintf(writer, "(%d) %s\t", i+1, r.Name)
- }
- writer.Flush()
- fmt.Print("\n\nSave a project as default (number): ")
- reader := bufio.NewReader(os.Stdin)
- value, err := reader.ReadString('\n')
- if err == nil {
- l := strings.Trim(value, "\n")
- v, _ := strconv.Atoi(l)
- if err != nil {
- log.Fatal(err)
- } else {
- p := results[v-1]
- viper.Set("default-project", p.Id)
- viper.WriteConfig()
- fmt.Println("Default project set:", p)
- }
-
- }
- },
-}
-
-var offProjectsCmd = &cobra.Command{
- Use: "off-projects",
- Short: "Select which projects should be omitted from reports",
- Long: `Display all projects and select which shouldn't be included in 'balance' report.
- By not selecting these projects the CLI cannot figure out which projects not to exclude when counting your balance.`,
- Run: func(cmd *cobra.Command, args []string) {
- key := viper.Get("API-KEY").(string)
- workspace := viper.Get("WORKSPACE")
- client := &http.Client{}
- req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/projects", workspace), nil)
- req.Header.Set("X-API-KEY", key)
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
-
- body, err := ioutil.ReadAll(resp.Body)
- results := []project{}
- jsonErr := json.Unmarshal(body, &results)
- if jsonErr != nil {
- log.Fatal(jsonErr)
- }
-
- writer := new(tabwriter.Writer)
- writer.Init(os.Stdout, 12, 8, 12, '\t', 0)
-
- for i, r := range results {
- if i > 0 && i%3 == 0 {
- fmt.Fprintf(writer, "\n")
- }
- fmt.Fprintf(writer, "(%d) %s\t", i+1, r.Name)
- }
- writer.Flush()
- fmt.Print("\n\nSelect projects to exclude from report (number)\nThe projects should be comma-separated\nAny previous projects will be overwritten: ")
- reader := bufio.NewReader(os.Stdin)
- value, err := reader.ReadString('\n')
- if err == nil {
- l := strings.Trim(value, "\n")
- p := strings.Split(l, ",")
- var ps []string
- for _, s := range p {
- v, _ := strconv.Atoi(strings.TrimSpace(s))
- ps = append(ps, results[v-1].Id)
- fmt.Println(results[v-1])
- }
- if err != nil {
- log.Fatal(err)
- } else {
- viper.Set("off-projects", ps)
- viper.WriteConfig()
- fmt.Println("Off projects updated,", ps)
- }
- }
- },
-}
diff --git a/cmd/report.go b/cmd/report.go
deleted file mode 100644
index 19e41c7..0000000
--- a/cmd/report.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package cmd
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "log"
- "math"
- "net/http"
- "time"
-
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-/* Request type */
-type report struct {
- Start string `json:"dateRangeStart"`
- End string `json:"dateRangeEnd"`
- SummaryFilter *report_filter `json:"summaryFilter,omitempty"`
- SortOrder string `json:"sortOrder"`
- Users *report_user `json:"users,omitempty"`
-}
-type report_filter struct {
- Groups []string `json:"groups"`
-}
-type report_user struct {
- Ids []string `json:"ids"`
- Contains string `json:"contains"`
- Status string `json:"status"`
-}
-
-/* Response types */
-type Result struct {
- Entries []Entry `json:"groupOne"`
-}
-
-type Entry struct {
- Name string
- Duration int
- Children []groupChild
-}
-type groupChild struct {
- Duration int
- Name string
-}
-
-type partTime []struct {
- startDate string
- endDate string
- capacity float64
-}
-
-/* const */
-const (
- week_seconds = 27000 // 37.5 * 60 * 60
-)
-
-var balanceCmd = &cobra.Command{
- Use: "balance",
- Short: "Display if you're above or below zero balance.",
- Run: func(cmd *cobra.Command, args []string) {
-
- loc, _ := time.LoadLocation("UTC")
- cur_time := time.Now().In(loc)
- end_time := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02dZ", cur_time.Year(), cur_time.Month(), cur_time.Day(), cur_time.Hour(), cur_time.Minute(), cur_time.Second())
-
- var res *Result
- res = new(Result)
-
- for i := 0; i >= -1; i-- {
- cur_time = cur_time.AddDate(i, 0, 0)
- first_day := fmt.Sprintf("%d-01-01T00:00:00Z", cur_time.Year())
- last_day := fmt.Sprintf("%d-12-31T00:00:00Z", cur_time.Year())
- tmp_res, _ := getEntries(first_day, last_day)
- res.addEntries(tmp_res)
- }
-
- entry := createEntry(res)
- if entry != nil {
- first_day := findFirstDay(entry)
- first_day_str := fmt.Sprintf("%d-%02d-%02d", first_day.Year(), first_day.Month(), first_day.Day())
- log.Println("Your first day:", first_day_str)
- work_days := daysCalc(end_time[0:10], first_day_str)
-
- var partTime []struct {
- StartDate string
- EndDate string
- Capacity float64
- }
- viper.UnmarshalKey("part-time", &partTime)
- log.Println(partTime)
-
- var part_time_days float64
- var part_time_percent float64 // => 80%
- for _, part := range partTime {
- if part.EndDate == "" {
- part_time_days = daysCalc(fmt.Sprintf("%d-%02d-%02d", time.Now().Year(), time.Now().Month(), time.Now().Day()), part.StartDate)
- } else {
- part_time_days = daysCalc(part.StartDate, part.EndDate)
- }
- part_time_percent = 1 - part.Capacity
- }
- log.Println("Part time days:", part_time_days)
- log.Println("Total minutes worked:", entry.Duration)
- var balance = (float64(entry.Duration) - work_days*week_seconds + part_time_days*week_seconds*part_time_percent) / (60 * 60)
- fmt.Printf("----------------------\nYou have worked %.0f days.\nYou have worked %.2f hours, and the recommended amount is %.2f hours.\nWhich makes your balance %dh%dmin (%.2f)\nThis calculator includes today.\n",
- work_days, // days
- float64(entry.Duration)/float64(60.0*60.0), // hours worked
- float64((work_days*week_seconds-part_time_days*week_seconds*part_time_percent)/(60*60)), // recommended hours
- int(balance), // balance (hours)
- int((math.Abs(balance)-math.Abs(float64(int(balance))))*60), // balance (minutes)
- balance) // balance (decimal)
- } else {
- fmt.Println("Could not get report")
- }
- },
-}
-
-func (result *Result) addEntries(newResult *Result) []Entry {
- result.Entries = append(result.Entries, newResult.Entries...)
- return result.Entries
-}
-
-func findFirstDay(entry *Entry) time.Time {
- first_day := time.Now()
- for _, day := range entry.Children {
- d, _ := time.Parse("2006-01-02", day.Name)
- if first_day.Sub(d) > 0 {
- first_day = d
- }
- }
- return first_day
-}
-
-func createEntry(result *Result) *Entry {
- var entry *Entry
- entry = new(Entry)
- entry.Name = result.Entries[0].Name
- for _, e := range result.Entries {
- entry.Children = append(entry.Children, e.Children...)
- entry.Duration += e.Duration
- }
- return entry
-}
-
-func getEntries(start_time string, end_time string) (*Result, error) {
- user := viper.GetString("USER-ID")
-
- if len(user) == 0 {
- fmt.Println("User not set, please run `clockify user` to update the current user.")
- return nil, errors.New("User not set, please run `clockify user`")
- }
-
- users := report_user{
- Ids: []string{user},
- Contains: "CONTAINS",
- Status: "All",
- }
- filter := report_filter{
- Groups: []string{"user", "date"},
- }
- reqBody, err := json.Marshal(report{
- Start: start_time,
- End: end_time,
- SummaryFilter: &filter,
- SortOrder: "Ascending",
- Users: &users,
- })
-
- if err != nil {
- log.Fatal(err)
- }
- res, err := requestReport(reqBody)
-
- return res, err
-}
-
-func requestReport(body []byte) (*Result, error) {
- key := viper.GetString("API-KEY")
- workspace := viper.GetString("WORKSPACE")
-
- if len(key) == 0 {
- return nil, errors.New("API KEY NOT SET! Please run clockify app-key")
- }
-
- client := &http.Client{}
- req, _ := http.NewRequest("POST", fmt.Sprintf("https://reports.api.clockify.me/v1/workspaces/%s/reports/summary", workspace), bytes.NewBuffer(body))
- req.Header.Set("X-API-KEY", key)
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
-
- resp, err := client.Do(req)
-
- if err != nil {
- return nil, err
- }
-
- defer resp.Body.Close()
-
- res := new(Result)
-
- json.NewDecoder(resp.Body).Decode(&res)
- return res, err
-}
-
-func daysCalc(today string, start string) float64 {
-
- // Get first day of reports (of this year)
- first_day, _ := time.Parse("2006-01-02", start)
-
- // today
- now, _ := time.Parse("2006-01-02", today)
- days := (now.Sub(first_day).Hours() / 24) + 1
-
- // This is ugly, but works for now. Use fancy algorithm later
- var this_day = first_day
- for (this_day.Sub(now).Hours() / 24) <= 0 {
- // TODO: Compare if day == holiday
- if this_day.Weekday().String() == "Saturday" || this_day.Weekday().String() == "Sunday" {
- days -= 1
- }
- // Add one day.
- this_day = this_day.Add(time.Hour * 24)
- }
- return days
-}
-
-func stringInSlice(a string, list interface{}) bool {
-
- listSlice, ok := list.([]interface{})
- if !ok {
- return false
- }
- for _, v := range listSlice {
- if a == v {
- return true
- }
- }
- return false
-}
diff --git a/cmd/root.go b/cmd/root.go
deleted file mode 100644
index 7eae66b..0000000
--- a/cmd/root.go
+++ /dev/null
@@ -1,206 +0,0 @@
-package cmd
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "os"
- "strings"
-
- homedir "github.com/mitchellh/go-homedir"
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-var (
- home = os.Getenv("HOME")
-
- cfgPath string
- // Used for flags.
- cfgFile string
- defFlag bool
- rootCmd = &cobra.Command{
- Use: "clockify-cli",
- Short: "A Clockify-cli",
- Run: func(cmd *cobra.Command, args []string) {
- // Execute empty command
- },
- }
-)
-
-var initCmd = &cobra.Command{
- Use: "init",
- Run: func(cmd *cobra.Command, args []string) {
- key := ""
- reader := bufio.NewReader(os.Stdin)
- if len(args) == 0 {
- fmt.Print("Please enter your api-key:")
- in, _ := reader.ReadString('\n')
- key = strings.Trim(in, "\n")
- } else {
- key = args[0]
- }
-
- fmt.Print("Are you sure you want to add a new key (Y/N): ")
- char, _, err := reader.ReadRune()
-
- if err != nil {
- fmt.Println(err)
- }
-
- switch char {
- case 'Y':
- viper.Set("API-KEY", key)
- fmt.Println("Saving", viper.Get("API-KEY"), `as your user key, this can be changed later by initializing the same command.
-as of now, no more the one key can be used at the same time.`)
- err := viper.WriteConfig() // Find and read the config file
- if err != nil { // Handle errors reading the config file
- panic(fmt.Errorf("Fatal error config file: %s \n", err))
- }
-
- case 'N':
- fmt.Println("The key was NOT added.")
- }
-
- client := &http.Client{}
- req, _ := http.NewRequest("GET", "https://api.clockify.me/api/v1/user", nil)
- req.Header.Set("X-API-KEY", key)
-
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
-
- var result map[string]interface{}
-
- json.NewDecoder(resp.Body).Decode(&result)
- if result != nil {
- fmt.Println("Found user:", result["email"], result["id"])
-
- viper.Set("USER-ID", result["id"])
- viper.Set("WORKSPACE", result["activeWorkspace"])
- fmt.Println("Updating config with user id.")
- fmt.Println("You're ready to go.. check help for more commands")
- viper.WriteConfig()
- } else {
- log.Println("Could not find user.. try again later.")
- }
- },
-}
-
-var addKeyCmd = &cobra.Command{
- Use: "add-key [API-KEY]",
- Short: "Add users API-KEY",
- Long: `Add users API-KEY, get the key from clockify.me/user/settings.
- At the bottom of the page, generate KEY.`,
- Run: func(cmd *cobra.Command, args []string) {
-
- key := ""
- reader := bufio.NewReader(os.Stdin)
- if len(args) == 0 {
- fmt.Print("Please enter your api-key:")
- in, _ := reader.ReadString('\n')
- key = strings.Trim(in, "\n")
- } else {
- key = args[0]
- }
-
- fmt.Print("Are you sure you want to add a new key (Y/N): ")
- char, _, err := reader.ReadRune()
-
- if err != nil {
- fmt.Println(err)
- }
-
- switch char {
- case 'Y':
- viper.Set("API-KEY", key)
- fmt.Println("Saving", viper.Get("API-KEY"), `as your user key, this can be changed later by initializing the same command.
-as of now, no more the one key can be used at the same time.`)
- err := viper.WriteConfig() // Find and read the config file
- if err != nil { // Handle errors reading the config file
- panic(fmt.Errorf("Fatal error config file: %s \n", err))
- }
-
- case 'N':
- fmt.Println("The key was NOT added.")
- }
-
- },
-}
-var resetViperCmd = &cobra.Command{
- Use: "reset",
- Short: "Resets viper values",
- Run: func(cmd *cobra.Command, args []string) {
- viper.Reset()
- },
-}
-
-// Execute executes the root command.
-func Execute() error {
- return rootCmd.Execute()
-}
-
-func init() {
-
- home, _ := homedir.Dir()
- cobra.OnInitialize(initConfig)
-
- // viper.Debug()
-
- rootCmd.PersistentFlags().StringVar(&cfgFile, "config", home+"/.clockify-cli/config.yaml", "config file (default is $HOME/.clockify-cli/config.yaml)")
- rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
- // viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
-
- //root.go
- rootCmd.AddCommand(addKeyCmd)
- rootCmd.AddCommand(resetViperCmd)
- rootCmd.AddCommand(initCmd)
- // project.go
- rootCmd.AddCommand(projectsCmd)
- rootCmd.AddCommand(offProjectsCmd)
- // entry.go
- rootCmd.AddCommand(startActivityCmd)
- rootCmd.AddCommand(stopActivityCmd)
-
- startActivityCmd.Flags().BoolVarP(&defFlag, "default", "d", false, "Use default project id.")
- // viper.BindPFlag("default", startActivityCmd.Flags().Lookup("default-project"))
- // user.go
- rootCmd.AddCommand(userCmd)
- // workspace.go
- rootCmd.AddCommand(workspaceCmd)
- // utils.go
- rootCmd.AddCommand(versionCmd)
- // report.go
- rootCmd.AddCommand(balanceCmd)
-}
-
-func er(msg interface{}) {
- fmt.Println("Error:", msg)
- os.Exit(1)
-}
-
-func initConfig() {
- if cfgFile != "" {
- // Use config file from the flag.
- viper.SetConfigFile(cfgFile)
- } else {
- // Find home directory.
- home, err := homedir.Dir()
- if err != nil {
- er(err)
- }
- viper.AddConfigPath(home + "/.clockify-cli")
- viper.SetConfigName("config")
- viper.SetConfigType("yaml")
- }
-
- viper.AutomaticEnv()
-
- if err := viper.ReadInConfig(); err == nil {
- // fmt.Println("Using config file:", viper.ConfigFileUsed())
- }
-}
diff --git a/cmd/user.go b/cmd/user.go
deleted file mode 100644
index d6aeaf5..0000000
--- a/cmd/user.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package cmd
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "net/http"
-
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-var userCmd = &cobra.Command{
- Use: "user",
- Short: "get current user",
- Run: func(cmd *cobra.Command, args []string) {
- key := viper.GetString("API-KEY")
- client := &http.Client{}
- req, _ := http.NewRequest("GET", "https://api.clockify.me/api/v1/user", nil)
- req.Header.Set("X-API-KEY", key)
-
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
-
- var result map[string]interface{}
-
- json.NewDecoder(resp.Body).Decode(&result)
- if result != nil {
- fmt.Println("Found user:", result["email"], result["id"])
-
- viper.Set("USER-ID", result["id"])
- viper.Set("WORKSPACE", result["activeWorkspace"])
- fmt.Println("Updating config with user id.")
- viper.WriteConfig()
- } else {
- log.Println("Could not find user.. try again later.")
- }
- },
-}
diff --git a/cmd/utils.go b/cmd/utils.go
deleted file mode 100644
index f9d1c7d..0000000
--- a/cmd/utils.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package cmd
-
-import (
- "fmt"
-
- "github.com/spf13/cobra"
-)
-
-var versionCmd = &cobra.Command{
- Use: "version",
- Short: "Print the version number of Clockify-cli",
- Long: `All software has versions. This is clockify-cli's`,
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println("Clockify-cli application version 1.1 -- HEAD")
- },
-}
diff --git a/cmd/workspace.go b/cmd/workspace.go
deleted file mode 100644
index 0521483..0000000
--- a/cmd/workspace.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package cmd
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "net/http"
-
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-)
-
-var workspaceCmd = &cobra.Command{
- Use: "workspace",
- Short: "Get workspaces",
- Run: func(cmd *cobra.Command, args []string) {
- key := viper.Get("API-KEY").(string)
- client := &http.Client{}
- req, _ := http.NewRequest("GET", "https://api.clockify.me/api/v1/workspaces", nil)
- req.Header.Set("X-API-KEY", key)
- req.Header.Set("Host", "api.clockify.me")
-
- resp, err := client.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- defer resp.Body.Close()
-
- var result map[string]interface{}
-
- json.NewDecoder(resp.Body).Decode(&result)
-
- fmt.Println("Found user:", result)
- },
-}
diff --git a/go.mod b/go.mod
index b367766..aaa9725 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/joho/godotenv v1.3.0
github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 // indirect
github.com/landoop/tableprinter v0.0.0-20200805134727-ea32388e35c1
+ github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/sirupsen/logrus v1.2.0
diff --git a/go.sum b/go.sum
index ae984e4..f62cf2a 100644
--- a/go.sum
+++ b/go.sum
@@ -25,6 +25,10 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -118,6 +122,8 @@ github.com/landoop/tableprinter v0.0.0-20200805134727-ea32388e35c1/go.mod h1:f0X
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
+github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
@@ -250,6 +256,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/install.sh b/install.sh
index 16a160e..80037d0 100755
--- a/install.sh
+++ b/install.sh
@@ -21,12 +21,12 @@ fi
echo "Creating alias for clockify"
if [ -f "$HOME/.zshrc" ]; then
echo "Adding it to your zshrc file."
- alias clockify >/dev/null 2>&1 && echo "clockify is set as an alias, skipping update of source file." || echo "alias clockify='$PROJECT_HOME/clockify-cli'" >> $HOME/.zshrc
+ alias clockify >/dev/null 2>&1 && echo "clockify is set as an alias, skipping update of source file." || echo "alias clockify='$PROJECT_HOME/clockify-cli'" >>$HOME/.zshrc
elif [ -f "$HOME/.bash_profile" ]; then
echo "Adding it to your bash_profile file."
alias clockify >/dev/null 2>&1 && echo "clockify is set as an alias, skipping update of source file." || echo "alias clockify='$PROJECT_HOME/clockify-cli'" >>$HOME/.bash_profile
-else
- echo "Could not fine a terminal profile, please manually add 'alias clockify='$PROJECT_HOME/clockify-cli' to your profile.";
+else
+ echo "Could not fine a terminal profile, please manually add 'alias clockify='$PROJECT_HOME/clockify-cli' to your profile."
fi
processor="$(uname -m)"
@@ -164,7 +164,7 @@ fi
echo 'Done!'
echo '-------------------'
-mv clockify-cli $PROJECT_HOME/clockify-cli
+mv clockify-cli $PROJECT_HOME
echo 'To get started you will need a API-key. The key can be genereted on your profile page.'
$PROJECT_HOME/clockify-cli init
@@ -173,4 +173,3 @@ $PROJECT_HOME/clockify-cli user
echo "Initialization completed, please run 'clockify help' to get started."
echo "You will have to restart any active terminal instance to access you newly created command."
echo "You can also read the source file with e.g. 'source ~/.zshrc'".
-
diff --git a/main.go b/main.go
index 6de6085..32ccd0c 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,9 @@
package main
import (
- "github.com/Faagerholm/clockify-cli/cmd"
+ "github.com/Faagerholm/clockify-cli/pkg"
)
func main() {
- cmd.Execute()
+ pkg.Execute()
}
diff --git a/pkg/API/basic.go b/pkg/API/basic.go
new file mode 100644
index 0000000..bc086df
--- /dev/null
+++ b/pkg/API/basic.go
@@ -0,0 +1,137 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "time"
+
+ model "github.com/Faagerholm/clockify-cli/pkg/Model"
+ utils "github.com/Faagerholm/clockify-cli/pkg/Utils"
+)
+
+func Start(start_time time.Time, project string) {
+
+ api_key, workspace := utils.ExtractAPIKeyAndWorkspace()
+
+ start_time_str := start_time.Format("2006-01-02T15:04:05Z")
+
+ reqBody, err := json.Marshal(map[string]string{
+ "start": start_time_str,
+ "projectId": project,
+ })
+
+ if err != nil {
+ log.Fatal(err)
+ }
+ client := &http.Client{}
+ req, _ := http.NewRequest("POST", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/time-entries", workspace), bytes.NewBuffer(reqBody))
+ req.Header.Set("X-API-KEY", api_key)
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ } else {
+ fmt.Println("Project started")
+ }
+ defer resp.Body.Close()
+}
+
+func Stop(end_time_str string) model.Entry {
+ key, workspace, user := utils.ExtractAPIKeyAndWorkspaceAndUserId()
+
+ reqBody, err := json.Marshal(map[string]string{
+ "end": end_time_str,
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ client := &http.Client{}
+ req, _ := http.NewRequest("PATCH", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/user/%s/time-entries", workspace, user), bytes.NewBuffer(reqBody))
+ req.Header.Set("X-API-KEY", key)
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ } else {
+ log.Println("Project stopped")
+ }
+ defer resp.Body.Close()
+
+ var entry model.Entry
+ json.NewDecoder(resp.Body).Decode(&entry)
+ return entry
+}
+
+func GetProjects() (
+ []model.Project,
+ error,
+) {
+ key, workspace := utils.ExtractAPIKeyAndWorkspace()
+
+ client := &http.Client{}
+ req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/projects", workspace), nil)
+ req.Header.Set("X-API-KEY", key)
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ results := []model.Project{}
+ jsonErr := json.Unmarshal(body, &results)
+ if jsonErr != nil {
+ return nil, jsonErr
+ }
+ return results, nil
+}
+
+func GetUser() *model.User {
+ key := utils.ExtractAPIKey()
+
+ client := &http.Client{}
+ req, _ := http.NewRequest("GET", "https://api.clockify.me/api/v1/user", nil)
+ req.Header.Set("X-API-KEY", key)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer resp.Body.Close()
+
+ var user *model.User
+
+ json.NewDecoder(resp.Body).Decode(&user)
+ return user
+}
+
+func AddDescription(entryID string, updateEntry model.UpdateEntry) {
+ key, workspace := utils.ExtractAPIKeyAndWorkspace()
+
+ reqBody, err := json.Marshal(updateEntry)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ client := &http.Client{}
+ req, _ := http.NewRequest("PUT", fmt.Sprintf("https://api.clockify.me/api/v1/workspaces/%s/time-entries/%s", workspace, entryID), bytes.NewBuffer(reqBody))
+ req.Header.Set("X-API-KEY", key)
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println(resp)
+ defer resp.Body.Close()
+}
diff --git a/pkg/API/entries.go b/pkg/API/entries.go
new file mode 100644
index 0000000..3a6ae01
--- /dev/null
+++ b/pkg/API/entries.go
@@ -0,0 +1,128 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+
+ model "github.com/Faagerholm/clockify-cli/pkg/Model"
+ "github.com/spf13/viper"
+)
+
+// GetAllEntries returns all entries from the Clockify API.
+// From the first day, until today
+func GetAllEntries() (
+ *model.ResultUser,
+ error,
+) {
+ var first_day_str, last_day_str string
+
+ loc, _ := time.LoadLocation("UTC")
+ now := time.Now().In(loc)
+
+ last_day_str = now.Format("2006-01-02T15:04:05Z")
+ first_day_str = fmt.Sprintf("%d-01-01T00:00:00Z", now.Year())
+
+ users, err := GetEntries(first_day_str, last_day_str)
+ if err != nil {
+ return nil, err
+ }
+ if len(users) != 1 {
+ return nil, errors.New(fmt.Sprintf("Invalid entries count found, expected 1, got %d", len(users)))
+ }
+ user := users[0]
+ year := now.Year() - 1
+
+ for true {
+ first_day_str = fmt.Sprintf("%d-01-01T00:00:00Z", year)
+ last_day_str = fmt.Sprintf("%d-12-31T23:59:59Z", year)
+
+ users, err := GetEntries(first_day_str, last_day_str)
+ if err != nil {
+ return nil, err
+ }
+ if len(users) != 1 {
+ break
+ }
+ user.Duration += users[0].Duration
+ user.Entries = append(user.Entries, users[0].Entries...)
+ year--
+ }
+
+ if err != nil {
+ log.Fatal(err)
+ }
+ return &user, err
+}
+
+func GetEntries(start_date, end_date string) ([]model.ResultUser, error) {
+ result, err := getEntries(start_date, end_date)
+ if err != nil {
+ return nil, err
+ }
+ return result.Entries, nil
+}
+
+func getEntries(start_date, end_date string) (*model.Result, error) {
+ user := viper.GetString("USER-ID")
+
+ if len(user) == 0 {
+ return nil, errors.New("HANDLE ME")
+ }
+
+ reqBody, err := marshalRequestBody(user, start_date, end_date)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ res, err := requestReport(reqBody)
+ return res, err
+}
+
+func requestReport(body []byte) (*model.Result, error) {
+ key := viper.GetString("API-KEY")
+ workspace := viper.GetString("WORKSPACE")
+
+ if len(key) == 0 {
+ return nil, errors.New("API KEY NOT SET! Please run clockify app-key")
+ }
+
+ client := &http.Client{}
+ req, _ := http.NewRequest("POST", fmt.Sprintf("https://reports.api.clockify.me/v1/workspaces/%s/reports/summary", workspace), bytes.NewBuffer(body))
+ req.Header.Set("X-API-KEY", key)
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ resp, err := client.Do(req)
+
+ if err != nil {
+ return nil, err
+ }
+
+ defer resp.Body.Close()
+
+ res := new(model.Result)
+
+ json.NewDecoder(resp.Body).Decode(&res)
+ return res, err
+}
+
+func marshalRequestBody(user, start_date, end_date string) ([]byte, error) {
+
+ return json.Marshal(model.Report{
+ Start: start_date,
+ End: end_date,
+ SummaryFilter: &model.Report_filter{
+ Groups: []string{"user", "date"},
+ },
+ SortOrder: "Ascending",
+ Users: &model.Report_user{
+ Ids: []string{user},
+ Contains: "CONTAINS",
+ Status: "All",
+ },
+ })
+}
diff --git a/pkg/Controller/Projects_controller.go b/pkg/Controller/Projects_controller.go
new file mode 100644
index 0000000..8ea1620
--- /dev/null
+++ b/pkg/Controller/Projects_controller.go
@@ -0,0 +1,107 @@
+package controller
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ api "github.com/Faagerholm/clockify-cli/pkg/API"
+ "github.com/manifoldco/promptui"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var DefaultProjectCmd = &cobra.Command{
+ Use: "default-project",
+ Short: "Select default workspace project",
+ Long: `Display all workspace projects and
+ select the default project to use when starting a timer`,
+ Run: func(cmd *cobra.Command, args []string) {
+ DefaultProject()
+ },
+}
+
+var ListProjectsCmd = &cobra.Command{
+ Use: "list-projects",
+ Short: "List all projects",
+ Long: `Display all projects in the current workspace`,
+ Run: func(cmd *cobra.Command, args []string) {
+ ListProjects()
+ },
+}
+
+func ListProjects() {
+ fmt.Println("Listing projects, this may take a while...")
+ fmt.Println("Note, noting happens if you select a project as of now")
+ projects, err := api.GetProjects()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ templates := &promptui.SelectTemplates{
+ Label: "{{ . }}?",
+ Active: "\U0001F449 {{ .Name | cyan }}",
+ Inactive: " {{ .Name | cyan }}",
+ Selected: "\U0001F449 {{ .Name | red | cyan }}",
+ }
+
+ searcher := func(input string, index int) bool {
+ project := projects[index]
+ name := strings.Replace(strings.ToLower(project.Name), " ", "", -1)
+ input = strings.Replace(strings.ToLower(input), " ", "", -1)
+
+ return strings.Contains(strings.ToLower(name), input)
+ }
+ prompt := promptui.Select{
+ Label: "This is all projects I could find",
+ Items: projects,
+ Templates: templates,
+ Size: 20,
+ Searcher: searcher,
+ }
+
+ _, _, err = prompt.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func DefaultProject() {
+
+ projects, err := api.GetProjects()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ templates := &promptui.SelectTemplates{
+ Label: "{{ . }}?",
+ Active: "\U0001F449 {{ .Name | cyan }}",
+ Inactive: " {{ .Name | cyan }}",
+ Selected: "\U0001F449 {{ .Name | red | cyan }}",
+ }
+
+ searcher := func(input string, index int) bool {
+ project := projects[index]
+ name := strings.Replace(strings.ToLower(project.Name), " ", "", -1)
+ input = strings.Replace(strings.ToLower(input), " ", "", -1)
+
+ return strings.Contains(strings.ToLower(name), input)
+ }
+ prompt := promptui.Select{
+ Label: "Select default project (this is used when starting a timer)",
+ Items: projects,
+ Templates: templates,
+ Size: 10,
+ Searcher: searcher,
+ }
+
+ i, _, err := prompt.Run()
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+ viper.Set("default-project", projects[i])
+ viper.WriteConfig()
+ fmt.Println("Default project set:", projects[i].Name)
+
+}
diff --git a/pkg/Controller/authenticate_controller.go b/pkg/Controller/authenticate_controller.go
new file mode 100644
index 0000000..15c41e4
--- /dev/null
+++ b/pkg/Controller/authenticate_controller.go
@@ -0,0 +1,152 @@
+package controller
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ api "github.com/Faagerholm/clockify-cli/pkg/API"
+ utils "github.com/Faagerholm/clockify-cli/pkg/Utils"
+ "github.com/manifoldco/promptui"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var GetUserCmd = &cobra.Command{
+ Use: "current-user",
+ Short: "get current user",
+ Run: func(cmd *cobra.Command, args []string) {
+ getUser()
+ },
+}
+
+var AddKeyCmd = &cobra.Command{
+ Use: "add-key [API-KEY]",
+ Short: "Add users API-KEY, this will store it in a yaml file.",
+ Long: `Add users API-KEY, get the key from clockify.me/user/settings.
+ At the bottom of the page, generate KEY.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ key := ""
+ fmt.Println(len(args))
+ if len(args) == 0 {
+ key = viper.GetString("API-KEY")
+ } else {
+ key = args[0]
+ }
+ AddKey(key)
+ },
+}
+
+var SetupCmd = &cobra.Command{
+ Use: "setup",
+ Short: "Setup",
+ Long: `Setup the application, this will ask you for your API key and store it in a yaml file.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ Authenticate()
+ },
+}
+
+func Authenticate() {
+
+ // This is a method for setting the API key and storing it in a yaml file
+ key := viper.GetString("API-KEY")
+
+ prompt := promptui.Prompt{
+ Label: "Do you wish to add a new API key?",
+ IsConfirm: true,
+ }
+
+ result, err := prompt.Run()
+
+ if err != nil {
+ // fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+
+ if strings.ToLower(result) == "y" {
+ prompt := promptui.Prompt{
+ Label: "Enter your API key",
+ Default: key,
+ AllowEdit: true,
+ }
+
+ result, err := prompt.Run()
+
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+
+ viper.Set("API-KEY", result)
+ viper.WriteConfig()
+
+ user := api.GetUser()
+ if user != nil {
+ viper.Set("USER-ID", user.ID)
+ viper.Set("WORKSPACE", user.ActiveWorkspace)
+
+ fmt.Println("Updating config with user id.")
+ fmt.Println("You're ready to go.. check help for more commands")
+ viper.WriteConfig()
+ } else {
+ log.Println("Could not find user.. try again later.")
+ }
+ }
+
+}
+
+func getUser() {
+ user := api.GetUser()
+ if user != nil {
+ workspace := utils.ExtractWorksapce()
+ if user.ActiveWorkspace != workspace {
+ viper.Set("WORKSPACE", user.ActiveWorkspace)
+ viper.WatchConfig()
+ }
+ fmt.Printf("Active user: %s\n", user.Name)
+ } else {
+ fmt.Println("Could not find user.. try again later.")
+ }
+}
+
+func AddKey(key string) {
+ prompt := promptui.Prompt{
+ Label: "Enter your API key",
+ Default: key,
+ AllowEdit: true,
+ }
+
+ result, err := prompt.Run()
+
+ if err != nil {
+ // fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+ key = result
+
+ prompt = promptui.Prompt{
+ Label: "Do you wish to add a new API key",
+ IsConfirm: true,
+ }
+
+ result, err = prompt.Run()
+
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+
+ switch strings.ToLower(result) {
+ case "y":
+ viper.Set("API-KEY", key)
+ fmt.Println("Saving", viper.Get("API-KEY"), `as your user key, this can be changed later by initializing the same command.
+As of now, no more the one key can be used at the same time.`)
+ err := viper.WriteConfig()
+ if err != nil {
+ panic(fmt.Errorf("Fatal error in config file: %s \n", err))
+ }
+
+ case "n":
+ fmt.Println("The key was NOT added.")
+ }
+}
diff --git a/pkg/Controller/balance_controller.go b/pkg/Controller/balance_controller.go
new file mode 100644
index 0000000..c08d916
--- /dev/null
+++ b/pkg/Controller/balance_controller.go
@@ -0,0 +1,134 @@
+package controller
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ api "github.com/Faagerholm/clockify-cli/pkg/API"
+ model "github.com/Faagerholm/clockify-cli/pkg/Model"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var CheckBalanceCmd = &cobra.Command{
+ Use: "check-balance",
+ Short: "Check balance",
+ Long: `Check the balance of the current account`,
+ Run: func(cmd *cobra.Command, args []string) {
+ CheckBalance()
+ },
+}
+
+var AddPartTimeCmd = &cobra.Command{
+ Use: "add-part-time",
+ Short: "Add part-time work to your account",
+ Long: `Add part-time, if you have been working less during a time period.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ AddPartTimeTimespan()
+ },
+}
+
+func CheckBalance() {
+ user, err := api.GetAllEntries()
+
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ first_day := extractFirstDayFromEntries(user.Entries)
+ expected_work, _ := countExpectedWorkingTime(first_day, time.Now())
+
+ balance := countBalance(user.Duration, expected_work)
+
+ fmt.Printf("Balance: %.2f hours\n", balance)
+}
+
+func AddPartTimeTimespan() {
+ var partTimes []model.PartTime
+ viper.UnmarshalKey("part-time", &partTimes)
+
+ var newPartTime model.PartTime
+ fmt.Print("Enter start date (YYYY-MM-DD): ")
+ fmt.Scanln(&newPartTime.Start)
+ fmt.Print("Enter end date (YYYY-MM-DD): ")
+ fmt.Scanln(&newPartTime.End)
+ fmt.Print("Enter capacity (0-100): ")
+ fmt.Scanln(&newPartTime.Capacity)
+
+ partTimes = append(partTimes, newPartTime)
+ viper.Set("part-time", partTimes)
+ viper.WriteConfig()
+
+ fmt.Printf("Added part-time: %v\n", newPartTime)
+}
+
+func extractFirstDayFromEntries(entries []model.ReportEntry) time.Time {
+ first_day := time.Now()
+ for _, day := range entries {
+ d, _ := time.Parse("2006-01-02", day.Date)
+ if first_day.Sub(d) > 0 {
+ first_day = d
+ }
+ }
+ return first_day
+}
+
+func countWorkingDaysBetweenTwoDays(start, end time.Time) int32 {
+
+ days := int32(end.Sub(start).Hours()/24) + 1
+
+ // This is ugly, but works for now. Use fancy algorithm later
+ // Maybe some AI >_<
+ var this_day = start
+ for (this_day.Sub(end).Hours() / 24) <= 0 {
+ // We are marking holidays in clockify, so we want to include them
+ if this_day.Weekday().String() == "Saturday" || this_day.Weekday().String() == "Sunday" {
+ days -= 1
+ }
+ // Add one day.
+ this_day = this_day.Add(time.Hour * 24)
+ }
+ return days
+}
+
+func countExpectedWorkingTime(start, end time.Time) (int64, int32) {
+ working_days := countWorkingDaysBetweenTwoDays(start, end)
+ expected_working_days_in_seconds := int64(float64(working_days) * 7.5 * 60.0 * 60.0)
+ expected_working_days_in_seconds = subtractPartTimeWork(expected_working_days_in_seconds)
+ return expected_working_days_in_seconds, working_days
+}
+
+func subtractPartTimeWork(duration int64) int64 {
+ var partTimes []model.PartTime
+ viper.UnmarshalKey("part-time", &partTimes)
+
+ for _, partTime := range partTimes {
+ start, err := time.Parse("2006-01-02", partTime.Start)
+ if err != nil {
+ log.Printf("Error parsing start time of part-time: %v\n", err)
+ return duration
+ }
+ end, err := time.Parse("2006-01-02", partTime.End)
+ if err != nil {
+ log.Printf("Error parsing end date of part-time: %v\n", err)
+ return duration
+ }
+ days := countWorkingDaysBetweenTwoDays(start, end)
+ reduction := int64(float64(days) * (7.5 * float64(100-partTime.Capacity) / 100) * 60.0 * 60.0)
+ duration -= reduction
+ }
+ return duration
+}
+
+// Count balance, duration is in seconds
+func countBalance(duration int64, expected_working_days_in_seconds int64) float64 {
+ balance_in_seconds := duration - expected_working_days_in_seconds
+ balance_in_hours := float64(balance_in_seconds) / 3600.0
+ return balance_in_hours
+}
diff --git a/pkg/Controller/menu_controller.go b/pkg/Controller/menu_controller.go
new file mode 100644
index 0000000..f5f4870
--- /dev/null
+++ b/pkg/Controller/menu_controller.go
@@ -0,0 +1,61 @@
+package controller
+
+import (
+ "fmt"
+
+ model "github.com/Faagerholm/clockify-cli/pkg/Model"
+ "github.com/manifoldco/promptui"
+ "github.com/spf13/cobra"
+)
+
+var MenuCmd = &cobra.Command{
+ Use: "menu",
+ Short: "Select action to perform",
+ Long: `Display all available actions and
+ select the action to perform`,
+ Run: func(cmd *cobra.Command, args []string) {
+ Menu()
+ },
+}
+
+func Menu() {
+
+ templates := &promptui.SelectTemplates{
+ Label: "{{ . }}?",
+ Active: "\U0001F449 {{ .Name | cyan }}",
+ Inactive: " {{ .Name | cyan }}",
+ Selected: "\U0001F449 {{ .Name | red | cyan }}",
+ }
+
+ prompt := promptui.Select{
+ Label: "Select action",
+ Items: model.MainMenuActions,
+ Templates: templates,
+ Size: 10,
+ }
+
+ i, _, err := prompt.Run()
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+
+ switch model.MainMenuActions[i] {
+ case model.MainMenuActionChangeAPIKey:
+ Authenticate()
+ case model.MainMenuActionStart:
+ StartProject()
+ case model.MainMenuActionStop:
+ StopTimer()
+ case model.MainMenuActionShowProjects:
+ ListProjects()
+ case model.MainMenuActionCheckBalance:
+ CheckBalance()
+ case model.MainMenuActionSetPartTime:
+ AddPartTimeTimespan()
+ case model.MainMenuActionQuit:
+ fmt.Println("Bye!")
+ default:
+ fmt.Println("Unknown action")
+ }
+}
diff --git a/pkg/Controller/start_controller.go b/pkg/Controller/start_controller.go
new file mode 100644
index 0000000..b029954
--- /dev/null
+++ b/pkg/Controller/start_controller.go
@@ -0,0 +1,95 @@
+package controller
+
+import (
+ "fmt"
+ "log"
+ "strings"
+ "time"
+
+ api "github.com/Faagerholm/clockify-cli/pkg/API"
+ model "github.com/Faagerholm/clockify-cli/pkg/Model"
+ "github.com/manifoldco/promptui"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var StartTimerCmd = &cobra.Command{
+ Use: "start-timer",
+ Short: "Select a project and start a timer",
+ Long: `Display all projects in the current workspace and
+ select the project to start a timer in. A default project can be used`,
+ Run: func(cmd *cobra.Command, args []string) {
+ StartProject()
+ },
+}
+
+func StartProject() {
+
+ project := checkDefaultProject()
+
+ if project == nil {
+
+ projects, err := api.GetProjects()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ templates := &promptui.SelectTemplates{
+ Label: "{{ . }}?",
+ Active: "\U0001F449 {{ .Name | cyan }}",
+ Inactive: " {{ .Name | cyan }}",
+ Selected: "\U0001F449 {{ .Name | red | cyan }}",
+ }
+
+ searcher := func(input string, index int) bool {
+ project := projects[index]
+ name := strings.Replace(strings.ToLower(project.Name), " ", "", -1)
+ input = strings.Replace(strings.ToLower(input), " ", "", -1)
+
+ return strings.Contains(strings.ToLower(name), input)
+ }
+ prompt := promptui.Select{
+ Label: "Select default project (this is used when starting a timer)",
+ Items: projects,
+ Templates: templates,
+ Size: 10,
+ Searcher: searcher,
+ }
+
+ i, _, err := prompt.Run()
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+ project = &projects[i]
+ }
+ loc, _ := time.LoadLocation("UTC")
+ now := time.Now().In(loc)
+
+ api.Start(now, project.ID)
+
+}
+
+func checkDefaultProject() *model.Project {
+ var defaultProject *model.Project
+ viper.UnmarshalKey("default-project", &defaultProject)
+ if defaultProject == nil {
+ fmt.Println("No default project set")
+ return nil
+ }
+
+ prompt := promptui.Select{
+ Label: "Do you wish to use your default project?",
+ Items: []string{"Yes", "No"},
+ }
+ i, _, err := prompt.Run()
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return nil
+ }
+ if i == 1 {
+ return nil
+ }
+
+ return defaultProject
+}
diff --git a/pkg/Controller/stop_controller.go b/pkg/Controller/stop_controller.go
new file mode 100644
index 0000000..d9b3bb6
--- /dev/null
+++ b/pkg/Controller/stop_controller.go
@@ -0,0 +1,53 @@
+package controller
+
+import (
+ "fmt"
+ "time"
+
+ api "github.com/Faagerholm/clockify-cli/pkg/API"
+ model "github.com/Faagerholm/clockify-cli/pkg/Model"
+ "github.com/manifoldco/promptui"
+ "github.com/spf13/cobra"
+)
+
+var StopTimerCmd = &cobra.Command{
+ Use: "stop-timer",
+ Short: "Stop timer",
+ Long: `Stop the current timer`,
+ Run: func(cmd *cobra.Command, args []string) {
+ StopTimer()
+ },
+}
+
+func StopTimer() {
+ loc, _ := time.LoadLocation("UTC")
+ cur_time := time.Now().In(loc)
+ end_time_str := cur_time.Format("2006-01-02T15:04:05.000Z")
+
+ entry := api.Stop(end_time_str)
+
+ prompt := promptui.Prompt{
+ Label: "Please fill in a description",
+ }
+
+ description, err := prompt.Run()
+ if err != nil {
+ fmt.Printf("Prompt failed %v\n", err)
+ return
+ }
+ entry.Description = description
+ updateEntry := convertyEntryToUpdateEntry(entry)
+ api.AddDescription(entry.ID, updateEntry)
+}
+
+func convertyEntryToUpdateEntry(entry model.Entry) model.UpdateEntry {
+ return model.UpdateEntry{
+ Start: entry.TimeInterval.Start,
+ Billable: entry.Billable,
+ Description: entry.Description,
+ ProjectID: entry.ProjectID,
+ TaskID: entry.TaskID,
+ End: entry.TimeInterval.End,
+ TagIDs: entry.TagIDs,
+ }
+}
diff --git a/pkg/Model/entry.go b/pkg/Model/entry.go
new file mode 100644
index 0000000..a640898
--- /dev/null
+++ b/pkg/Model/entry.go
@@ -0,0 +1,24 @@
+package model
+
+type Entry struct {
+ ID string `json:"id"`
+ ProjectID string `json:"projectId"`
+ Description string `json:"description"`
+ TaskID string `json:"taskId"`
+ Billable bool `json:"billable"`
+ TimeInterval struct {
+ Start string `json:"start"`
+ End string `json:"end"`
+ }
+ TagIDs []string `json:"tagIds"`
+}
+
+type UpdateEntry struct {
+ Start string `json:"start"`
+ Billable bool `json:"billable"`
+ Description string `json:"description"`
+ ProjectID string `json:"projectId"`
+ TaskID string `json:"taskId"`
+ End string `json:"end"`
+ TagIDs []string `json:"tagIds"`
+}
diff --git a/pkg/Model/main_menu.go b/pkg/Model/main_menu.go
new file mode 100644
index 0000000..d0cc0a9
--- /dev/null
+++ b/pkg/Model/main_menu.go
@@ -0,0 +1,24 @@
+package model
+
+type MainMenuAction struct {
+ Name string
+ Idx int
+}
+
+var MainMenuActionStart = MainMenuAction{Name: "Start", Idx: 0}
+var MainMenuActionStop = MainMenuAction{Name: "Stop", Idx: 1}
+var MainMenuActionShowProjects = MainMenuAction{Name: "Show Projects", Idx: 2}
+var MainMenuActionCheckBalance = MainMenuAction{Name: "Check Balance", Idx: 3}
+var MainMenuActionSetPartTime = MainMenuAction{Name: "Set Part Time", Idx: 4}
+var MainMenuActionChangeAPIKey = MainMenuAction{Name: "Change API key", Idx: 5}
+var MainMenuActionQuit = MainMenuAction{Name: "Quit", Idx: 6}
+
+var MainMenuActions = []MainMenuAction{
+ MainMenuActionStart,
+ MainMenuActionStop,
+ MainMenuActionShowProjects,
+ MainMenuActionCheckBalance,
+ MainMenuActionSetPartTime,
+ MainMenuActionChangeAPIKey,
+ MainMenuActionQuit,
+}
diff --git a/pkg/Model/project.go b/pkg/Model/project.go
new file mode 100644
index 0000000..fc13bf6
--- /dev/null
+++ b/pkg/Model/project.go
@@ -0,0 +1,6 @@
+package model
+
+type Project struct {
+ Name string `json:"name"`
+ ID string `json:"id"`
+}
diff --git a/pkg/Model/report.go b/pkg/Model/report.go
new file mode 100644
index 0000000..62d9be3
--- /dev/null
+++ b/pkg/Model/report.go
@@ -0,0 +1,38 @@
+package model
+
+type Report struct {
+ Start string `json:"dateRangeStart"`
+ End string `json:"dateRangeEnd"`
+ SummaryFilter *Report_filter `json:"summaryFilter,omitempty"`
+ SortOrder string `json:"sortOrder"`
+ Users *Report_user `json:"users,omitempty"`
+}
+type Report_filter struct {
+ Groups []string `json:"groups"`
+}
+type Report_user struct {
+ Ids []string `json:"ids"`
+ Contains string `json:"contains"`
+ Status string `json:"status"`
+}
+
+type Result struct {
+ Entries []ResultUser `json:"groupOne"`
+}
+
+type ResultUser struct {
+ Name string
+ Duration int64
+ Entries []ReportEntry `json:"children"`
+}
+
+type ReportEntry struct {
+ Duration int
+ Date string `json:"name"`
+}
+
+type PartTime struct {
+ Start string
+ End string
+ Capacity int64 // 0...100 (percent)
+}
diff --git a/pkg/Model/user.go b/pkg/Model/user.go
new file mode 100644
index 0000000..1962475
--- /dev/null
+++ b/pkg/Model/user.go
@@ -0,0 +1,8 @@
+package model
+
+type User struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ ActiveWorkspace string `json:"activeWorkspace"`
+}
diff --git a/pkg/Utils/utils.go b/pkg/Utils/utils.go
new file mode 100644
index 0000000..1d9168a
--- /dev/null
+++ b/pkg/Utils/utils.go
@@ -0,0 +1,65 @@
+package utils
+
+import (
+ "log"
+
+ "github.com/spf13/viper"
+)
+
+func ExtractAPIKey() string {
+ api_key := viper.GetString("api-key")
+
+ if api_key == "" {
+ log.Fatal("No API key found. Please set the API key with 'clockify-cli config set api-key '")
+ }
+
+ return api_key
+}
+
+func ExtractWorksapce() string {
+ workspace := viper.GetString("WORKSPACE")
+
+ if workspace == "" {
+ log.Fatal("No workspace found. Please set the workspace with 'clockify-cli config set workspace '")
+ }
+
+ return workspace
+}
+
+func ExtractAPIKeyAndWorkspace() (string, string) {
+ api_key := viper.GetString("api-key")
+ workspace := viper.GetString("WORKSPACE")
+
+ if api_key == "" {
+ log.Fatal("No API key found. Please set the API key with 'clockify-cli config set api-key '")
+ } else if workspace == "" {
+ log.Fatal("No workspace found. Please set the workspace with 'clockify-cli config set workspace '")
+ }
+
+ return api_key, workspace
+}
+
+func ExtractAPIKeyAndWorkspaceAndUserId() (
+ api_key string,
+ workspace string,
+ user_id string,
+) {
+ api_key, workspace = ExtractAPIKeyAndWorkspace()
+ user_id = viper.GetString("USER-ID")
+
+ if user_id == "" {
+ log.Fatal("No user id found. Please set the user id with 'clockify-cli config set user-id '")
+ }
+
+ return api_key, workspace, user_id
+}
+
+func IsUserAuthenticated() bool {
+ user_id := viper.GetString("USER-ID")
+
+ if user_id == "" {
+ return false
+ }
+
+ return true
+}
diff --git a/pkg/root.go b/pkg/root.go
new file mode 100644
index 0000000..c90151a
--- /dev/null
+++ b/pkg/root.go
@@ -0,0 +1,97 @@
+package pkg
+
+import (
+ "fmt"
+ "os"
+
+ controller "github.com/Faagerholm/clockify-cli/pkg/Controller"
+ homedir "github.com/mitchellh/go-homedir"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var (
+ home = os.Getenv("HOME")
+
+ cfgPath string
+ // Used for flags.
+ cfgFile string
+ defFlag bool
+ rootCmd = &cobra.Command{
+ Use: "clockify-cli",
+ Short: "A Clockify-cli",
+ Run: func(cmd *cobra.Command, args []string) {
+ // Start menu if no subcommand is given.
+ controller.Menu()
+ },
+ }
+)
+
+var resetViperCmd = &cobra.Command{
+ Use: "reset",
+ Short: "Resets viper values",
+ Run: func(cmd *cobra.Command, args []string) {
+ viper.Reset()
+ },
+}
+
+// Execute executes the root command.
+func Execute() error {
+ return rootCmd.Execute()
+}
+
+func init() {
+
+ home, _ := homedir.Dir()
+ cobra.OnInitialize(initConfig)
+
+ rootCmd.PersistentFlags().StringVar(&cfgFile, "config", home+"/.clockify-cli/config.yaml", "config file (default is $HOME/.clockify-cli/config.yaml)")
+ rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
+
+ //root.go
+ rootCmd.AddCommand(controller.AddKeyCmd)
+ rootCmd.AddCommand(resetViperCmd)
+ rootCmd.AddCommand(controller.SetupCmd)
+ // menu.go
+ rootCmd.AddCommand(controller.MenuCmd)
+ // project.go
+ rootCmd.AddCommand(controller.DefaultProjectCmd)
+ rootCmd.AddCommand(controller.ListProjectsCmd)
+ // entry.go
+ rootCmd.AddCommand(controller.StartTimerCmd)
+ rootCmd.AddCommand(controller.StopTimerCmd)
+
+ controller.StartTimerCmd.Flags().BoolVarP(&defFlag, "default", "d", false, "Use default project id.")
+ // user.go
+ rootCmd.AddCommand(controller.GetUserCmd)
+ rootCmd.AddCommand(controller.AddPartTimeCmd)
+ // report.go
+ rootCmd.AddCommand(controller.CheckBalanceCmd)
+}
+
+func er(msg interface{}) {
+ fmt.Println("Error:", msg)
+ os.Exit(1)
+}
+
+func initConfig() {
+ if cfgFile != "" {
+ // Use config file from the flag.
+ viper.SetConfigFile(cfgFile)
+ } else {
+ // Find home directory.
+ home, err := homedir.Dir()
+ if err != nil {
+ er(err)
+ }
+ viper.AddConfigPath(home + "/.clockify-cli")
+ viper.SetConfigName("config")
+ viper.SetConfigType("yaml")
+ }
+
+ viper.AutomaticEnv()
+
+ if err := viper.ReadInConfig(); err == nil {
+ // fmt.Println("Using config file:", viper.ConfigFileUsed())
+ }
+}