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}