sessions.go

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