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