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
25 keyMap struct {
26 Select key.Binding
27 Next key.Binding
28 Previous key.Binding
29 Close key.Binding
30 }
31}
32
33var _ Dialog = (*Session)(nil)
34
35// NewSessions creates a new Session dialog.
36func NewSessions(com *common.Common) (*Session, error) {
37 s := new(Session)
38 s.com = com
39 sessions, err := com.App.Sessions.List(context.TODO())
40 if err != nil {
41 return nil, err
42 }
43
44 help := help.New()
45 help.Styles = com.Styles.DialogHelpStyles()
46
47 s.help = help
48 s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
49 s.list.Focus()
50 s.list.SetSelected(0)
51
52 s.input = textinput.New()
53 s.input.SetVirtualCursor(false)
54 s.input.Placeholder = "Enter session name"
55 s.input.SetStyles(com.Styles.TextInput)
56 s.input.Focus()
57
58 s.keyMap.Select = key.NewBinding(
59 key.WithKeys("enter", "tab", "ctrl+y"),
60 key.WithHelp("enter", "choose"),
61 )
62 s.keyMap.Next = key.NewBinding(
63 key.WithKeys("down", "ctrl+n"),
64 key.WithHelp("↓", "next item"),
65 )
66 s.keyMap.Previous = key.NewBinding(
67 key.WithKeys("up", "ctrl+p"),
68 key.WithHelp("↑", "previous item"),
69 )
70 s.keyMap.Close = CloseKey
71
72 return s, nil
73}
74
75// SetSize sets the size of the dialog.
76func (s *Session) SetSize(width, height int) {
77 t := s.com.Styles
78 s.width = width
79 s.height = height
80 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
81 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + 1 + // (1) title content
82 t.Dialog.InputPrompt.GetVerticalFrameSize() + 1 + // (1) input content
83 t.Dialog.HelpView.GetVerticalFrameSize() +
84 t.Dialog.View.GetVerticalFrameSize()
85 s.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
86 s.list.SetSize(innerWidth, height-heightOffset)
87 s.help.SetWidth(width)
88}
89
90// ID implements Dialog.
91func (s *Session) ID() string {
92 return SessionsID
93}
94
95// Update implements Dialog.
96func (s *Session) Update(msg tea.Msg) tea.Msg {
97 switch msg := msg.(type) {
98 case tea.KeyPressMsg:
99 switch {
100 case key.Matches(msg, s.keyMap.Close):
101 return CloseMsg{}
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 SessionSelectedMsg{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 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// View implements [Dialog].
144func (s *Session) View() string {
145 titleStyle := s.com.Styles.Dialog.Title
146 dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
147 header := common.DialogTitle(s.com.Styles, "Switch Session",
148 max(0, s.width-dialogStyle.GetHorizontalFrameSize()-
149 titleStyle.GetHorizontalFrameSize()))
150
151 return HeaderInputListHelpView(s.com.Styles, s.width, s.list.Height(), header,
152 s.input.View(), s.list.Render(), s.help.View(s))
153}
154
155// ShortHelp implements [help.KeyMap].
156func (s *Session) ShortHelp() []key.Binding {
157 updown := key.NewBinding(
158 key.WithKeys("down", "up"),
159 key.WithHelp("↑↓", "choose"),
160 )
161 return []key.Binding{
162 updown,
163 s.keyMap.Select,
164 s.keyMap.Close,
165 }
166}
167
168// FullHelp implements [help.KeyMap].
169func (s *Session) FullHelp() [][]key.Binding {
170 m := [][]key.Binding{}
171 slice := []key.Binding{
172 s.keyMap.Select,
173 s.keyMap.Next,
174 s.keyMap.Previous,
175 s.keyMap.Close,
176 }
177 for i := 0; i < len(slice); i += 4 {
178 end := min(i+4, len(slice))
179 m = append(m, slice[i:end])
180 }
181 return m
182}