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