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