core.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package core
  2. import (
  3. "image/color"
  4. "strings"
  5. "github.com/alecthomas/chroma/v2"
  6. "github.com/charmbracelet/bubbles/v2/help"
  7. "github.com/charmbracelet/bubbles/v2/key"
  8. "github.com/charmbracelet/crush/internal/tui/exp/diffview"
  9. "github.com/charmbracelet/crush/internal/tui/styles"
  10. "github.com/charmbracelet/lipgloss/v2"
  11. "github.com/charmbracelet/x/ansi"
  12. )
  13. type KeyMapHelp interface {
  14. Help() help.KeyMap
  15. }
  16. type simpleHelp struct {
  17. shortList []key.Binding
  18. fullList [][]key.Binding
  19. }
  20. func NewSimpleHelp(shortList []key.Binding, fullList [][]key.Binding) help.KeyMap {
  21. return &simpleHelp{
  22. shortList: shortList,
  23. fullList: fullList,
  24. }
  25. }
  26. // FullHelp implements help.KeyMap.
  27. func (s *simpleHelp) FullHelp() [][]key.Binding {
  28. return s.fullList
  29. }
  30. // ShortHelp implements help.KeyMap.
  31. func (s *simpleHelp) ShortHelp() []key.Binding {
  32. return s.shortList
  33. }
  34. func Section(text string, width int) string {
  35. t := styles.CurrentTheme()
  36. char := "─"
  37. length := lipgloss.Width(text) + 1
  38. remainingWidth := width - length
  39. lineStyle := t.S().Base.Foreground(t.Border)
  40. if remainingWidth > 0 {
  41. text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth))
  42. }
  43. return text
  44. }
  45. func SectionWithInfo(text string, width int, info string) string {
  46. t := styles.CurrentTheme()
  47. char := "─"
  48. length := lipgloss.Width(text) + 1
  49. remainingWidth := width - length
  50. if info != "" {
  51. remainingWidth -= lipgloss.Width(info) + 1 // 1 for the space before info
  52. }
  53. lineStyle := t.S().Base.Foreground(t.Border)
  54. if remainingWidth > 0 {
  55. text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth)) + " " + info
  56. }
  57. return text
  58. }
  59. func Title(title string, width int) string {
  60. t := styles.CurrentTheme()
  61. char := "╱"
  62. length := lipgloss.Width(title) + 1
  63. remainingWidth := width - length
  64. titleStyle := t.S().Base.Foreground(t.Primary)
  65. if remainingWidth > 0 {
  66. lines := strings.Repeat(char, remainingWidth)
  67. lines = styles.ApplyForegroundGrad(lines, t.Primary, t.Secondary)
  68. title = titleStyle.Render(title) + " " + lines
  69. }
  70. return title
  71. }
  72. type StatusOpts struct {
  73. Icon string // if empty no icon will be shown
  74. Title string
  75. TitleColor color.Color
  76. Description string
  77. DescriptionColor color.Color
  78. ExtraContent string // additional content to append after the description
  79. }
  80. func Status(opts StatusOpts, width int) string {
  81. t := styles.CurrentTheme()
  82. icon := opts.Icon
  83. title := opts.Title
  84. titleColor := t.FgMuted
  85. if opts.TitleColor != nil {
  86. titleColor = opts.TitleColor
  87. }
  88. description := opts.Description
  89. descriptionColor := t.FgSubtle
  90. if opts.DescriptionColor != nil {
  91. descriptionColor = opts.DescriptionColor
  92. }
  93. title = t.S().Base.Foreground(titleColor).Render(title)
  94. if description != "" {
  95. extraContentWidth := lipgloss.Width(opts.ExtraContent)
  96. if extraContentWidth > 0 {
  97. extraContentWidth += 1
  98. }
  99. description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
  100. }
  101. description = t.S().Base.Foreground(descriptionColor).Render(description)
  102. content := []string{}
  103. if icon != "" {
  104. content = append(content, icon)
  105. }
  106. content = append(content, title, description)
  107. if opts.ExtraContent != "" {
  108. content = append(content, opts.ExtraContent)
  109. }
  110. return strings.Join(content, " ")
  111. }
  112. type ButtonOpts struct {
  113. Text string
  114. UnderlineIndex int // Index of character to underline (0-based)
  115. Selected bool // Whether this button is selected
  116. }
  117. // SelectableButton creates a button with an underlined character and selection state
  118. func SelectableButton(opts ButtonOpts) string {
  119. t := styles.CurrentTheme()
  120. // Base style for the button
  121. buttonStyle := t.S().Text
  122. // Apply selection styling
  123. if opts.Selected {
  124. buttonStyle = buttonStyle.Foreground(t.White).Background(t.Secondary)
  125. } else {
  126. buttonStyle = buttonStyle.Background(t.BgSubtle)
  127. }
  128. // Create the button text with underlined character
  129. text := opts.Text
  130. if opts.UnderlineIndex >= 0 && opts.UnderlineIndex < len(text) {
  131. before := text[:opts.UnderlineIndex]
  132. underlined := text[opts.UnderlineIndex : opts.UnderlineIndex+1]
  133. after := text[opts.UnderlineIndex+1:]
  134. message := buttonStyle.Render(before) +
  135. buttonStyle.Underline(true).Render(underlined) +
  136. buttonStyle.Render(after)
  137. return buttonStyle.Padding(0, 2).Render(message)
  138. }
  139. // Fallback if no underline index specified
  140. return buttonStyle.Padding(0, 2).Render(text)
  141. }
  142. // SelectableButtons creates a horizontal row of selectable buttons
  143. func SelectableButtons(buttons []ButtonOpts, spacing string) string {
  144. if spacing == "" {
  145. spacing = " "
  146. }
  147. var parts []string
  148. for i, button := range buttons {
  149. parts = append(parts, SelectableButton(button))
  150. if i < len(buttons)-1 {
  151. parts = append(parts, spacing)
  152. }
  153. }
  154. return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
  155. }
  156. // SelectableButtonsVertical creates a vertical row of selectable buttons
  157. func SelectableButtonsVertical(buttons []ButtonOpts, spacing int) string {
  158. var parts []string
  159. for i, button := range buttons {
  160. parts = append(parts, SelectableButton(button))
  161. if i < len(buttons)-1 {
  162. for range spacing {
  163. parts = append(parts, "")
  164. }
  165. }
  166. }
  167. return lipgloss.JoinVertical(lipgloss.Center, parts...)
  168. }
  169. func DiffFormatter() *diffview.DiffView {
  170. t := styles.CurrentTheme()
  171. formatDiff := diffview.New()
  172. style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
  173. diff := formatDiff.ChromaStyle(style).Style(t.S().Diff).TabWidth(4)
  174. return diff
  175. }