Skip to content

Commit 5509748

Browse files
committed
Add Location wrapping and quote-aware key=value parsing for context view
- Location field now wraps long paths across multiple lines - Custom and Environment edits now support quoted values: KEY="sentence with spaces", KEY='value', KEY=`value` - Added wrapText helper that breaks on path separators - Added parseKeyValue function with tests
1 parent c2613e4 commit 5509748

File tree

3 files changed

+207
-3
lines changed

3 files changed

+207
-3
lines changed

internal/components/contextview/model.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const (
6060
EditAWSRegion EditField = "aws.region"
6161
EditGitBranch EditField = "git.branch"
6262
EditEnvVar EditField = "env"
63+
EditCustom EditField = "custom"
6364
)
6465

6566
// Model represents the context component state
@@ -214,6 +215,48 @@ func (m Model) handleEditKeys(msg tea.KeyMsg) (Model, tea.Cmd) {
214215
return m, cmd
215216
}
216217

218+
// parseKeyValue parses a KEY=VALUE string where VALUE can be quoted with ", ', or `
219+
// Examples:
220+
// - "foo=bar" -> ("foo", "bar")
221+
// - "foo='hello world'" -> ("foo", "hello world")
222+
// - `foo="my sentence"` -> ("foo", "my sentence")
223+
// - "foo=`backtick quoted`" -> ("foo", "backtick quoted")
224+
func parseKeyValue(input string) (key, value string, ok bool) {
225+
// Find the first = sign
226+
eqIdx := -1
227+
for i, c := range input {
228+
if c == '=' {
229+
eqIdx = i
230+
break
231+
}
232+
}
233+
234+
if eqIdx <= 0 {
235+
return "", "", false
236+
}
237+
238+
key = input[:eqIdx]
239+
remainder := input[eqIdx+1:]
240+
241+
// Check if value is quoted
242+
if len(remainder) >= 2 {
243+
first := remainder[0]
244+
if first == '"' || first == '\'' || first == '`' {
245+
// Find matching closing quote
246+
for i := len(remainder) - 1; i > 0; i-- {
247+
if remainder[i] == first {
248+
value = remainder[1:i]
249+
return key, value, true
250+
}
251+
}
252+
}
253+
}
254+
255+
// Not quoted, use the whole remainder
256+
value = remainder
257+
return key, value, true
258+
}
259+
217260
// saveEdit saves the current edit to the context
218261
func (m *Model) saveEdit() {
219262
if m.current == nil {
@@ -243,6 +286,26 @@ func (m *Model) saveEdit() {
243286
if git := m.current.GetGit(); git != nil {
244287
git.Branch = value
245288
}
289+
case EditEnvVar:
290+
// Parse KEY=VALUE with quote support and merge into existing env
291+
if k, v, ok := parseKeyValue(value); ok {
292+
env := m.current.GetEnv()
293+
if env == nil {
294+
env = make(map[string]string)
295+
}
296+
env[k] = v
297+
m.current.SetEnv(env)
298+
}
299+
case EditCustom:
300+
// Parse KEY=VALUE with quote support and merge into existing custom
301+
if k, v, ok := parseKeyValue(value); ok {
302+
custom := m.current.GetCustom()
303+
if custom == nil {
304+
custom = make(map[string]string)
305+
}
306+
custom[k] = v
307+
m.current.SetCustom(custom)
308+
}
246309
}
247310

248311
// Save context
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package contextview
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestParseKeyValue(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
input string
11+
wantKey string
12+
wantVal string
13+
wantOk bool
14+
}{
15+
{
16+
name: "simple key=value",
17+
input: "foo=bar",
18+
wantKey: "foo",
19+
wantVal: "bar",
20+
wantOk: true,
21+
},
22+
{
23+
name: "double quoted value",
24+
input: `foo="hello world"`,
25+
wantKey: "foo",
26+
wantVal: "hello world",
27+
wantOk: true,
28+
},
29+
{
30+
name: "single quoted value",
31+
input: `foo='hello world'`,
32+
wantKey: "foo",
33+
wantVal: "hello world",
34+
wantOk: true,
35+
},
36+
{
37+
name: "backtick quoted value",
38+
input: "foo=`hello world`",
39+
wantKey: "foo",
40+
wantVal: "hello world",
41+
wantOk: true,
42+
},
43+
{
44+
name: "quoted sentence",
45+
input: `TARGET="production cluster in us-east-1"`,
46+
wantKey: "TARGET",
47+
wantVal: "production cluster in us-east-1",
48+
wantOk: true,
49+
},
50+
{
51+
name: "value with equals",
52+
input: `CONNECT_STRING="user=admin host=localhost"`,
53+
wantKey: "CONNECT_STRING",
54+
wantVal: "user=admin host=localhost",
55+
wantOk: true,
56+
},
57+
{
58+
name: "no equals sign",
59+
input: "justkey",
60+
wantOk: false,
61+
},
62+
{
63+
name: "empty key",
64+
input: "=value",
65+
wantOk: false,
66+
},
67+
{
68+
name: "empty value",
69+
input: "key=",
70+
wantKey: "key",
71+
wantVal: "",
72+
wantOk: true,
73+
},
74+
{
75+
name: "unquoted with spaces",
76+
input: "key=hello world",
77+
wantKey: "key",
78+
wantVal: "hello world",
79+
wantOk: true,
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
key, val, ok := parseKeyValue(tt.input)
86+
if ok != tt.wantOk {
87+
t.Errorf("parseKeyValue(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
88+
}
89+
if ok && key != tt.wantKey {
90+
t.Errorf("parseKeyValue(%q) key = %q, want %q", tt.input, key, tt.wantKey)
91+
}
92+
if ok && val != tt.wantVal {
93+
t.Errorf("parseKeyValue(%q) val = %q, want %q", tt.input, val, tt.wantVal)
94+
}
95+
})
96+
}
97+
}

internal/components/contextview/view.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@ import (
88
"github.com/charmbracelet/lipgloss/table"
99
)
1010

11+
// wrapText wraps text at the given width, breaking on path separators when possible
12+
func wrapText(text string, width int) []string {
13+
if len(text) <= width {
14+
return []string{text}
15+
}
16+
17+
var lines []string
18+
remaining := text
19+
20+
for len(remaining) > width {
21+
// Find a good break point (prefer / for paths)
22+
breakAt := width
23+
for i := width - 1; i > width/2; i-- {
24+
if remaining[i] == '/' {
25+
breakAt = i + 1
26+
break
27+
}
28+
}
29+
lines = append(lines, remaining[:breakAt])
30+
remaining = remaining[breakAt:]
31+
}
32+
if len(remaining) > 0 {
33+
lines = append(lines, remaining)
34+
}
35+
36+
return lines
37+
}
38+
1139
// View renders the context display
1240
func (m Model) View() string {
1341
if m.editMode {
@@ -33,9 +61,18 @@ func (m Model) renderCurrentContext() string {
3361
return sb.String()
3462
}
3563

36-
// Project info
37-
sb.WriteString(m.theme.Selected.Render("📁 Project:") + " ")
38-
sb.WriteString(m.theme.Normal.Render(m.current.ProjectRoot) + "\n\n")
64+
// Project info - wrap long paths across multiple lines
65+
sb.WriteString(m.theme.Selected.Render("📁 Location:") + "\n")
66+
// Wrap the path with a reasonable width
67+
pathWidth := m.width - 4
68+
if pathWidth < 40 {
69+
pathWidth = 40
70+
}
71+
wrappedPath := wrapText(m.current.ProjectRoot, pathWidth)
72+
for _, line := range wrappedPath {
73+
sb.WriteString(" " + m.theme.Normal.Render(line) + "\n")
74+
}
75+
sb.WriteString("\n")
3976

4077
// Build table rows for context details
4178
var rows [][]string
@@ -206,6 +243,13 @@ func (m Model) renderEditView() string {
206243

207244
sb.WriteString(m.theme.Dim.Render("Field: ") + m.theme.Normal.Render(string(m.editField)) + "\n\n")
208245
sb.WriteString(m.editInput.View() + "\n\n")
246+
247+
// Show format hint for key=value fields
248+
if m.editField == EditEnvVar || m.editField == EditCustom {
249+
sb.WriteString(m.theme.Dim.Render("Format: KEY=value or KEY=\"sentence with spaces\"") + "\n")
250+
sb.WriteString(m.theme.Dim.Render(" Quotes: \" ' or ` for multi-word values") + "\n\n")
251+
}
252+
209253
sb.WriteString(m.theme.Dim.Render("Enter:save Esc:cancel"))
210254

211255
return sb.String()

0 commit comments

Comments
 (0)