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+ }
0 commit comments