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.Delete):
115 if len(s.sessionsList.Items()) > 0 {
116 items := s.sessionsList.Items()
117 selectedItemInx := s.sessionsList.SelectedIndex()
118 selectedSession := items[selectedItemInx].(completions.CompletionItem).Value().(session.Session)
119 return s, util.CmdHandler(dialogs.OpenDialogMsg{
120 Model: NewDeleteSessionDialog(selectedSession),
121 })
122 }
123 case key.Matches(msg, s.keyMap.Close):
124 return s, util.CmdHandler(dialogs.CloseDialogMsg{})
125 default:
126 u, cmd := s.sessionsList.Update(msg)
127 s.sessionsList = u.(list.ListModel)
128 return s, cmd
129 }
130 }
131 return s, nil
132}
133
134func (s *sessionDialogCmp) View() string {
135 t := styles.CurrentTheme()
136 listView := s.sessionsList.View()
137 content := lipgloss.JoinVertical(
138 lipgloss.Left,
139 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
140 listView,
141 "",
142 t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
143 )
144
145 return s.style().Render(content)
146}
147
148func (s *sessionDialogCmp) Cursor() *tea.Cursor {
149 if cursor, ok := s.sessionsList.(util.Cursor); ok {
150 cursor := cursor.Cursor()
151 if cursor != nil {
152 cursor = s.moveCursor(cursor)
153 }
154 return cursor
155 }
156 return nil
157}
158
159func (s *sessionDialogCmp) style() lipgloss.Style {
160 t := styles.CurrentTheme()
161 return t.S().Base.
162 Width(s.width).
163 Border(lipgloss.RoundedBorder()).
164 BorderForeground(t.BorderFocus)
165}
166
167func (s *sessionDialogCmp) listHeight() int {
168 return s.wHeight/2 - 6 // 5 for the border, title and help
169}
170
171func (s *sessionDialogCmp) listWidth() int {
172 return s.width - 2 // 2 for the border
173}
174
175func (s *sessionDialogCmp) Position() (int, int) {
176 row := s.wHeight/4 - 2 // just a bit above the center
177 col := s.wWidth / 2
178 col -= s.width / 2
179 return row, col
180}
181
182func (s *sessionDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
183 row, col := s.Position()
184 offset := row + 3 // Border + title
185 cursor.Y += offset
186 cursor.X = cursor.X + col + 2
187 return cursor
188}
189
190// ID implements SessionDialog.
191func (s *sessionDialogCmp) ID() dialogs.DialogID {
192 return SessionsDialogID
193}