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