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) (util.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}