@@ -0,0 +1,196 @@
1+package dialog
2+
3+import (
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.
17+const SessionDialogID = "session"
18+
19+// Session is a session selector dialog.
20+type 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+
35+var _ Dialog = (*Session)(nil)
36+
37+// SessionSelectedMsg is a message sent when a session is selected.
38+type SessionSelectedMsg struct {
39+ Session session.Session
40+}
41+
42+// NewSessions creates a new Session dialog.
43+func 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.
77+func (s *Session) Cursor() *tea.Cursor {
78+ return s.input.Cursor()
79+}
80+
81+// SetSize sets the size of the dialog.
82+func (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.
93+func (s *Session) SelectedItem() list.Item {
94+ return s.list.SelectedItem()
95+}
96+
97+// ID implements Dialog.
98+func (s *Session) ID() string {
99+ return SessionDialogID
100+}
101+
102+// Update implements Dialog.
103+func (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.
131+func (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].
163+func (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].
176+func (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.
192+func SessionSelectCmd(s session.Session) tea.Cmd {
193+ return func() tea.Msg {
194+ return SessionSelectedMsg{Session: s}
195+ }
196+}