sessions.go

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