// The package defines an extensible TUI via the bubbletea framework. // // While the package remains in v0.0.X releases, this TUI may be undocumented. package issues import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // The main bubbletea Model type Model struct { 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 m.load } // Handles quit logic and viewport scroll and size updates func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: 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 } // 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 var ( titleStyle = lipgloss.NewStyle(). Bold(true). Underline(true) statusStyle = lipgloss.NewStyle(). Faint(true). Italic(true) variadicTitleStyle = lipgloss.NewStyle(). Align(lipgloss.Left). Italic(true) variadicDataStyle = lipgloss.NewStyle(). Width(40). BorderStyle(lipgloss.ASCIIBorder()) borderStyle = lipgloss.NewStyle(). Padding(1, 2). Margin(1). 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] func (m Model) renderIssue() tea.Msg { var output string // title output = output + titleStyle.Render(m.issue.Title) // status output = output + fmt.Sprintf("\n%s", statusStyle.Render(m.issue.Status.Data)) // variadics var tags string for _, field := range m.issue.Tags.Fields { tags = tags + field.Path + ", " } tags = strings.TrimRight(tags, ", ") var blockedby string for _, field := range m.issue.Blockedby.Fields { blockedby = blockedby + field.Path + ", " } blockedby = strings.TrimRight(blockedby, ", ") if len(m.issue.Tags.Fields) > 0 { output = output + variadicTitleStyle.Render("\n\nTags:") output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(tags)) } if len(m.issue.Blockedby.Fields) > 0 { output = output + variadicTitleStyle.Render("\n\nBlockedby:") output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(blockedby)) } // description output = output + titleStyle.Render("\n\nDescription:\n") output = output + fmt.Sprintf("\n%s", m.issue.Description.Data) 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 func (m Model) View() string { if len(m.content) == 0 { return "loading..." } return m.content }