sessions.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. package sessions
  2. import (
  3. "github.com/charmbracelet/bubbles/v2/help"
  4. "github.com/charmbracelet/bubbles/v2/key"
  5. tea "github.com/charmbracelet/bubbletea/v2"
  6. "github.com/charmbracelet/crush/internal/event"
  7. "github.com/charmbracelet/crush/internal/session"
  8. "github.com/charmbracelet/crush/internal/tui/components/chat"
  9. "github.com/charmbracelet/crush/internal/tui/components/core"
  10. "github.com/charmbracelet/crush/internal/tui/components/dialogs"
  11. "github.com/charmbracelet/crush/internal/tui/exp/list"
  12. "github.com/charmbracelet/crush/internal/tui/styles"
  13. "github.com/charmbracelet/crush/internal/tui/util"
  14. "github.com/charmbracelet/lipgloss/v2"
  15. )
  16. const SessionsDialogID dialogs.DialogID = "sessions"
  17. // SessionDialog interface for the session switching dialog
  18. type SessionDialog interface {
  19. dialogs.DialogModel
  20. }
  21. type SessionsList = list.FilterableList[list.CompletionItem[session.Session]]
  22. type sessionDialogCmp struct {
  23. selectedInx int
  24. wWidth int
  25. wHeight int
  26. width int
  27. selectedSessionID string
  28. keyMap KeyMap
  29. sessionsList SessionsList
  30. help help.Model
  31. }
  32. // NewSessionDialogCmp creates a new session switching dialog
  33. func NewSessionDialogCmp(sessions []session.Session, selectedID string) SessionDialog {
  34. t := styles.CurrentTheme()
  35. listKeyMap := list.DefaultKeyMap()
  36. keyMap := DefaultKeyMap()
  37. listKeyMap.Down.SetEnabled(false)
  38. listKeyMap.Up.SetEnabled(false)
  39. listKeyMap.DownOneItem = keyMap.Next
  40. listKeyMap.UpOneItem = keyMap.Previous
  41. items := make([]list.CompletionItem[session.Session], len(sessions))
  42. if len(sessions) > 0 {
  43. for i, session := range sessions {
  44. items[i] = list.NewCompletionItem(session.Title, session, list.WithCompletionID(session.ID))
  45. }
  46. }
  47. inputStyle := t.S().Base.PaddingLeft(1).PaddingBottom(1)
  48. sessionsList := list.NewFilterableList(
  49. items,
  50. list.WithFilterPlaceholder("Enter a session name"),
  51. list.WithFilterInputStyle(inputStyle),
  52. list.WithFilterListOptions(
  53. list.WithKeyMap(listKeyMap),
  54. list.WithWrapNavigation(),
  55. ),
  56. )
  57. help := help.New()
  58. help.Styles = t.S().Help
  59. s := &sessionDialogCmp{
  60. selectedSessionID: selectedID,
  61. keyMap: DefaultKeyMap(),
  62. sessionsList: sessionsList,
  63. help: help,
  64. }
  65. return s
  66. }
  67. func (s *sessionDialogCmp) Init() tea.Cmd {
  68. var cmds []tea.Cmd
  69. cmds = append(cmds, s.sessionsList.Init())
  70. cmds = append(cmds, s.sessionsList.Focus())
  71. return tea.Sequence(cmds...)
  72. }
  73. func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
  74. switch msg := msg.(type) {
  75. case tea.WindowSizeMsg:
  76. var cmds []tea.Cmd
  77. s.wWidth = msg.Width
  78. s.wHeight = msg.Height
  79. s.width = min(120, s.wWidth-8)
  80. s.sessionsList.SetInputWidth(s.listWidth() - 2)
  81. cmds = append(cmds, s.sessionsList.SetSize(s.listWidth(), s.listHeight()))
  82. if s.selectedSessionID != "" {
  83. cmds = append(cmds, s.sessionsList.SetSelected(s.selectedSessionID))
  84. }
  85. return s, tea.Batch(cmds...)
  86. case tea.KeyPressMsg:
  87. switch {
  88. case key.Matches(msg, s.keyMap.Select):
  89. selectedItem := s.sessionsList.SelectedItem()
  90. if selectedItem != nil {
  91. selected := *selectedItem
  92. event.SessionSwitched()
  93. return s, tea.Sequence(
  94. util.CmdHandler(dialogs.CloseDialogMsg{}),
  95. util.CmdHandler(
  96. chat.SessionSelectedMsg(selected.Value()),
  97. ),
  98. )
  99. }
  100. case key.Matches(msg, s.keyMap.Close):
  101. return s, util.CmdHandler(dialogs.CloseDialogMsg{})
  102. default:
  103. u, cmd := s.sessionsList.Update(msg)
  104. s.sessionsList = u.(SessionsList)
  105. return s, cmd
  106. }
  107. }
  108. return s, nil
  109. }
  110. func (s *sessionDialogCmp) View() string {
  111. t := styles.CurrentTheme()
  112. listView := s.sessionsList.View()
  113. content := lipgloss.JoinVertical(
  114. lipgloss.Left,
  115. t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
  116. listView,
  117. "",
  118. t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
  119. )
  120. return s.style().Render(content)
  121. }
  122. func (s *sessionDialogCmp) Cursor() *tea.Cursor {
  123. if cursor, ok := s.sessionsList.(util.Cursor); ok {
  124. cursor := cursor.Cursor()
  125. if cursor != nil {
  126. cursor = s.moveCursor(cursor)
  127. }
  128. return cursor
  129. }
  130. return nil
  131. }
  132. func (s *sessionDialogCmp) style() lipgloss.Style {
  133. t := styles.CurrentTheme()
  134. return t.S().Base.
  135. Width(s.width).
  136. Border(lipgloss.RoundedBorder()).
  137. BorderForeground(t.BorderFocus)
  138. }
  139. func (s *sessionDialogCmp) listHeight() int {
  140. return s.wHeight/2 - 6 // 5 for the border, title and help
  141. }
  142. func (s *sessionDialogCmp) listWidth() int {
  143. return s.width - 2 // 2 for the border
  144. }
  145. func (s *sessionDialogCmp) Position() (int, int) {
  146. row := s.wHeight/4 - 2 // just a bit above the center
  147. col := s.wWidth / 2
  148. col -= s.width / 2
  149. return row, col
  150. }
  151. func (s *sessionDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
  152. row, col := s.Position()
  153. offset := row + 3 // Border + title
  154. cursor.Y += offset
  155. cursor.X = cursor.X + col + 2
  156. return cursor
  157. }
  158. // ID implements SessionDialog.
  159. func (s *sessionDialogCmp) ID() dialogs.DialogID {
  160. return SessionsDialogID
  161. }