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