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(defaultDialogMaxWidth, area.Dx()))
147 height := max(0, min(defaultDialogHeight, area.Dy()))
148 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize() - 2
149 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
150 t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
151 t.Dialog.HelpView.GetVerticalFrameSize() +
152 t.Dialog.View.GetVerticalFrameSize()
153 s.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
154 s.list.SetSize(innerWidth, height-heightOffset)
155 s.help.SetWidth(innerWidth)
156
157 titleStyle := s.com.Styles.Dialog.Title
158 dialogStyle := s.com.Styles.Dialog.View.Width(width)
159 header := common.DialogTitle(s.com.Styles, "Switch Session",
160 max(0, width-dialogStyle.GetHorizontalFrameSize()-
161 titleStyle.GetHorizontalFrameSize()))
162
163 helpView := ansi.Truncate(s.help.View(s), innerWidth, "")
164 view := HeaderInputListHelpView(s.com.Styles, width, s.list.Height(), header,
165 s.input.View(), s.list.Render(), helpView)
166
167 cur := s.Cursor()
168 DrawCenterCursor(scr, area, view, cur)
169 return cur
170}
171
172// ShortHelp implements [help.KeyMap].
173func (s *Session) ShortHelp() []key.Binding {
174 return []key.Binding{
175 s.keyMap.UpDown,
176 s.keyMap.Select,
177 s.keyMap.Close,
178 }
179}
180
181// FullHelp implements [help.KeyMap].
182func (s *Session) FullHelp() [][]key.Binding {
183 m := [][]key.Binding{}
184 slice := []key.Binding{
185 s.keyMap.Select,
186 s.keyMap.Next,
187 s.keyMap.Previous,
188 s.keyMap.Close,
189 }
190 for i := 0; i < len(slice); i += 4 {
191 end := min(i+4, len(slice))
192 m = append(m, slice[i:end])
193 }
194 return m
195}