1package dialog
2
3import (
4 "context"
5
6 "charm.land/bubbles/v2/help"
7 "charm.land/bubbles/v2/key"
8 "charm.land/bubbles/v2/textinput"
9 tea "charm.land/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/list"
12)
13
14// SessionsID is the identifier for the session selector dialog.
15const SessionsID = "session"
16
17// Session is a session selector dialog.
18type Session struct {
19 width, height int
20 com *common.Common
21 help help.Model
22 list *list.FilterableList
23 input textinput.Model
24 selectedSessionInx int
25
26 keyMap struct {
27 Select key.Binding
28 Next key.Binding
29 Previous key.Binding
30 Close key.Binding
31 }
32}
33
34var _ Dialog = (*Session)(nil)
35
36// NewSessions creates a new Session dialog.
37func NewSessions(com *common.Common, selectedSessionID string) (*Session, error) {
38 s := new(Session)
39 s.com = com
40 sessions, err := com.App.Sessions.List(context.TODO())
41 if err != nil {
42 return nil, err
43 }
44
45 for i, sess := range sessions {
46 if sess.ID == selectedSessionID {
47 s.selectedSessionInx = i
48 break
49 }
50 }
51
52 help := help.New()
53 help.Styles = com.Styles.DialogHelpStyles()
54
55 s.help = help
56 s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
57 s.list.Focus()
58
59 s.input = textinput.New()
60 s.input.SetVirtualCursor(false)
61 s.input.Placeholder = "Enter session name"
62 s.input.SetStyles(com.Styles.TextInput)
63 s.input.Focus()
64
65 s.keyMap.Select = key.NewBinding(
66 key.WithKeys("enter", "tab", "ctrl+y"),
67 key.WithHelp("enter", "choose"),
68 )
69 s.keyMap.Next = key.NewBinding(
70 key.WithKeys("down", "ctrl+n"),
71 key.WithHelp("↓", "next item"),
72 )
73 s.keyMap.Previous = key.NewBinding(
74 key.WithKeys("up", "ctrl+p"),
75 key.WithHelp("↑", "previous item"),
76 )
77 s.keyMap.Close = CloseKey
78
79 return s, nil
80}
81
82// SetSize sets the size of the dialog.
83func (s *Session) SetSize(width, height int) {
84 t := s.com.Styles
85 s.width = width
86 s.height = height
87 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
88 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + 1 + // (1) title content
89 t.Dialog.InputPrompt.GetVerticalFrameSize() + 1 + // (1) input content
90 t.Dialog.HelpView.GetVerticalFrameSize() +
91 t.Dialog.View.GetVerticalFrameSize()
92 s.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
93 s.list.SetSize(innerWidth, height-heightOffset)
94 s.help.SetWidth(width)
95
96 // Now that we know the height we can select the selected session and scroll to it.
97 s.list.SetSelected(s.selectedSessionInx)
98 s.list.ScrollToSelected()
99}
100
101// ID implements Dialog.
102func (s *Session) ID() string {
103 return SessionsID
104}
105
106// Update implements Dialog.
107func (s *Session) Update(msg tea.Msg) tea.Msg {
108 switch msg := msg.(type) {
109 case tea.KeyPressMsg:
110 switch {
111 case key.Matches(msg, s.keyMap.Close):
112 return CloseMsg{}
113 case key.Matches(msg, s.keyMap.Previous):
114 s.list.Focus()
115 if s.list.IsSelectedFirst() {
116 s.list.SelectLast()
117 s.list.ScrollToBottom()
118 break
119 }
120 s.list.SelectPrev()
121 s.list.ScrollToSelected()
122 case key.Matches(msg, s.keyMap.Next):
123 s.list.Focus()
124 if s.list.IsSelectedLast() {
125 s.list.SelectFirst()
126 s.list.ScrollToTop()
127 break
128 }
129 s.list.SelectNext()
130 s.list.ScrollToSelected()
131 case key.Matches(msg, s.keyMap.Select):
132 if item := s.list.SelectedItem(); item != nil {
133 sessionItem := item.(*SessionItem)
134 return SessionSelectedMsg{sessionItem.Session}
135 }
136 default:
137 var cmd tea.Cmd
138 s.input, cmd = s.input.Update(msg)
139 value := s.input.Value()
140 s.list.SetFilter(value)
141 s.list.ScrollToTop()
142 s.list.SetSelected(0)
143 return cmd
144 }
145 }
146 return nil
147}
148
149// Cursor returns the cursor position relative to the dialog.
150func (s *Session) Cursor() *tea.Cursor {
151 return InputCursor(s.com.Styles, s.input.Cursor())
152}
153
154// View implements [Dialog].
155func (s *Session) View() string {
156 titleStyle := s.com.Styles.Dialog.Title
157 dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
158 header := common.DialogTitle(s.com.Styles, "Switch Session",
159 max(0, s.width-dialogStyle.GetHorizontalFrameSize()-
160 titleStyle.GetHorizontalFrameSize()))
161
162 return HeaderInputListHelpView(s.com.Styles, s.width, s.list.Height(), header,
163 s.input.View(), s.list.Render(), s.help.View(s))
164}
165
166// ShortHelp implements [help.KeyMap].
167func (s *Session) ShortHelp() []key.Binding {
168 updown := key.NewBinding(
169 key.WithKeys("down", "up"),
170 key.WithHelp("↑↓", "choose"),
171 )
172 return []key.Binding{
173 updown,
174 s.keyMap.Select,
175 s.keyMap.Close,
176 }
177}
178
179// FullHelp implements [help.KeyMap].
180func (s *Session) FullHelp() [][]key.Binding {
181 m := [][]key.Binding{}
182 slice := []key.Binding{
183 s.keyMap.Select,
184 s.keyMap.Next,
185 s.keyMap.Previous,
186 s.keyMap.Close,
187 }
188 for i := 0; i < len(slice); i += 4 {
189 end := min(i+4, len(slice))
190 m = append(m, slice[i:end])
191 }
192 return m
193}