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	uv "github.com/charmbracelet/ultraviolet"
 13)
 14
 15// SessionsID is the identifier for the session selector dialog.
 16const SessionsID = "session"
 17
 18// Session is a session selector dialog.
 19type Session struct {
 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	s.list.SetSelected(s.selectedSessionInx)
 60
 61	s.input = textinput.New()
 62	s.input.SetVirtualCursor(false)
 63	s.input.Placeholder = "Enter session name"
 64	s.input.SetStyles(com.Styles.TextInput)
 65	s.input.Focus()
 66
 67	s.keyMap.Select = key.NewBinding(
 68		key.WithKeys("enter", "tab", "ctrl+y"),
 69		key.WithHelp("enter", "choose"),
 70	)
 71	s.keyMap.Next = key.NewBinding(
 72		key.WithKeys("down", "ctrl+n"),
 73		key.WithHelp("↓", "next item"),
 74	)
 75	s.keyMap.Previous = key.NewBinding(
 76		key.WithKeys("up", "ctrl+p"),
 77		key.WithHelp("↑", "previous item"),
 78	)
 79	s.keyMap.UpDown = key.NewBinding(
 80		key.WithKeys("up", "down"),
 81		key.WithHelp("↑↓", "choose"),
 82	)
 83	s.keyMap.Close = CloseKey
 84
 85	return s, nil
 86}
 87
 88// ID implements Dialog.
 89func (s *Session) ID() string {
 90	return SessionsID
 91}
 92
 93// HandleMsg implements Dialog.
 94func (s *Session) HandleMsg(msg tea.Msg) Action {
 95	switch msg := msg.(type) {
 96	case tea.KeyPressMsg:
 97		switch {
 98		case key.Matches(msg, s.keyMap.Close):
 99			return ActionClose{}
100		case key.Matches(msg, s.keyMap.Previous):
101			s.list.Focus()
102			if s.list.IsSelectedFirst() {
103				s.list.SelectLast()
104				s.list.ScrollToBottom()
105				break
106			}
107			s.list.SelectPrev()
108			s.list.ScrollToSelected()
109		case key.Matches(msg, s.keyMap.Next):
110			s.list.Focus()
111			if s.list.IsSelectedLast() {
112				s.list.SelectFirst()
113				s.list.ScrollToTop()
114				break
115			}
116			s.list.SelectNext()
117			s.list.ScrollToSelected()
118		case key.Matches(msg, s.keyMap.Select):
119			if item := s.list.SelectedItem(); item != nil {
120				sessionItem := item.(*SessionItem)
121				return ActionSelectSession{sessionItem.Session}
122			}
123		default:
124			var cmd tea.Cmd
125			s.input, cmd = s.input.Update(msg)
126			value := s.input.Value()
127			s.list.SetFilter(value)
128			s.list.ScrollToTop()
129			s.list.SetSelected(0)
130			return ActionCmd{cmd}
131		}
132	}
133	return nil
134}
135
136// Cursor returns the cursor position relative to the dialog.
137func (s *Session) Cursor() *tea.Cursor {
138	return InputCursor(s.com.Styles, s.input.Cursor())
139}
140
141// Draw implements [Dialog].
142func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
143	t := s.com.Styles
144	width := max(0, min(defaultDialogMaxWidth, area.Dx()))
145	height := max(0, min(defaultDialogHeight, area.Dy()))
146	innerWidth := width - t.Dialog.View.GetHorizontalFrameSize() - 2
147	heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
148		t.Dialog.InputPrompt.GetVerticalFrameSize() + inputContentHeight +
149		t.Dialog.HelpView.GetVerticalFrameSize() +
150		t.Dialog.View.GetVerticalFrameSize()
151	s.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
152	s.list.SetSize(innerWidth, height-heightOffset)
153	s.help.SetWidth(innerWidth)
154
155	// This makes it so we do not scroll the list if we don't have to
156	start, end := s.list.VisibleItemIndices()
157
158	// if selected index is outside visible range, scroll to it
159	if s.selectedSessionInx < start || s.selectedSessionInx > end {
160		s.list.ScrollToSelected()
161	}
162
163	rc := NewRenderContext(t, width)
164	rc.Title = "Switch Session"
165	inputView := t.Dialog.InputPrompt.Render(s.input.View())
166	rc.AddPart(inputView)
167	listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render())
168	rc.AddPart(listView)
169	rc.Help = s.help.View(s)
170
171	view := rc.Render()
172
173	cur := s.Cursor()
174	DrawCenterCursor(scr, area, view, cur)
175	return cur
176}
177
178// ShortHelp implements [help.KeyMap].
179func (s *Session) ShortHelp() []key.Binding {
180	return []key.Binding{
181		s.keyMap.UpDown,
182		s.keyMap.Select,
183		s.keyMap.Close,
184	}
185}
186
187// FullHelp implements [help.KeyMap].
188func (s *Session) FullHelp() [][]key.Binding {
189	m := [][]key.Binding{}
190	slice := []key.Binding{
191		s.keyMap.Select,
192		s.keyMap.Next,
193		s.keyMap.Previous,
194		s.keyMap.Close,
195	}
196	for i := 0; i < len(slice); i += 4 {
197		end := min(i+4, len(slice))
198		m = append(m, slice[i:end])
199	}
200	return m
201}