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