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