From df0f47541f4d501bc48f234dec92e630c4aec051 Mon Sep 17 00:00:00 2001 From: Dhruv Thakur Date: Tue, 12 Mar 2024 17:32:51 +0100 Subject: [PATCH] feat: add support for reading config from a file --- .gitignore | 1 + cmd/config.go | 32 ++++++++++++++++++ cmd/root.go | 94 +++++++++++++++++++++++++++++++++++++++++---------- cmd/utils.go | 18 ++++++++++ go.mod | 1 + go.sum | 2 ++ ui/help.go | 4 +-- ui/types.go | 12 +++---- ui/update.go | 4 +-- 9 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 cmd/config.go create mode 100644 cmd/utils.go diff --git a/.gitignore b/.gitignore index 23cb177..8eba1db 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ punchout debug.log punchout.v*.db .quickrun +justfile diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..5585151 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/BurntSushi/toml" +) + +type JiraConfig struct { + JiraURL *string `toml:"jira_url"` + JiraToken *string `toml:"jira_token"` + Jql *string + JiraTimeDeltaMins *int `toml:"jira_time_delta_mins"` +} + +type POConfig struct { + DbPath *string `toml:"db_path"` + Jira JiraConfig +} + +func readConfig(filePath string) (POConfig, error) { + + var config POConfig + _, err := toml.DecodeFile(expandTilde(filePath), &config) + if err != nil { + return config, err + } + if config.DbPath != nil { + *config.DbPath = expandTilde(*config.DbPath) + } + + return config, nil + +} diff --git a/cmd/root.go b/cmd/root.go index b4711bd..91efa88 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/user" + "strconv" jira "github.com/andygrunwald/go-jira/v2/onpremise" "github.com/dhth/punchout/ui" @@ -16,14 +17,22 @@ func die(msg string, args ...any) { } var ( - jiraURL = flag.String("jira-url", "https://jira.company.com", "URL of the JIRA server") - jiraToken = flag.String("jira-token", "", "personal access token for the JIRA server") - jql = flag.String("jql", "assignee = currentUser() AND updatedDate >= -14d ORDER BY updatedDate DESC", "JQL to use to query issues at startup") - jiraTimeDeltaMins = flag.Int("jira-time-delta-mins", 0, "Time delta (in minutes) between your timezone and the timezone of the server; can be +/-") + jiraURL = flag.String("jira-url", "", "URL of the JIRA server") + jiraToken = flag.String("jira-token", "", "personal access token for the JIRA server") + jql = flag.String("jql", "", "JQL to use to query issues at startup") + jiraTimeDeltaMinsStr = flag.String("jira-time-delta-mins", "", "Time delta (in minutes) between your timezone and the timezone of the server; can be +/-") + listConfig = flag.Bool("list-config", false, "Whether to only print out the config that punchout will use or not") ) func Execute() { currentUser, err := user.Current() + + var defaultConfigFP string + if err == nil { + defaultConfigFP = fmt.Sprintf("%s/.config/punchout/punchout.toml", currentUser.HomeDir) + } + configFilePath := flag.String("config-file-path", defaultConfigFP, "location of the punchout config file") + var defaultDBPath string if err == nil { defaultDBPath = fmt.Sprintf("%s/punchout.v%s.db", currentUser.HomeDir, PUNCHOUT_DB_VERSION) @@ -31,41 +40,92 @@ func Execute() { dbPath := flag.String("db-path", defaultDBPath, "location where punchout should create its DB file") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Take the suck out of logging time on JIRA.\n\nFlags:\n") + fmt.Fprintf(os.Stdout, "Take the suck out of logging time on JIRA.\n\nFlags:\n") + flag.CommandLine.SetOutput(os.Stdout) flag.PrintDefaults() - fmt.Fprintf(os.Stderr, "\n------\n%s", ui.HelpText) + fmt.Fprintf(os.Stdout, "\n------\n%s", ui.HelpText) } flag.Parse() - if *dbPath == "" { - die("db-path cannot be empty") + if *configFilePath == "" { + die("config-file-path cannot be empty") } - if *jql == "" { - die("jql cannot be empty") + var jiraTimeDeltaMins int + if *jiraTimeDeltaMinsStr != "" { + jiraTimeDeltaMins, err = strconv.Atoi(*jiraTimeDeltaMinsStr) + if err != nil { + die("could't convert jira-time-delta-mins to a number") + } + } + + poCfg, err := readConfig(*configFilePath) + if err != nil { + die("error reading config at %s: %s", *configFilePath, err.Error()) + } + if *dbPath != "" { + expandedPath := expandTilde(*dbPath) + poCfg.DbPath = &expandedPath + } + + if *jiraURL != "" { + poCfg.Jira.JiraURL = jiraURL + } + + if *jiraToken != "" { + poCfg.Jira.JiraToken = jiraToken + } + + if *jql != "" { + poCfg.Jira.Jql = jql + } + if *jiraTimeDeltaMinsStr != "" { + poCfg.Jira.JiraTimeDeltaMins = &jiraTimeDeltaMins } - if *jiraURL == "" { + configKeyMaxLen := 40 + if *listConfig { + fmt.Fprint(os.Stdout, "Config:\n\n") + fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("Config File Path", configKeyMaxLen), *configFilePath) + fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("DB File Path", configKeyMaxLen), *poCfg.DbPath) + fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA URL", configKeyMaxLen), *poCfg.Jira.JiraURL) + fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JIRA Token", configKeyMaxLen), *poCfg.Jira.JiraToken) + fmt.Fprintf(os.Stdout, "%s%s\n", ui.RightPadTrim("JQL", configKeyMaxLen), *poCfg.Jira.Jql) + fmt.Fprintf(os.Stdout, "%s%d\n", ui.RightPadTrim("JIRA Time Delta Mins", configKeyMaxLen), *poCfg.Jira.JiraTimeDeltaMins) + os.Exit(0) + } + + // validations + if *poCfg.DbPath == "" { + die("db-path cannot be empty") + } + + if *poCfg.Jira.JiraURL == "" { die("jira-url cannot be empty") } - if *jiraToken == "" { + if *poCfg.Jira.JiraToken == "" { die("jira-token cannot be empty") } - db, err := setupDB(*dbPath) + if *poCfg.Jira.Jql == "" { + die("jql cannot be empty") + } + + db, err := setupDB(*poCfg.DbPath) if err != nil { - fmt.Fprintf(os.Stderr, "Couldn't set up punchout database. This is a fatal error") + fmt.Fprintf(os.Stderr, "Couldn't set up punchout database. This is a fatal error\n") os.Exit(1) } tp := jira.BearerAuthTransport{ - Token: *jiraToken, + Token: *poCfg.Jira.JiraToken, } - cl, err := jira.NewClient(*jiraURL, tp.Client()) + cl, err := jira.NewClient(*poCfg.Jira.JiraURL, tp.Client()) if err != nil { panic(err) } - ui.RenderUI(db, cl, *jql, *jiraTimeDeltaMins) + + ui.RenderUI(db, cl, *poCfg.Jira.Jql, *poCfg.Jira.JiraTimeDeltaMins) } diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..ea1318e --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "os" + "os/user" + "strings" +) + +func expandTilde(path string) string { + if strings.HasPrefix(path, "~") { + usr, err := user.Current() + if err != nil { + os.Exit(1) + } + return strings.Replace(path, "~", usr.HomeDir, 1) + } + return path +} diff --git a/go.mod b/go.mod index 3ab913b..57e1bb8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/dhth/punchout go 1.22.0 require ( + github.com/BurntSushi/toml v1.3.2 github.com/andygrunwald/go-jira/v2 v2.0.0-20240116150243-50d59fe116d6 github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 diff --git a/go.sum b/go.sum index cb7727e..1b24548 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/andygrunwald/go-jira/v2 v2.0.0-20240116150243-50d59fe116d6 h1:pb8RtP8VEWP/BX1M7Kk/AAIGtqThmahV4L/+VcIVcEc= github.com/andygrunwald/go-jira/v2 v2.0.0-20240116150243-50d59fe116d6/go.mod h1:TrfsnL20VgD+KgEw4gbTYuSAPE8T1ZxjMCFBGgGvNvI= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= diff --git a/ui/help.go b/ui/help.go index 236b8b1..efce596 100644 --- a/ui/help.go +++ b/ui/help.go @@ -22,8 +22,8 @@ General List Controls / Start filtering Worklog List View - d Delete worklog entry + Delete worklog entry s Sync all visible entries to JIRA - Refresh list + Refresh list ` ) diff --git a/ui/types.go b/ui/types.go index 7382ede..7ee9a82 100644 --- a/ui/types.go +++ b/ui/types.go @@ -14,11 +14,11 @@ type Issue struct { } func (issue Issue) Title() string { - return fmt.Sprintf("%s", RightPadTrim(issue.Summary, listWidth-20)) + return fmt.Sprintf("%s", RightPadTrim(issue.Summary, int(float64(listWidth)*0.6))) } func (issue Issue) Description() string { - issueType := getIssueTypeStyle(issue.IssueType).Render(Trim(issue.IssueType, 20)) - return fmt.Sprintf("%s%s", RightPadTrim(issue.IssueKey, listWidth-40), issueType) + issueType := getIssueTypeStyle(issue.IssueType).Render(Trim(issue.IssueType, int(float64(listWidth)*0.2))) + return fmt.Sprintf("%s%s", RightPadTrim(issue.IssueKey, int(float64(listWidth)*0.6)), issueType) } func (issue Issue) FilterValue() string { return issue.IssueKey + " : " + issue.Summary } @@ -53,9 +53,9 @@ func (entry WorklogEntry) Description() string { minsSpent := int(entry.EndTS.Sub(entry.BeginTS).Minutes()) minsSpentStr := fmt.Sprintf("spent %d mins", minsSpent) return fmt.Sprintf("%s%s%s%s", - RightPadTrim(entry.IssueKey, 40), - RightPadTrim("started: "+entry.BeginTS.Format("Mon, 3:04pm"), 40), - RightPadTrim(minsSpentStr, 40), + RightPadTrim(entry.IssueKey, int(listWidth/4)), + RightPadTrim("started: "+entry.BeginTS.Format("Mon, 3:04pm"), int(listWidth/4)), + RightPadTrim(minsSpentStr, int(listWidth/4)), syncedStatus, ) } diff --git a/ui/update.go b/ui/update.go index f29d349..b3977fa 100644 --- a/ui/update.go +++ b/ui/update.go @@ -79,7 +79,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, fetchLogEntries(m.db)) return m, tea.Batch(cmds...) } - case "d": + case "ctrl+d": switch m.activeView { case WorklogView: issue, ok := m.worklogList.SelectedItem().(WorklogEntry) @@ -216,7 +216,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case WLAddedOnJIRA: if msg.err != nil { msg.entry.Error = msg.err - m.worklogList.SetItem(msg.index, msg.entry) + m.worklogList.SetItem(msg.index, msg.entry) m.messages = append(m.messages, msg.err.Error()) } else { msg.entry.Synced = true