Skip to content

Commit

Permalink
feat: add assignee, time spent details
Browse files Browse the repository at this point in the history
  • Loading branch information
dhth committed Apr 3, 2024
1 parent ae8da5c commit 5df7247
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 11 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
`punchout` takes the suck out of logging time on JIRA.

<p align="center">
<img src="./punchout.gif?raw=true" alt="Usage" />
<img src="https://tools.dhruvs.space/images/punchout/punchout.gif" alt="Usage" />
</p>

Install
Expand Down Expand Up @@ -49,6 +49,9 @@ jql = "assignee = currentUser() AND updatedDate >= -14d ORDER BY updatedDate DES
jira_time_delta_mins = 300
```

*Note: `punchout` only supports [on-premise] installations of JIRA for now. I
might add support for cloud installations in the future.*

### Using command line flags

Use `punchout -h` for help.
Expand All @@ -72,6 +75,16 @@ punchout \
jql='assignee = currentUser() AND updatedDate >= -14d ORDER BY updatedDate DESC'
```

Screenshots
---

<p align="center">
<img src="https://tools.dhruvs.space/images/punchout/punchout-1.png" alt="Usage" />
</p>
<p align="center">
<img src="https://tools.dhruvs.space/images/punchout/punchout-2.png" alt="Usage" />
</p>

Reference Manual
---

Expand Down Expand Up @@ -115,3 +128,4 @@ Acknowledgements
`punchout` is built using the awesome TUI framework [bubbletea][1].

[1]: https://github.com/charmbracelet/bubbletea
[2]: https://community.atlassian.com/t5/Atlassian-Migration-Program/Product-features-comparison-Atlassian-Cloud-vs-on-premise/ba-p/1918147
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ require (
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.10.0
github.com/dustin/go-humanize v1.0.1
modernc.org/sqlite v1.29.3
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
Expand Down
36 changes: 35 additions & 1 deletion ui/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package ui

import (
"database/sql"
"os/exec"
"runtime"
"time"

jira "github.com/andygrunwald/go-jira/v2/onpremise"
Expand Down Expand Up @@ -129,7 +131,22 @@ func fetchJIRAIssues(cl *jira.Client, jql string) tea.Cmd {
jIssues, err := getIssues(cl, jql)
var issues []Issue
for _, issue := range jIssues {
issues = append(issues, Issue{issue.Key, issue.Fields.Type.Name, issue.Fields.Summary})
var assignee string
var totalSecsSpent int
var status string
if issue.Fields != nil {
if issue.Fields.Assignee != nil {
assignee = issue.Fields.Assignee.Name
}

totalSecsSpent = issue.Fields.AggregateTimeSpent

if issue.Fields.Status != nil {
status = issue.Fields.Status.Name

}
}
issues = append(issues, Issue{issue.Key, issue.Fields.Type.Name, issue.Fields.Summary, assignee, status, totalSecsSpent})
}
return IssuesFetchedFromJIRAMsg{issues, err}
}
Expand All @@ -147,3 +164,20 @@ func hideHelp(interval time.Duration) tea.Cmd {
return HideHelpMsg{}
})
}

func openURLInBrowser(url string) tea.Cmd {
var openCmd string
switch runtime.GOOS {
case "darwin":
openCmd = "open"
default:
openCmd = "xdg-open"
}
c := exec.Command(openCmd, url)
return tea.ExecProcess(c, func(err error) tea.Msg {
if err != nil {
return URLOpenedinBrowserMsg{url: url, err: err}
}
return tea.Msg(URLOpenedinBrowserMsg{url: url})
})
}
5 changes: 5 additions & 0 deletions ui/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ type WLAddedOnJIRA struct {
entry WorklogEntry
err error
}

type URLOpenedinBrowserMsg struct {
url string
err error
}
39 changes: 37 additions & 2 deletions ui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import (
"hash/fnv"
)

const (
ActiveIssueColor = "#d3869b"
IssueStatusColor = "#665c54"
AggTimeSpentColor = "#928374"
)

var (
baseStyle = lipgloss.NewStyle().
PaddingLeft(1).
Expand Down Expand Up @@ -47,7 +53,7 @@ var (

activeIssueMsgStyle = baseStyle.Copy().
Bold(true).
Foreground(lipgloss.Color("#d3869b"))
Foreground(lipgloss.Color(ActiveIssueColor))

helpTitleStyle = baseStyle.Copy().
Bold(true).
Expand All @@ -64,11 +70,40 @@ var (
color := issueTypeColors[int(hash)%len(issueTypeColors)]
return lipgloss.NewStyle().
PaddingLeft(1).
PaddingRight(1).
Foreground(lipgloss.Color("#282828")).Copy().
Bold(true).
Align(lipgloss.Center).
Width(18).
Background(lipgloss.Color(color))
}

assigneeColors = []string{
"#ccccff", // Lavender Blue
"#ffa87d", // Light orange
"#7385D8", // Light blue
"#fabd2f", // Bright Yellow
"#00abe5", // Deep Sky
"#d3691e", // Chocolate
}
assigneeStyle = func(assignee string) lipgloss.Style {
h := fnv.New32()
h.Write([]byte(assignee))
hash := h.Sum32()

color := assigneeColors[int(hash)%len(assigneeColors)]

st := lipgloss.NewStyle().
PaddingLeft(1).
Foreground(lipgloss.Color(color))

return st
}

issueStatusStyle = lipgloss.NewStyle().
PaddingLeft(1).
Foreground(lipgloss.Color(IssueStatusColor))

aggTimeSpentStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(lipgloss.Color(AggTimeSpentColor))
)
35 changes: 30 additions & 5 deletions ui/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,44 @@ import (
)

type Issue struct {
IssueKey string
IssueType string
Summary string
IssueKey string
IssueType string
Summary string
Assignee string
Status string
AggSecondsSpent int
}

func (issue Issue) Title() string {
return RightPadTrim(issue.Summary, int(float64(listWidth)*0.8))
}
func (issue Issue) Description() string {
// TODO: The padding here is a bit of a mess; make it more readable
var assignee string
var status string
var totalSecsSpent string

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.78)), issueType)

if issue.Assignee != "" {
assignee = assigneeStyle(issue.Assignee).Render(RightPadTrim("@"+issue.Assignee, int(float64(listWidth)*0.2)))
} else {
assignee = assigneeStyle(issue.Assignee).Render(RightPadTrim("", int(float64(listWidth)*0.2)))
}

status = issueStatusStyle.Render(RightPadTrim(issue.Status, int(float64(listWidth)*0.2)))

if issue.AggSecondsSpent > 0 {
if issue.AggSecondsSpent < 3600 {
totalSecsSpent = aggTimeSpentStyle.Render(fmt.Sprintf("%2dm", int(issue.AggSecondsSpent/60)))
} else {
totalSecsSpent = aggTimeSpentStyle.Render(fmt.Sprintf("%2dh", int(issue.AggSecondsSpent/3600)))
}
}

return fmt.Sprintf("%s%s%s%s%s", RightPadTrim(issue.IssueKey, int(float64(listWidth)*0.3)), status, assignee, issueType, totalSecsSpent)
}
func (issue Issue) FilterValue() string { return issue.IssueKey + " : " + issue.Summary }
func (issue Issue) FilterValue() string { return issue.IssueKey }

type WorklogEntry struct {
Id int
Expand Down
15 changes: 14 additions & 1 deletion ui/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ui

import (
"fmt"
"log"
"time"

Expand Down Expand Up @@ -159,7 +160,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, fetchLogEntries(m.db))
}
case "ctrl+r":
if m.activeView == WorklogView {
switch m.activeView {
case IssueListView:
cmds = append(cmds, fetchJIRAIssues(m.jiraClient, m.jql))
case WorklogView:
cmds = append(cmds, fetchLogEntries(m.db))
}
case "ctrl+s":
Expand Down Expand Up @@ -231,6 +235,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "?":
m.lastView = m.activeView
m.activeView = HelpView
case "ctrl+b":
if m.activeView == IssueListView {
selectedIssue := m.issueList.SelectedItem().FilterValue()
cmds = append(cmds, openURLInBrowser(fmt.Sprintf("%sbrowse/%s", m.jiraClient.BaseURL.String(), selectedIssue)))
}
}

case tea.WindowSizeMsg:
Expand Down Expand Up @@ -356,6 +365,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case HideHelpMsg:
m.showHelpIndicator = false
case URLOpenedinBrowserMsg:
if msg.err != nil {
m.message = fmt.Sprintf("Error opening url: %s", msg.err.Error())
}
}

switch m.activeView {
Expand Down

0 comments on commit 5df7247

Please sign in to comment.