modal.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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. // Base style for the modal
  76. baseStyle := styles.BaseStyle().
  77. Background(t.BackgroundElement()).
  78. Foreground(t.TextMuted())
  79. // Add title if provided
  80. var finalContent string
  81. if m.title != "" {
  82. titleStyle := baseStyle.
  83. Foreground(t.Primary()).
  84. Bold(true).
  85. Padding(0, 1)
  86. escStyle := baseStyle.Foreground(t.TextMuted())
  87. escText := escStyle.Render("esc")
  88. // Calculate position for esc text
  89. titleWidth := lipgloss.Width(m.title)
  90. escWidth := lipgloss.Width(escText)
  91. spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
  92. spacer := strings.Repeat(" ", spacesNeeded)
  93. titleLine := m.title + spacer + escText
  94. titleLine = titleStyle.Render(titleLine)
  95. finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
  96. } else {
  97. finalContent = contentView
  98. }
  99. modalStyle := baseStyle.
  100. PaddingTop(1).
  101. PaddingBottom(1).
  102. PaddingLeft(2).
  103. PaddingRight(2)
  104. modalView := modalStyle.
  105. Width(outerWidth).
  106. Render(finalContent)
  107. // Calculate position for centering
  108. bgHeight := lipgloss.Height(background)
  109. bgWidth := lipgloss.Width(background)
  110. modalHeight := lipgloss.Height(modalView)
  111. modalWidth := lipgloss.Width(modalView)
  112. row := (bgHeight - modalHeight) / 2
  113. col := (bgWidth - modalWidth) / 2
  114. return layout.PlaceOverlay(
  115. col,
  116. row,
  117. modalView,
  118. background,
  119. layout.WithOverlayBorder(),
  120. layout.WithOverlayBorderColor(t.Primary()),
  121. )
  122. }