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 uv "github.com/charmbracelet/ultraviolet"
13)
14
15// SessionsID is the identifier for the session selector dialog.
16const SessionsID = "session"
17
18// Session is a session selector dialog.
19type Session struct {
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 s.list.SetSelected(s.selectedSessionInx)
60
61 s.input = textinput.New()
62 s.input.SetVirtualCursor(false)
63 s.input.Placeholder = "Enter session name"
64 s.input.SetStyles(com.Styles.TextInput)
65 s.input.Focus()
66
67 s.keyMap.Select = key.NewBinding(
68 key.WithKeys("enter", "tab", "ctrl+y"),
69 key.WithHelp("enter", "choose"),
70 )
71 s.keyMap.Next = key.NewBinding(
72 key.WithKeys("down", "ctrl+n"),
73 key.WithHelp("↓", "next item"),
74 )
75 s.keyMap.Previous = key.NewBinding(
76 key.WithKeys("up", "ctrl+p"),
77 key.WithHelp("↑", "previous item"),
78 )
79 s.keyMap.UpDown = key.NewBinding(
80 key.WithKeys("up", "down"),
81 key.WithHelp("↑↓", "choose"),
82 )
83 s.keyMap.Close = CloseKey
84
85 return s, nil
86}
87
88// ID implements Dialog.
89func (s *Session) ID() string {
90 return SessionsID
91}
92
93// HandleMsg implements Dialog.
94func (s *Session) HandleMsg(msg tea.Msg) Action {
95 switch msg := msg.(type) {
96 case tea.KeyPressMsg:
97 switch {
98 case key.Matches(msg, s.keyMap.Close):
99 return ActionClose{}
100 case key.Matches(msg, s.keyMap.Previous):
101 s.list.Focus()
102 if s.list.IsSelectedFirst() {
103 s.list.SelectLast()
104 s.list.ScrollToBottom()
105 break
106 }
107 s.list.SelectPrev()
108 s.list.ScrollToSelected()
109 case key.Matches(msg, s.keyMap.Next):
110 s.list.Focus()
111 if s.list.IsSelectedLast() {
112 s.list.SelectFirst()
113 s.list.ScrollToTop()
114 break
115 }
116 s.list.SelectNext()
117 s.list.ScrollToSelected()
118 case key.Matches(msg, s.keyMap.Select):
119 if item := s.list.SelectedItem(); item != nil {
120 sessionItem := item.(*SessionItem)
121 return ActionSelectSession{sessionItem.Session}
122 }
123 default:
124 var cmd tea.Cmd
125 s.input, cmd = s.input.Update(msg)
126 value := s.input.Value()
127 s.list.SetFilter(value)
128 s.list.ScrollToTop()
129 s.list.SetSelected(0)
130 return ActionCmd{cmd}
131 }
132 }
133 return nil
134}
135
136// Cursor returns the cursor position relative to the dialog.
137func (s *Session) Cursor() *tea.Cursor {
138 return InputCursor(s.com.Styles, s.input.Cursor())
139}
140
141// Draw implements [Dialog].
142func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
143 t := s.com.Styles
144 width := max(0, min(defaultDialogMaxWidth, area.Dx()))
145 height := max(0, min(defaultDialogHeight, area.Dy()))
146 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize() - 2
147 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
148 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
149 t.Dialog.HelpView.GetVerticalFrameSize() +
150 t.Dialog.View.GetVerticalFrameSize()
151 s.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
152 s.list.SetSize(innerWidth, height-heightOffset)
153 s.help.SetWidth(innerWidth)
154
155 // This makes it so we do not scroll the list if we don't have to
156 start, end := s.list.VisibleItemIndices()
157
158 // if selected index is outside visible range, scroll to it
159 if s.selectedSessionInx < start || s.selectedSessionInx > end {
160 s.list.ScrollToSelected()
161 }
162
163 rc := NewRenderContext(t, width)
164 rc.Title = "Switch Session"
165 inputView := t.Dialog.InputPrompt.Render(s.input.View())
166 rc.AddPart(inputView)
167 listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render())
168 rc.AddPart(listView)
169 rc.Help = s.help.View(s)
170
171 view := rc.Render()
172
173 cur := s.Cursor()
174 DrawCenterCursor(scr, area, view, cur)
175 return cur
176}
177
178// ShortHelp implements [help.KeyMap].
179func (s *Session) ShortHelp() []key.Binding {
180 return []key.Binding{
181 s.keyMap.UpDown,
182 s.keyMap.Select,
183 s.keyMap.Close,
184 }
185}
186
187// FullHelp implements [help.KeyMap].
188func (s *Session) FullHelp() [][]key.Binding {
189 m := [][]key.Binding{}
190 slice := []key.Binding{
191 s.keyMap.Select,
192 s.keyMap.Next,
193 s.keyMap.Previous,
194 s.keyMap.Close,
195 }
196 for i := 0; i < len(slice); i += 4 {
197 end := min(i+4, len(slice))
198 m = append(m, slice[i:end])
199 }
200 return m
201}