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