Skip to content

Commit fee5d5e

Browse files
authored
Merge pull request #53 from DevopsArtFactory/feat/context-filter
feat: add filter to context switcher
2 parents 6d55fcc + 3380aec commit fee5d5e

File tree

4 files changed

+80
-12
lines changed

4 files changed

+80
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Available on: EC2 instances, VPC/Subnets, RDS instances, Route53 zones/records,
155155
| `Enter` | Select |
156156
| `Esc`/`q` | Go back |
157157
| `H` | Go to home (service list) |
158+
| `/` | Filter (instances, IPs, contexts) |
158159
| `C` | Context switcher |
159160
| `q` (on service list) | Quit |
160161

internal/app/app.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ type Model struct {
122122
// Context picker
123123
configPath string
124124
ctxList []config.ContextInfo
125+
filteredCtxList []config.ContextInfo
125126
ctxIdx int
127+
ctxFilterInput string
128+
ctxFilterActive bool
126129
ctxPrevScreen screen
127130
pendingContextName string
128131

@@ -310,8 +313,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
310313

311314
case contextsLoadedMsg:
312315
m.ctxList = msg.contexts
316+
m.filteredCtxList = msg.contexts
313317
m.ctxIdx = 0
314-
for i, ctx := range m.ctxList {
318+
m.ctxFilterInput = ""
319+
m.ctxFilterActive = false
320+
for i, ctx := range m.filteredCtxList {
315321
if ctx.Current {
316322
m.ctxIdx = i
317323
break

internal/app/screen_context.go

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app
22

33
import (
44
"context"
5+
"fmt"
56
"strings"
67

78
tea "github.com/charmbracelet/bubbletea"
@@ -23,7 +24,30 @@ func (m Model) loadContexts() tea.Cmd {
2324
}
2425

2526
func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
26-
switch msg.String() {
27+
key := msg.String()
28+
29+
// If filter is active, handle text input
30+
if m.ctxFilterActive {
31+
switch key {
32+
case "esc":
33+
m.ctxFilterActive = false
34+
case "enter":
35+
m.ctxFilterActive = false
36+
case "backspace":
37+
if len(m.ctxFilterInput) > 0 {
38+
m.ctxFilterInput = m.ctxFilterInput[:len(m.ctxFilterInput)-1]
39+
m.applyCtxFilter()
40+
}
41+
default:
42+
if len(key) == 1 {
43+
m.ctxFilterInput += key
44+
m.applyCtxFilter()
45+
}
46+
}
47+
return m, nil
48+
}
49+
50+
switch key {
2751
case "q":
2852
m.quitting = true
2953
return m, tea.Quit
@@ -32,6 +56,8 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
3256
// If initial launch, quit.
3357
if m.cfg.ContextName != "" {
3458
m.screen = m.ctxPrevScreen
59+
m.ctxFilterInput = ""
60+
m.filteredCtxList = m.ctxList
3561
} else {
3662
m.quitting = true
3763
return m, tea.Quit
@@ -41,12 +67,14 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
4167
m.ctxIdx--
4268
}
4369
case "down", "j":
44-
if m.ctxIdx < len(m.ctxList)-1 {
70+
if m.ctxIdx < len(m.filteredCtxList)-1 {
4571
m.ctxIdx++
4672
}
73+
case "/":
74+
m.ctxFilterActive = true
4775
case "enter":
48-
if len(m.ctxList) > 0 && m.ctxIdx < len(m.ctxList) {
49-
selected := m.ctxList[m.ctxIdx]
76+
if len(m.filteredCtxList) > 0 && m.ctxIdx < len(m.filteredCtxList) {
77+
selected := m.filteredCtxList[m.ctxIdx]
5078
m.pendingContextName = selected.Name
5179
m.screen = screenLoading
5280
return m, m.switchContext(selected.Name)
@@ -63,6 +91,22 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
6391
return m, nil
6492
}
6593

94+
func (m *Model) applyCtxFilter() {
95+
if m.ctxFilterInput == "" {
96+
m.filteredCtxList = m.ctxList
97+
} else {
98+
query := strings.ToLower(m.ctxFilterInput)
99+
var result []config.ContextInfo
100+
for _, ctx := range m.ctxList {
101+
if strings.Contains(ctx.FilterText(), query) {
102+
result = append(result, ctx)
103+
}
104+
}
105+
m.filteredCtxList = result
106+
}
107+
m.ctxIdx = 0
108+
}
109+
66110
func (m Model) switchContext(name string) tea.Cmd {
67111
return func() tea.Msg {
68112
if err := config.SetCurrent(m.configPath, name); err != nil {
@@ -127,17 +171,28 @@ func (m Model) doFinalizeContextSwitch() tea.Cmd {
127171
func (m Model) viewContextPicker() string {
128172
var b strings.Builder
129173
b.WriteString(titleStyle.Render("Select Context"))
174+
b.WriteString("\n")
175+
176+
// Filter bar
177+
if m.ctxFilterActive {
178+
b.WriteString(filterStyle.Render(fmt.Sprintf("Filter: %s▏", m.ctxFilterInput)))
179+
} else if m.ctxFilterInput != "" {
180+
b.WriteString(dimStyle.Render(fmt.Sprintf("Filter: %s", m.ctxFilterInput)))
181+
}
130182
b.WriteString("\n\n")
131183

132184
if len(m.ctxList) == 0 {
133185
b.WriteString(normalStyle.Render(" No contexts defined."))
134186
b.WriteString("\n\n")
135187
b.WriteString(dimStyle.Render(" Press 'a' to add your first context."))
136188
b.WriteString("\n")
189+
} else if len(m.filteredCtxList) == 0 {
190+
b.WriteString(dimStyle.Render(" No matching contexts"))
191+
b.WriteString("\n")
137192
} else {
138193
// Measure max widths for alignment
139194
maxName, maxRegion := 4, 6 // "NAME", "REGION"
140-
for _, ctx := range m.ctxList {
195+
for _, ctx := range m.filteredCtxList {
141196
if len(ctx.Name) > maxName {
142197
maxName = len(ctx.Name)
143198
}
@@ -153,16 +208,16 @@ func (m Model) viewContextPicker() string {
153208
b.WriteString(dimStyle.Render(" " + nameCol.Render("NAME") + regionCol.Render("REGION") + "AUTH"))
154209
b.WriteString("\n")
155210

156-
// overhead: title (1) + blank (1) + table header (1) + blank (1) + footer (1) = 5
157-
visibleLines := max(m.height-5, 3)
211+
// overhead: title (1) + filter (1) + blank (1) + table header (1) + blank (1) + footer (1) = 6
212+
visibleLines := max(m.height-6, 3)
158213
start := 0
159214
if m.ctxIdx >= visibleLines {
160215
start = m.ctxIdx - visibleLines + 1
161216
}
162-
end := min(start+visibleLines, len(m.ctxList))
217+
end := min(start+visibleLines, len(m.filteredCtxList))
163218

164219
for i := start; i < end; i++ {
165-
ctx := m.ctxList[i]
220+
ctx := m.filteredCtxList[i]
166221
cursor := " "
167222
style := normalStyle
168223
if i == m.ctxIdx {
@@ -181,9 +236,9 @@ func (m Model) viewContextPicker() string {
181236

182237
b.WriteString("\n")
183238
if m.cfg.ContextName != "" {
184-
b.WriteString(dimStyle.Render("↑/↓: navigate • enter: select • a: add • esc: back • q: quit"))
239+
b.WriteString(dimStyle.Render("↑/↓: navigate • /: filter • enter: select • a: add • esc: back • q: quit"))
185240
} else {
186-
b.WriteString(dimStyle.Render("↑/↓: navigate • enter: select • a: add • q: quit"))
241+
b.WriteString(dimStyle.Render("↑/↓: navigate • /: filter • enter: select • a: add • q: quit"))
187242
}
188243
return b.String()
189244
}

internal/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
"gopkg.in/yaml.v3"
910

@@ -82,6 +83,11 @@ type ContextInfo struct {
8283
Current bool
8384
}
8485

86+
// FilterText returns a lowercase string for keyword matching.
87+
func (c ContextInfo) FilterText() string {
88+
return strings.ToLower(fmt.Sprintf("%s %s %s", c.Name, c.Profile, c.Region))
89+
}
90+
8591
// Load resolves config with priority: CLI flags > context > config file defaults > hardcoded defaults.
8692
func Load(cliProfile, cliRegion *string, configPath string) (*Config, error) {
8793
var fc fileConfig

0 commit comments

Comments
 (0)