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