Skip to content

Commit 2cf292b

Browse files
committed
feat(tui): implement headers and query params editors in req builder
1 parent 7da2794 commit 2cf292b

3 files changed

Lines changed: 329 additions & 35 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
package components
2+
3+
import (
4+
tea "github.com/charmbracelet/bubbletea"
5+
"github.com/charmbracelet/lipgloss"
6+
"github.com/maniac-en/req/internal/tui/styles"
7+
)
8+
9+
type KeyValuePair struct {
10+
Key string
11+
Value string
12+
Enabled bool
13+
}
14+
15+
type KeyValueEditor struct {
16+
label string
17+
pairs []KeyValuePair
18+
width int
19+
height int
20+
focused bool
21+
focusIndex int // Which pair is focused
22+
fieldIndex int // 0=key, 1=value, 2=enabled
23+
}
24+
25+
func NewKeyValueEditor(label string) KeyValueEditor {
26+
return KeyValueEditor{
27+
label: label,
28+
pairs: []KeyValuePair{{"", "", true}}, // Start with one empty pair
29+
width: 50,
30+
height: 6,
31+
focused: false,
32+
focusIndex: 0,
33+
fieldIndex: 0,
34+
}
35+
}
36+
37+
func (kv *KeyValueEditor) SetSize(width, height int) {
38+
kv.width = width
39+
kv.height = height
40+
}
41+
42+
func (kv *KeyValueEditor) Focus() {
43+
kv.focused = true
44+
}
45+
46+
func (kv *KeyValueEditor) Blur() {
47+
kv.focused = false
48+
}
49+
50+
func (kv KeyValueEditor) Focused() bool {
51+
return kv.focused
52+
}
53+
54+
func (kv *KeyValueEditor) SetPairs(pairs []KeyValuePair) {
55+
if len(pairs) == 0 {
56+
kv.pairs = []KeyValuePair{{"", "", true}}
57+
} else {
58+
kv.pairs = pairs
59+
}
60+
// Ensure focus is within bounds
61+
if kv.focusIndex >= len(kv.pairs) {
62+
kv.focusIndex = len(kv.pairs) - 1
63+
}
64+
}
65+
66+
func (kv KeyValueEditor) GetPairs() []KeyValuePair {
67+
return kv.pairs
68+
}
69+
70+
func (kv KeyValueEditor) GetEnabledPairsAsMap() map[string]string {
71+
result := make(map[string]string)
72+
for _, pair := range kv.pairs {
73+
if pair.Enabled && pair.Key != "" {
74+
result[pair.Key] = pair.Value
75+
}
76+
}
77+
return result
78+
}
79+
80+
func (kv KeyValueEditor) Update(msg tea.Msg) (KeyValueEditor, tea.Cmd) {
81+
if !kv.focused {
82+
return kv, nil
83+
}
84+
85+
switch msg := msg.(type) {
86+
case tea.KeyMsg:
87+
switch msg.String() {
88+
case "tab":
89+
// Move to next field
90+
kv.fieldIndex++
91+
if kv.fieldIndex > 2 { // key, value, enabled
92+
kv.fieldIndex = 0
93+
kv.focusIndex++
94+
if kv.focusIndex >= len(kv.pairs) {
95+
kv.focusIndex = 0
96+
}
97+
}
98+
case "shift+tab":
99+
// Move to previous field
100+
kv.fieldIndex--
101+
if kv.fieldIndex < 0 {
102+
kv.fieldIndex = 2
103+
kv.focusIndex--
104+
if kv.focusIndex < 0 {
105+
kv.focusIndex = len(kv.pairs) - 1
106+
}
107+
}
108+
case "up":
109+
if kv.focusIndex > 0 {
110+
kv.focusIndex--
111+
}
112+
case "down":
113+
if kv.focusIndex < len(kv.pairs)-1 {
114+
kv.focusIndex++
115+
}
116+
case "ctrl+n":
117+
// Add new pair
118+
kv.pairs = append(kv.pairs, KeyValuePair{"", "", true})
119+
case "ctrl+d":
120+
// Delete current pair (but keep at least one)
121+
if len(kv.pairs) > 1 {
122+
kv.pairs = append(kv.pairs[:kv.focusIndex], kv.pairs[kv.focusIndex+1:]...)
123+
if kv.focusIndex >= len(kv.pairs) {
124+
kv.focusIndex = len(kv.pairs) - 1
125+
}
126+
}
127+
case " ":
128+
// Toggle enabled state when on enabled field
129+
if kv.fieldIndex == 2 {
130+
kv.pairs[kv.focusIndex].Enabled = !kv.pairs[kv.focusIndex].Enabled
131+
}
132+
case "backspace":
133+
// Delete character from current field
134+
if kv.fieldIndex == 0 && len(kv.pairs[kv.focusIndex].Key) > 0 {
135+
kv.pairs[kv.focusIndex].Key = kv.pairs[kv.focusIndex].Key[:len(kv.pairs[kv.focusIndex].Key)-1]
136+
} else if kv.fieldIndex == 1 && len(kv.pairs[kv.focusIndex].Value) > 0 {
137+
kv.pairs[kv.focusIndex].Value = kv.pairs[kv.focusIndex].Value[:len(kv.pairs[kv.focusIndex].Value)-1]
138+
}
139+
default:
140+
// Add printable characters
141+
if len(msg.String()) == 1 && msg.String() >= " " {
142+
char := msg.String()
143+
if kv.fieldIndex == 0 {
144+
kv.pairs[kv.focusIndex].Key += char
145+
} else if kv.fieldIndex == 1 {
146+
kv.pairs[kv.focusIndex].Value += char
147+
}
148+
}
149+
}
150+
}
151+
152+
return kv, nil
153+
}
154+
155+
func (kv KeyValueEditor) View() string {
156+
// Calculate container dimensions (use full width like textarea)
157+
containerWidth := kv.width - 4 // Just account for padding
158+
if containerWidth < 30 {
159+
containerWidth = 30
160+
}
161+
162+
container := styles.ListItemStyle.Copy().
163+
Width(containerWidth).
164+
Height(kv.height).
165+
Border(lipgloss.RoundedBorder()).
166+
BorderForeground(styles.Secondary).
167+
Padding(1, 1)
168+
169+
if kv.focused {
170+
container = container.BorderForeground(styles.Primary)
171+
}
172+
173+
// Build content
174+
var lines []string
175+
visibleHeight := kv.height - 2 // Account for border
176+
177+
// Header - better column proportions
178+
headerStyle := styles.ListItemStyle.Copy().Bold(true)
179+
availableWidth := containerWidth - 8 // Account for padding and separators
180+
keyWidth := availableWidth * 40 / 100 // 40% for key
181+
valueWidth := availableWidth * 50 / 100 // 50% for value
182+
enabledWidth := availableWidth * 10 / 100 // 10% for enabled
183+
184+
header := lipgloss.JoinHorizontal(
185+
lipgloss.Top,
186+
headerStyle.Copy().Width(keyWidth).Render("Key"),
187+
" ",
188+
headerStyle.Copy().Width(valueWidth).Render("Value"),
189+
" ",
190+
headerStyle.Copy().Width(enabledWidth).Align(lipgloss.Center).Render("On"),
191+
)
192+
lines = append(lines, header)
193+
194+
// Show pairs (limit to visible height)
195+
maxPairs := visibleHeight - 2 // Reserve space for header and instructions
196+
if maxPairs < 1 {
197+
maxPairs = 1
198+
}
199+
200+
for i := 0; i < maxPairs && i < len(kv.pairs); i++ {
201+
pair := kv.pairs[i]
202+
203+
// Style fields based on focus
204+
keyStyle := styles.ListItemStyle.Copy().Width(keyWidth)
205+
valueStyle := styles.ListItemStyle.Copy().Width(valueWidth)
206+
enabledStyle := styles.ListItemStyle.Copy().Width(enabledWidth).Align(lipgloss.Center)
207+
208+
if kv.focused && i == kv.focusIndex {
209+
if kv.fieldIndex == 0 {
210+
keyStyle = keyStyle.Background(styles.Primary).Foreground(styles.TextPrimary)
211+
} else if kv.fieldIndex == 1 {
212+
valueStyle = valueStyle.Background(styles.Primary).Foreground(styles.TextPrimary)
213+
} else if kv.fieldIndex == 2 {
214+
enabledStyle = enabledStyle.Background(styles.Primary).Foreground(styles.TextPrimary)
215+
}
216+
}
217+
218+
// Truncate long text
219+
keyText := pair.Key
220+
if len(keyText) > keyWidth-2 {
221+
keyText = keyText[:keyWidth-2]
222+
}
223+
valueText := pair.Value
224+
if len(valueText) > valueWidth-2 {
225+
valueText = valueText[:valueWidth-2]
226+
}
227+
228+
checkbox := "☐"
229+
if pair.Enabled {
230+
checkbox = "☑"
231+
}
232+
233+
row := lipgloss.JoinHorizontal(
234+
lipgloss.Top,
235+
keyStyle.Render(keyText),
236+
" ",
237+
valueStyle.Render(valueText),
238+
" ",
239+
enabledStyle.Render(checkbox),
240+
)
241+
lines = append(lines, row)
242+
}
243+
244+
// Add instructions at bottom
245+
if len(lines) < visibleHeight-1 {
246+
instructions := "tab: next field • ↑↓: navigate rows • space: toggle"
247+
instrStyle := styles.ListItemStyle.Copy().Foreground(styles.TextMuted)
248+
lines = append(lines, "", instrStyle.Render(instructions))
249+
}
250+
251+
// Fill remaining space
252+
for len(lines) < visibleHeight {
253+
lines = append(lines, "")
254+
}
255+
256+
content := lipgloss.JoinVertical(lipgloss.Left, lines...)
257+
containerView := container.Render(content)
258+
259+
return containerView
260+
}

internal/tui/components/textarea.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,8 @@ func (t Textarea) Update(msg tea.Msg) (Textarea, tea.Cmd) {
215215
}
216216

217217
func (t Textarea) View() string {
218-
labelStyle := styles.TitleStyle.Copy().
219-
Width(12).
220-
Align(lipgloss.Right)
221-
222-
// Use helper method for consistent width calculation
223-
containerWidth := t.width - 12 - 1 - 2
218+
// Use full width since we don't need label space
219+
containerWidth := t.width - 4 // Just account for padding
224220
if containerWidth < 20 {
225221
containerWidth = 20
226222
}
@@ -267,17 +263,12 @@ func (t Textarea) View() string {
267263
content := strings.Join(visibleLines, "\n")
268264
textareaView := container.Render(content)
269265

270-
return lipgloss.JoinHorizontal(
271-
lipgloss.Top,
272-
labelStyle.Render(t.label+":"),
273-
" ",
274-
textareaView,
275-
)
266+
return textareaView
276267
}
277268

278269
func (t Textarea) getContentWidth() int {
279-
// Calculate content width (accounting for label, colon, spacing, border, padding)
280-
containerWidth := t.width - 12 - 1 - 2 // label + colon + spacing
270+
// Calculate content width (no label needed)
271+
containerWidth := t.width - 4 // Just account for padding
281272
if containerWidth < 20 {
282273
containerWidth = 20
283274
}

0 commit comments

Comments
 (0)