tui.go 4.0 KB

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