tui.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. // The package defines an extensible TUI via the bubbletea framework.
  2. //
  3. // While the package remains in v0.0.X releases, this TUI may be undocumented.
  4. package issues
  5. import (
  6. "fmt"
  7. "strings"
  8. tea "github.com/charmbracelet/bubbletea"
  9. "github.com/charmbracelet/lipgloss"
  10. )
  11. // The main bubbletea Model
  12. type Model struct {
  13. issue Issue
  14. collection IssueCollection
  15. content string
  16. Path string
  17. selection int // index for the collection browser
  18. // viewport viewport.Model
  19. }
  20. func (m Model) Init() tea.Cmd { return m.load }
  21. // Handles quit logic and viewport scroll and size updates
  22. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  23. switch msg := msg.(type) {
  24. case tea.KeyMsg:
  25. switch msg.String() {
  26. case "q":
  27. return m, tea.Quit
  28. case "k":
  29. if m.selection+1 < len(m.collection) {
  30. m.selection = m.selection + 1
  31. } else {
  32. m.selection = 0
  33. }
  34. return m, m.renderIssueCollection
  35. case "j":
  36. if m.selection != 0 {
  37. m.selection = m.selection - 1
  38. } else {
  39. m.selection = len(m.collection) - 1
  40. }
  41. return m, m.renderIssueCollection
  42. case "enter":
  43. m.Path = m.collection[m.selection].Path
  44. return m, m.load
  45. }
  46. case Issue:
  47. m.issue = msg
  48. return m, m.renderIssue
  49. case IssueCollection:
  50. m.collection = msg
  51. return m, m.renderIssueCollection
  52. case string:
  53. m.content = msg
  54. return m, nil
  55. }
  56. return m, nil
  57. }
  58. // Handles load logic
  59. func (m Model) load() tea.Msg {
  60. if IsIssue(m.Path) {
  61. issue, err := m.issue.NewFromPath(m.Path)
  62. if err != nil {
  63. return nil
  64. }
  65. return issue
  66. }
  67. if IsIssueCollection(m.Path) {
  68. collection, err := m.collection.NewFromPath(m.Path)
  69. if err != nil {
  70. return nil
  71. }
  72. return collection
  73. }
  74. return nil
  75. }
  76. // [lipgloss] style definitions
  77. var (
  78. titleStyle = lipgloss.NewStyle().
  79. Bold(true).
  80. Underline(true)
  81. statusStyle = lipgloss.NewStyle().
  82. Faint(true).
  83. Italic(true)
  84. variadicTitleStyle = lipgloss.NewStyle().
  85. Align(lipgloss.Left).
  86. Italic(true)
  87. variadicDataStyle = lipgloss.NewStyle().
  88. Width(40).
  89. BorderStyle(lipgloss.ASCIIBorder())
  90. borderStyle = lipgloss.NewStyle().
  91. Padding(1, 2).
  92. Margin(1).
  93. BorderStyle(lipgloss.NormalBorder())
  94. indexStyle = lipgloss.NewStyle().
  95. Italic(true)
  96. pointerStyle = lipgloss.NewStyle().
  97. Faint(true)
  98. collectionStyleLeft = lipgloss.NewStyle().
  99. Align(lipgloss.Left)
  100. )
  101. // Handles all view logic for [issue.Issue]
  102. func (m Model) renderIssue() tea.Msg {
  103. var output string
  104. // title
  105. output = output + titleStyle.Render(m.issue.Title)
  106. // status
  107. output = output + fmt.Sprintf("\n%s", statusStyle.Render(m.issue.Status.Data))
  108. // variadics
  109. var tags string
  110. for _, field := range m.issue.Tags.Fields {
  111. tags = tags + field.Path + ", "
  112. }
  113. tags = strings.TrimRight(tags, ", ")
  114. var blockedby string
  115. for _, field := range m.issue.Blockedby.Fields {
  116. blockedby = blockedby + field.Path + ", "
  117. }
  118. blockedby = strings.TrimRight(blockedby, ", ")
  119. if len(m.issue.Tags.Fields) > 0 {
  120. output = output + variadicTitleStyle.Render("\n\nTags:")
  121. output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(tags))
  122. }
  123. if len(m.issue.Blockedby.Fields) > 0 {
  124. output = output + variadicTitleStyle.Render("\n\nBlockedby:")
  125. output = output + fmt.Sprintf("\n%s", variadicDataStyle.Render(blockedby))
  126. }
  127. // description
  128. output = output + titleStyle.Render("\n\nDescription:\n")
  129. output = output + fmt.Sprintf("\n%s", m.issue.Description.Data)
  130. return borderStyle.Render(output)
  131. }
  132. func (m Model) renderIssueCollection() tea.Msg {
  133. var output string
  134. var left string
  135. output = output + "Issues in " + m.Path + "...\n\n"
  136. for i, issue := range m.collection {
  137. // pointer render
  138. if i == m.selection {
  139. left = left + pointerStyle.Render("-> ")
  140. } else {
  141. left = left + pointerStyle.Render(" ")
  142. }
  143. // index render
  144. left = left + "[" + indexStyle.Render(fmt.Sprintf("%d", i+1)) + "]: "
  145. // title render
  146. left = left + fmt.Sprintf("%s\n", titleStyle.Render(issue.Title))
  147. }
  148. output = output + collectionStyleLeft.Render(left)
  149. output = output + "\nj/k: down/up\tenter: select\tq: quit"
  150. return output
  151. }
  152. // Wraps [issue.Model.renderIssue] in a viewport
  153. func (m Model) View() string {
  154. if len(m.content) == 0 {
  155. return "loading..."
  156. }
  157. return m.content
  158. }