|
@@ -4,30 +4,86 @@ import (
|
|
|
"fmt"
|
|
"fmt"
|
|
|
"strings"
|
|
"strings"
|
|
|
|
|
|
|
|
- "github.com/charmbracelet/bubbles/viewport"
|
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// The main bubbletea Model
|
|
// The main bubbletea Model
|
|
|
type Model struct {
|
|
type Model struct {
|
|
|
- Issue Issue
|
|
|
|
|
- Path string
|
|
|
|
|
- viewport viewport.Model
|
|
|
|
|
|
|
+ issue Issue
|
|
|
|
|
+ collection IssueCollection
|
|
|
|
|
+ content string
|
|
|
|
|
+ Path string
|
|
|
|
|
+ selection int // index for the collection browser
|
|
|
|
|
+ // viewport viewport.Model
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (m Model) Init() tea.Cmd { return nil }
|
|
|
|
|
|
|
+func (m Model) Init() tea.Cmd { return m.load }
|
|
|
|
|
|
|
|
// Handles quit logic and viewport scroll and size updates
|
|
// Handles quit logic and viewport scroll and size updates
|
|
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
- switch msg.(type) {
|
|
|
|
|
|
|
+ switch msg := msg.(type) {
|
|
|
case tea.KeyMsg:
|
|
case tea.KeyMsg:
|
|
|
- return m, tea.Quit
|
|
|
|
|
|
|
+ switch msg.String() {
|
|
|
|
|
+ case "q":
|
|
|
|
|
+ return m, tea.Quit
|
|
|
|
|
+ case "k":
|
|
|
|
|
+ if m.selection+1 < len(m.collection) {
|
|
|
|
|
+ m.selection = m.selection + 1
|
|
|
|
|
+ } else {
|
|
|
|
|
+ m.selection = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ return m, m.renderIssueCollection
|
|
|
|
|
+ case "j":
|
|
|
|
|
+ if m.selection != 0 {
|
|
|
|
|
+ m.selection = m.selection - 1
|
|
|
|
|
+ } else {
|
|
|
|
|
+ m.selection = len(m.collection) - 1
|
|
|
|
|
+ }
|
|
|
|
|
+ return m, m.renderIssueCollection
|
|
|
|
|
+ case "enter":
|
|
|
|
|
+ m.Path = m.collection[m.selection].Path
|
|
|
|
|
+ return m, m.load
|
|
|
|
|
+ }
|
|
|
|
|
+ case Issue:
|
|
|
|
|
+ m.issue = msg
|
|
|
|
|
+ return m, m.renderIssue
|
|
|
|
|
+ case IssueCollection:
|
|
|
|
|
+ m.collection = msg
|
|
|
|
|
+ return m, m.renderIssueCollection
|
|
|
|
|
+ case string:
|
|
|
|
|
+ m.content = msg
|
|
|
|
|
+ return m, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return m, nil
|
|
return m, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Handles load logic
|
|
|
|
|
+func (m Model) load() tea.Msg {
|
|
|
|
|
+ if IsIssue(m.Path) {
|
|
|
|
|
+ issue, err := m.issue.NewFromPath(m.Path)
|
|
|
|
|
+
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return issue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if IsIssueCollection(m.Path) {
|
|
|
|
|
+ collection, err := m.collection.NewFromPath(m.Path)
|
|
|
|
|
+
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return collection
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// [lipgloss] style definitions
|
|
// [lipgloss] style definitions
|
|
|
var (
|
|
var (
|
|
|
titleStyle = lipgloss.NewStyle().
|
|
titleStyle = lipgloss.NewStyle().
|
|
@@ -50,48 +106,83 @@ var (
|
|
|
Padding(1, 2).
|
|
Padding(1, 2).
|
|
|
Margin(1).
|
|
Margin(1).
|
|
|
BorderStyle(lipgloss.NormalBorder())
|
|
BorderStyle(lipgloss.NormalBorder())
|
|
|
|
|
+
|
|
|
|
|
+ indexStyle = lipgloss.NewStyle().
|
|
|
|
|
+ Italic(true)
|
|
|
|
|
+
|
|
|
|
|
+ pointerStyle = lipgloss.NewStyle().
|
|
|
|
|
+ Faint(true)
|
|
|
|
|
+
|
|
|
|
|
+ collectionStyleLeft = lipgloss.NewStyle().
|
|
|
|
|
+ Align(lipgloss.Left)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// Handles all view logic for [issue.Issue]
|
|
// Handles all view logic for [issue.Issue]
|
|
|
-func (m Model) renderIssue() string {
|
|
|
|
|
|
|
+func (m Model) renderIssue() tea.Msg {
|
|
|
var output string
|
|
var output string
|
|
|
// title
|
|
// title
|
|
|
- output = output + titleStyle.Render(m.Issue.Title)
|
|
|
|
|
|
|
+ output = output + titleStyle.Render(m.issue.Title)
|
|
|
|
|
|
|
|
// status
|
|
// status
|
|
|
- output = output + fmt.Sprintf("\n%s", statusStyle.Render(m.Issue.Status.Data))
|
|
|
|
|
|
|
+ output = output + fmt.Sprintf("\n%s", statusStyle.Render(m.issue.Status.Data))
|
|
|
|
|
|
|
|
// variadics
|
|
// variadics
|
|
|
var tags string
|
|
var tags string
|
|
|
- for _, field := range m.Issue.Tags.Fields {
|
|
|
|
|
|
|
+ for _, field := range m.issue.Tags.Fields {
|
|
|
tags = tags + field.Path + ", "
|
|
tags = tags + field.Path + ", "
|
|
|
}
|
|
}
|
|
|
tags = strings.TrimRight(tags, ", ")
|
|
tags = strings.TrimRight(tags, ", ")
|
|
|
|
|
|
|
|
var blockedby string
|
|
var blockedby string
|
|
|
- for _, field := range m.Issue.Blockedby.Fields {
|
|
|
|
|
|
|
+ for _, field := range m.issue.Blockedby.Fields {
|
|
|
blockedby = blockedby + field.Path + ", "
|
|
blockedby = blockedby + field.Path + ", "
|
|
|
}
|
|
}
|
|
|
blockedby = strings.TrimRight(blockedby, ", ")
|
|
blockedby = strings.TrimRight(blockedby, ", ")
|
|
|
|
|
|
|
|
- if len(m.Issue.Tags.Fields) > 0 {
|
|
|
|
|
|
|
+ if len(m.issue.Tags.Fields) > 0 {
|
|
|
output = output + variadicTitleStyle.Render("\n\nTags:")
|
|
output = output + variadicTitleStyle.Render("\n\nTags:")
|
|
|
output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(tags))
|
|
output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(tags))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if len(m.Issue.Blockedby.Fields) > 0 {
|
|
|
|
|
|
|
+ if len(m.issue.Blockedby.Fields) > 0 {
|
|
|
output = output + variadicTitleStyle.Render("\n\nBlockedby:")
|
|
output = output + variadicTitleStyle.Render("\n\nBlockedby:")
|
|
|
output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(blockedby))
|
|
output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(blockedby))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// description
|
|
// description
|
|
|
output = output + titleStyle.Render("\n\nDescription:\n")
|
|
output = output + titleStyle.Render("\n\nDescription:\n")
|
|
|
- output = output + fmt.Sprintf("\n%s", m.Issue.Description.Data)
|
|
|
|
|
|
|
+ output = output + fmt.Sprintf("\n%s", m.issue.Description.Data)
|
|
|
|
|
|
|
|
return borderStyle.Render(output)
|
|
return borderStyle.Render(output)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (m Model) renderIssueCollection() tea.Msg {
|
|
|
|
|
+ var output string
|
|
|
|
|
+ var left string
|
|
|
|
|
+ output = output + "Issues in " + m.Path + "...\n\n"
|
|
|
|
|
+ for i, issue := range m.collection {
|
|
|
|
|
+ // pointer render
|
|
|
|
|
+ if i == m.selection {
|
|
|
|
|
+ left = left + pointerStyle.Render("-> ")
|
|
|
|
|
+ } else {
|
|
|
|
|
+ left = left + pointerStyle.Render(" ")
|
|
|
|
|
+ }
|
|
|
|
|
+ // index render
|
|
|
|
|
+ left = left + "[" + indexStyle.Render(fmt.Sprintf("%d", i+1)) + "]: "
|
|
|
|
|
+
|
|
|
|
|
+ // title render
|
|
|
|
|
+ left = left + fmt.Sprintf("%s\n", titleStyle.Render(issue.Title))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ output = output + collectionStyleLeft.Render(left)
|
|
|
|
|
+ output = output + "\nj/k: down/up\tenter: select\tq: quit"
|
|
|
|
|
+ return output
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Wraps [issue.Model.renderIssue] in a viewport
|
|
// Wraps [issue.Model.renderIssue] in a viewport
|
|
|
func (m Model) View() string {
|
|
func (m Model) View() string {
|
|
|
- return m.renderIssue()
|
|
|
|
|
|
|
+ if len(m.content) == 0 {
|
|
|
|
|
+ return "loading..."
|
|
|
|
|
+ }
|
|
|
|
|
+ return m.content
|
|
|
}
|
|
}
|