1package core
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
7 "github.com/kujtimiihoxha/termai/internal/tui/styles"
8)
9
10// ButtonKeyMap defines key bindings for the button component
11type ButtonKeyMap struct {
12 Enter key.Binding
13}
14
15// DefaultButtonKeyMap returns default key bindings for the button
16func DefaultButtonKeyMap() ButtonKeyMap {
17 return ButtonKeyMap{
18 Enter: key.NewBinding(
19 key.WithKeys("enter"),
20 key.WithHelp("enter", "select"),
21 ),
22 }
23}
24
25// ShortHelp returns keybinding help
26func (k ButtonKeyMap) ShortHelp() []key.Binding {
27 return []key.Binding{k.Enter}
28}
29
30// FullHelp returns full help info for keybindings
31func (k ButtonKeyMap) FullHelp() [][]key.Binding {
32 return [][]key.Binding{
33 {k.Enter},
34 }
35}
36
37// ButtonState represents the state of a button
38type ButtonState int
39
40const (
41 // ButtonNormal is the default state
42 ButtonNormal ButtonState = iota
43 // ButtonHovered is when the button is focused/hovered
44 ButtonHovered
45 // ButtonPressed is when the button is being pressed
46 ButtonPressed
47 // ButtonDisabled is when the button is disabled
48 ButtonDisabled
49)
50
51// ButtonVariant defines the visual style variant of a button
52type ButtonVariant int
53
54const (
55 // ButtonPrimary uses primary color styling
56 ButtonPrimary ButtonVariant = iota
57 // ButtonSecondary uses secondary color styling
58 ButtonSecondary
59 // ButtonDanger uses danger/error color styling
60 ButtonDanger
61 // ButtonWarning uses warning color styling
62 ButtonWarning
63 // ButtonNeutral uses neutral color styling
64 ButtonNeutral
65)
66
67// ButtonMsg is sent when a button is clicked
68type ButtonMsg struct {
69 ID string
70 Payload any
71}
72
73// ButtonCmp represents a clickable button component
74type ButtonCmp struct {
75 id string
76 label string
77 width int
78 height int
79 state ButtonState
80 variant ButtonVariant
81 keyMap ButtonKeyMap
82 payload any
83 style lipgloss.Style
84 hoverStyle lipgloss.Style
85}
86
87// NewButtonCmp creates a new button component
88func NewButtonCmp(id, label string) *ButtonCmp {
89 b := &ButtonCmp{
90 id: id,
91 label: label,
92 state: ButtonNormal,
93 variant: ButtonPrimary,
94 keyMap: DefaultButtonKeyMap(),
95 width: len(label) + 4, // add some padding
96 height: 1,
97 }
98 b.updateStyles()
99 return b
100}
101
102// WithVariant sets the button variant
103func (b *ButtonCmp) WithVariant(variant ButtonVariant) *ButtonCmp {
104 b.variant = variant
105 b.updateStyles()
106 return b
107}
108
109// WithPayload sets the payload sent with button events
110func (b *ButtonCmp) WithPayload(payload any) *ButtonCmp {
111 b.payload = payload
112 return b
113}
114
115// WithWidth sets a custom width
116func (b *ButtonCmp) WithWidth(width int) *ButtonCmp {
117 b.width = width
118 b.updateStyles()
119 return b
120}
121
122// updateStyles recalculates styles based on current state and variant
123func (b *ButtonCmp) updateStyles() {
124 // Base styles
125 b.style = styles.Regular.
126 Padding(0, 1).
127 Width(b.width).
128 Align(lipgloss.Center).
129 BorderStyle(lipgloss.RoundedBorder())
130
131 b.hoverStyle = b.style.
132 Bold(true)
133
134 // Variant-specific styling
135 switch b.variant {
136 case ButtonPrimary:
137 b.style = b.style.
138 Foreground(styles.Base).
139 Background(styles.Primary).
140 BorderForeground(styles.Primary)
141
142 b.hoverStyle = b.hoverStyle.
143 Foreground(styles.Base).
144 Background(styles.Blue).
145 BorderForeground(styles.Blue)
146
147 case ButtonSecondary:
148 b.style = b.style.
149 Foreground(styles.Base).
150 Background(styles.Secondary).
151 BorderForeground(styles.Secondary)
152
153 b.hoverStyle = b.hoverStyle.
154 Foreground(styles.Base).
155 Background(styles.Mauve).
156 BorderForeground(styles.Mauve)
157
158 case ButtonDanger:
159 b.style = b.style.
160 Foreground(styles.Base).
161 Background(styles.Error).
162 BorderForeground(styles.Error)
163
164 b.hoverStyle = b.hoverStyle.
165 Foreground(styles.Base).
166 Background(styles.Red).
167 BorderForeground(styles.Red)
168
169 case ButtonWarning:
170 b.style = b.style.
171 Foreground(styles.Text).
172 Background(styles.Warning).
173 BorderForeground(styles.Warning)
174
175 b.hoverStyle = b.hoverStyle.
176 Foreground(styles.Text).
177 Background(styles.Peach).
178 BorderForeground(styles.Peach)
179
180 case ButtonNeutral:
181 b.style = b.style.
182 Foreground(styles.Text).
183 Background(styles.Grey).
184 BorderForeground(styles.Grey)
185
186 b.hoverStyle = b.hoverStyle.
187 Foreground(styles.Text).
188 Background(styles.DarkGrey).
189 BorderForeground(styles.DarkGrey)
190 }
191
192 // Disabled style override
193 if b.state == ButtonDisabled {
194 b.style = b.style.
195 Foreground(styles.SubText0).
196 Background(styles.LightGrey).
197 BorderForeground(styles.LightGrey)
198 }
199}
200
201// SetSize sets the button size
202func (b *ButtonCmp) SetSize(width, height int) {
203 b.width = width
204 b.height = height
205 b.updateStyles()
206}
207
208// Focus sets the button to focused state
209func (b *ButtonCmp) Focus() tea.Cmd {
210 if b.state != ButtonDisabled {
211 b.state = ButtonHovered
212 }
213 return nil
214}
215
216// Blur sets the button to normal state
217func (b *ButtonCmp) Blur() tea.Cmd {
218 if b.state != ButtonDisabled {
219 b.state = ButtonNormal
220 }
221 return nil
222}
223
224// Disable sets the button to disabled state
225func (b *ButtonCmp) Disable() {
226 b.state = ButtonDisabled
227 b.updateStyles()
228}
229
230// Enable enables the button if disabled
231func (b *ButtonCmp) Enable() {
232 if b.state == ButtonDisabled {
233 b.state = ButtonNormal
234 b.updateStyles()
235 }
236}
237
238// IsDisabled returns whether the button is disabled
239func (b *ButtonCmp) IsDisabled() bool {
240 return b.state == ButtonDisabled
241}
242
243// IsFocused returns whether the button is focused
244func (b *ButtonCmp) IsFocused() bool {
245 return b.state == ButtonHovered
246}
247
248// Init initializes the button
249func (b *ButtonCmp) Init() tea.Cmd {
250 return nil
251}
252
253// Update handles messages and user input
254func (b *ButtonCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255 // Skip updates if disabled
256 if b.state == ButtonDisabled {
257 return b, nil
258 }
259
260 switch msg := msg.(type) {
261 case tea.KeyMsg:
262 // Handle key presses when focused
263 if b.state == ButtonHovered {
264 switch {
265 case key.Matches(msg, b.keyMap.Enter):
266 b.state = ButtonPressed
267 return b, func() tea.Msg {
268 return ButtonMsg{
269 ID: b.id,
270 Payload: b.payload,
271 }
272 }
273 }
274 }
275 }
276
277 return b, nil
278}
279
280// View renders the button
281func (b *ButtonCmp) View() string {
282 if b.state == ButtonHovered || b.state == ButtonPressed {
283 return b.hoverStyle.Render(b.label)
284 }
285 return b.style.Render(b.label)
286}
287