modal.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package modal
  2. import (
  3. "strings"
  4. "github.com/charmbracelet/lipgloss/v2"
  5. "github.com/sst/opencode/internal/layout"
  6. "github.com/sst/opencode/internal/styles"
  7. "github.com/sst/opencode/internal/theme"
  8. )
  9. // CloseModalMsg is a message to signal that the active modal should be closed.
  10. type CloseModalMsg struct{}
  11. // Modal is a reusable modal component that handles frame rendering and overlay placement
  12. type Modal struct {
  13. width int
  14. height int
  15. title string
  16. maxWidth int
  17. maxHeight int
  18. fitContent bool
  19. }
  20. // ModalOption is a function that configures a Modal
  21. type ModalOption func(*Modal)
  22. // WithTitle sets the modal title
  23. func WithTitle(title string) ModalOption {
  24. return func(m *Modal) {
  25. m.title = title
  26. }
  27. }
  28. // WithMaxWidth sets the maximum width
  29. func WithMaxWidth(width int) ModalOption {
  30. return func(m *Modal) {
  31. m.maxWidth = width
  32. m.fitContent = false
  33. }
  34. }
  35. // WithMaxHeight sets the maximum height
  36. func WithMaxHeight(height int) ModalOption {
  37. return func(m *Modal) {
  38. m.maxHeight = height
  39. }
  40. }
  41. func WithFitContent(fit bool) ModalOption {
  42. return func(m *Modal) {
  43. m.fitContent = fit
  44. }
  45. }
  46. // New creates a new Modal with the given options
  47. func New(opts ...ModalOption) *Modal {
  48. m := &Modal{
  49. maxWidth: 0,
  50. maxHeight: 0,
  51. fitContent: true,
  52. }
  53. for _, opt := range opts {
  54. opt(m)
  55. }
  56. return m
  57. }
  58. func (m *Modal) SetTitle(title string) {
  59. m.title = title
  60. }
  61. // Render renders the modal centered on the screen
  62. func (m *Modal) Render(contentView string, background string) string {
  63. t := theme.CurrentTheme()
  64. outerWidth := layout.Current.Container.Width - 8
  65. if m.maxWidth > 0 && outerWidth > m.maxWidth {
  66. outerWidth = m.maxWidth
  67. }
  68. if m.fitContent {
  69. titleWidth := lipgloss.Width(m.title)
  70. contentWidth := lipgloss.Width(contentView)
  71. largestWidth := max(titleWidth+2, contentWidth)
  72. outerWidth = largestWidth + 6
  73. }
  74. innerWidth := outerWidth - 4
  75. baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel())
  76. var finalContent string
  77. if m.title != "" {
  78. titleStyle := baseStyle.
  79. Foreground(t.Text()).
  80. Bold(true).
  81. Padding(0, 1)
  82. escStyle := baseStyle.Foreground(t.TextMuted())
  83. escText := escStyle.Render("esc")
  84. // Calculate position for esc text
  85. titleWidth := lipgloss.Width(m.title)
  86. escWidth := lipgloss.Width(escText)
  87. spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
  88. spacer := strings.Repeat(" ", spacesNeeded)
  89. titleLine := m.title + spacer + escText
  90. titleLine = titleStyle.Render(titleLine)
  91. finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
  92. } else {
  93. finalContent = contentView
  94. }
  95. modalStyle := baseStyle.
  96. PaddingTop(1).
  97. PaddingBottom(1).
  98. PaddingLeft(2).
  99. PaddingRight(2)
  100. modalView := modalStyle.
  101. Width(outerWidth).
  102. Render(finalContent)
  103. // Calculate position for centering
  104. bgHeight := lipgloss.Height(background)
  105. bgWidth := lipgloss.Width(background)
  106. modalHeight := lipgloss.Height(modalView)
  107. modalWidth := lipgloss.Width(modalView)
  108. row := (bgHeight - modalHeight) / 2
  109. col := (bgWidth - modalWidth) / 2
  110. return layout.PlaceOverlay(
  111. col-1, // TODO: whyyyyy
  112. row,
  113. modalView,
  114. background,
  115. layout.WithOverlayBorder(),
  116. layout.WithOverlayBorderColor(t.BorderActive()),
  117. )
  118. }