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// SessionsID is the identifier for the session selector dialog.
17const SessionsID = "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// ID implements Dialog.
92func (s *Session) ID() string {
93 return SessionsID
94}
95
96// Update implements Dialog.
97func (s *Session) Update(msg tea.Msg) tea.Cmd {
98 switch msg := msg.(type) {
99 case tea.KeyPressMsg:
100 switch {
101 case key.Matches(msg, s.keyMap.Previous):
102 s.list.Focus()
103 s.list.SelectPrev()
104 s.list.ScrollToSelected()
105 case key.Matches(msg, s.keyMap.Next):
106 s.list.Focus()
107 s.list.SelectNext()
108 s.list.ScrollToSelected()
109 case key.Matches(msg, s.keyMap.Select):
110 if item := s.list.SelectedItem(); item != nil {
111 sessionItem := item.(*SessionItem)
112 return SessionSelectCmd(sessionItem.Session)
113 }
114 default:
115 var cmd tea.Cmd
116 s.input, cmd = s.input.Update(msg)
117 s.list.SetFilter(s.input.Value())
118 return cmd
119 }
120 }
121 return nil
122}
123
124// View implements [Dialog].
125func (s *Session) View() string {
126 titleStyle := s.com.Styles.Dialog.Title
127 helpStyle := s.com.Styles.Dialog.HelpView
128 dialogStyle := s.com.Styles.Dialog.View.Width(s.width)
129 inputStyle := s.com.Styles.Dialog.InputPrompt
130 helpStyle = helpStyle.Width(s.width - dialogStyle.GetHorizontalFrameSize())
131 listContent := s.list.Render()
132 if nlines := lipgloss.Height(listContent); nlines < s.list.Height() {
133 // pad the list content to avoid jumping when navigating
134 listContent += strings.Repeat("\n", max(0, s.list.Height()-nlines))
135 }
136
137 content := strings.Join([]string{
138 titleStyle.Render(
139 common.DialogTitle(
140 s.com.Styles,
141 "Switch Session",
142 max(0, s.width-
143 dialogStyle.GetHorizontalFrameSize()-
144 titleStyle.GetHorizontalFrameSize()))),
145 "",
146 inputStyle.Render(s.input.View()),
147 "",
148 listContent,
149 "",
150 helpStyle.Render(s.help.View(s)),
151 }, "\n")
152
153 return dialogStyle.Render(content)
154}
155
156// ShortHelp implements [help.KeyMap].
157func (s *Session) ShortHelp() []key.Binding {
158 updown := key.NewBinding(
159 key.WithKeys("down", "up"),
160 key.WithHelp("↑↓", "choose"),
161 )
162 return []key.Binding{
163 updown,
164 s.keyMap.Select,
165 s.keyMap.Close,
166 }
167}
168
169// FullHelp implements [help.KeyMap].
170func (s *Session) FullHelp() [][]key.Binding {
171 m := [][]key.Binding{}
172 slice := []key.Binding{
173 s.keyMap.Select,
174 s.keyMap.Next,
175 s.keyMap.Previous,
176 s.keyMap.Close,
177 }
178 for i := 0; i < len(slice); i += 4 {
179 end := min(i+4, len(slice))
180 m = append(m, slice[i:end])
181 }
182 return m
183}
184
185// SessionSelectCmd creates a command that sends a SessionSelectMsg.
186func SessionSelectCmd(s session.Session) tea.Cmd {
187 return func() tea.Msg {
188 return SessionSelectedMsg{Session: s}
189 }
190}