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}