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() tea.View {
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.String(),
132 "",
133 t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
134 )
135
136 v := tea.NewView(s.style().Render(content))
137 if listView.Cursor() != nil {
138 c := s.moveCursor(listView.Cursor())
139 v.SetCursor(c)
140 }
141 return v
142}
143
144func (s *sessionDialogCmp) style() lipgloss.Style {
145 t := styles.CurrentTheme()
146 return t.S().Base.
147 Width(s.width).
148 Border(lipgloss.RoundedBorder()).
149 BorderForeground(t.BorderFocus)
150}
151
152func (s *sessionDialogCmp) listHeight() int {
153 return s.wHeight/2 - 6 // 5 for the border, title and help
154}
155
156func (s *sessionDialogCmp) listWidth() int {
157 return s.width - 2 // 2 for the border
158}
159
160func (s *sessionDialogCmp) Position() (int, int) {
161 row := s.wHeight/4 - 2 // just a bit above the center
162 col := s.wWidth / 2
163 col -= s.width / 2
164 return row, col
165}
166
167func (s *sessionDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
168 row, col := s.Position()
169 offset := row + 3 // Border + title
170 cursor.Y += offset
171 cursor.X = cursor.X + col + 2
172 return cursor
173}
174
175// ID implements SessionDialog.
176func (s *sessionDialogCmp) ID() dialogs.DialogID {
177 return SessionsDialogID
178}