sessions.go

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