sessions.go

  1package dialog
  2
  3import (
  4	"charm.land/bubbles/v2/help"
  5	"charm.land/bubbles/v2/key"
  6	"charm.land/bubbles/v2/textinput"
  7	tea "charm.land/bubbletea/v2"
  8	"github.com/charmbracelet/crush/internal/session"
  9	"github.com/charmbracelet/crush/internal/ui/common"
 10	"github.com/charmbracelet/crush/internal/ui/list"
 11)
 12
 13// SessionsID is the identifier for the session selector dialog.
 14const SessionsID = "session"
 15
 16// Session is a session selector dialog.
 17type Session struct {
 18	width, height int
 19	com           *common.Common
 20	help          help.Model
 21	list          *list.FilterableList
 22	input         textinput.Model
 23
 24	keyMap struct {
 25		Select   key.Binding
 26		Next     key.Binding
 27		Previous key.Binding
 28		Close    key.Binding
 29	}
 30}
 31
 32var _ Dialog = (*Session)(nil)
 33
 34// NewSessions creates a new Session dialog.
 35func NewSessions(com *common.Common, sessions ...session.Session) *Session {
 36	s := new(Session)
 37	s.com = com
 38	help := help.New()
 39	help.Styles = com.Styles.DialogHelpStyles()
 40
 41	s.help = help
 42	s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
 43	s.list.Focus()
 44	s.list.SetSelected(0)
 45
 46	s.input = textinput.New()
 47	s.input.SetVirtualCursor(false)
 48	s.input.Placeholder = "Enter session name"
 49	s.input.SetStyles(com.Styles.TextInput)
 50	s.input.Focus()
 51
 52	s.keyMap.Select = key.NewBinding(
 53		key.WithKeys("enter", "tab", "ctrl+y"),
 54		key.WithHelp("enter", "choose"),
 55	)
 56	s.keyMap.Next = key.NewBinding(
 57		key.WithKeys("down", "ctrl+n"),
 58		key.WithHelp("↓", "next item"),
 59	)
 60	s.keyMap.Previous = key.NewBinding(
 61		key.WithKeys("up", "ctrl+p"),
 62		key.WithHelp("↑", "previous item"),
 63	)
 64	s.keyMap.Close = CloseKey
 65	return s
 66}
 67
 68// SetSize sets the size of the dialog.
 69func (s *Session) SetSize(width, height int) {
 70	t := s.com.Styles
 71	s.width = width
 72	s.height = height
 73	innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
 74	heightOffset := t.Dialog.Title.GetVerticalFrameSize() + 1 + // (1) title content
 75		t.Dialog.InputPrompt.GetVerticalFrameSize() + 1 + // (1) input content
 76		t.Dialog.HelpView.GetVerticalFrameSize() +
 77		t.Dialog.View.GetVerticalFrameSize()
 78	s.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
 79	s.list.SetSize(innerWidth, height-heightOffset)
 80	s.help.SetWidth(width)
 81}
 82
 83// ID implements Dialog.
 84func (s *Session) ID() string {
 85	return SessionsID
 86}
 87
 88// Update implements Dialog.
 89func (s *Session) Update(msg tea.Msg) tea.Msg {
 90	switch msg := msg.(type) {
 91	case tea.KeyPressMsg:
 92		switch {
 93		case key.Matches(msg, s.keyMap.Close):
 94			return CloseMsg{}
 95		case key.Matches(msg, s.keyMap.Previous):
 96			s.list.Focus()
 97			if s.list.IsSelectedFirst() {
 98				s.list.SelectLast()
 99				s.list.ScrollToBottom()
100				break
101			}
102			s.list.SelectPrev()
103			s.list.ScrollToSelected()
104		case key.Matches(msg, s.keyMap.Next):
105			s.list.Focus()
106			if s.list.IsSelectedLast() {
107				s.list.SelectFirst()
108				s.list.ScrollToTop()
109				break
110			}
111			s.list.SelectNext()
112			s.list.ScrollToSelected()
113		case key.Matches(msg, s.keyMap.Select):
114			if item := s.list.SelectedItem(); item != nil {
115				sessionItem := item.(*SessionItem)
116				return SessionSelectedMsg{sessionItem.Session}
117			}
118		default:
119			var cmd tea.Cmd
120			s.input, cmd = s.input.Update(msg)
121			value := s.input.Value()
122			s.list.SetFilter(value)
123			s.list.ScrollToTop()
124			s.list.SetSelected(0)
125			return cmd
126		}
127	}
128	return nil
129}
130
131// Cursor returns the cursor position relative to the dialog.
132func (s *Session) Cursor() *tea.Cursor {
133	return InputCursor(s.com.Styles, s.input.Cursor())
134}
135
136// View implements [Dialog].
137func (s *Session) View() string {
138	titleStyle := s.com.Styles.Dialog.Title
139	dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
140	header := common.DialogTitle(s.com.Styles, "Switch Session",
141		max(0, s.width-dialogStyle.GetHorizontalFrameSize()-
142			titleStyle.GetHorizontalFrameSize()))
143
144	return HeaderInputListHelpView(s.com.Styles, s.width, s.list.Height(), header,
145		s.input.View(), s.list.Render(), s.help.View(s))
146}
147
148// ShortHelp implements [help.KeyMap].
149func (s *Session) ShortHelp() []key.Binding {
150	updown := key.NewBinding(
151		key.WithKeys("down", "up"),
152		key.WithHelp("↑↓", "choose"),
153	)
154	return []key.Binding{
155		updown,
156		s.keyMap.Select,
157		s.keyMap.Close,
158	}
159}
160
161// FullHelp implements [help.KeyMap].
162func (s *Session) FullHelp() [][]key.Binding {
163	m := [][]key.Binding{}
164	slice := []key.Binding{
165		s.keyMap.Select,
166		s.keyMap.Next,
167		s.keyMap.Previous,
168		s.keyMap.Close,
169	}
170	for i := 0; i < len(slice); i += 4 {
171		end := min(i+4, len(slice))
172		m = append(m, slice[i:end])
173	}
174	return m
175}