Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ contexts:
| `Enter` | Select |
| `Esc`/`q` | Go back |
| `H` | Go to home (service list) |
| `/` | Filter (instances, IPs) |
| `/` | Filter (instances, IPs, contexts) |
| `C` | Context switcher |
| `s`/`x`/`f` | Start/Stop/Failover (RDS detail) |
| `q` (on service list) | Quit |
Expand Down
8 changes: 7 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ type Model struct {
// Context picker
configPath string
ctxList []config.ContextInfo
filteredCtxList []config.ContextInfo
ctxIdx int
ctxFilterInput string
ctxFilterActive bool
ctxPrevScreen screen
pendingContextName string

Expand Down Expand Up @@ -293,8 +296,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

case contextsLoadedMsg:
m.ctxList = msg.contexts
m.filteredCtxList = msg.contexts
m.ctxIdx = 0
for i, ctx := range m.ctxList {
m.ctxFilterInput = ""
m.ctxFilterActive = false
for i, ctx := range m.filteredCtxList {
if ctx.Current {
m.ctxIdx = i
break
Expand Down
77 changes: 66 additions & 11 deletions internal/app/screen_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"context"
"fmt"
"strings"

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

func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
key := msg.String()

// If filter is active, handle text input
if m.ctxFilterActive {
switch key {
case "esc":
m.ctxFilterActive = false
case "enter":
m.ctxFilterActive = false
case "backspace":
if len(m.ctxFilterInput) > 0 {
m.ctxFilterInput = m.ctxFilterInput[:len(m.ctxFilterInput)-1]
m.applyCtxFilter()
}
default:
if len(key) == 1 {
m.ctxFilterInput += key
m.applyCtxFilter()
}
}
return m, nil
}

switch key {
case "q":
m.quitting = true
return m, tea.Quit
Expand All @@ -32,6 +56,8 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// If initial launch, quit.
if m.cfg.ContextName != "" {
m.screen = m.ctxPrevScreen
m.ctxFilterInput = ""
m.filteredCtxList = m.ctxList
} else {
m.quitting = true
return m, tea.Quit
Expand All @@ -41,12 +67,14 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.ctxIdx--
}
case "down", "j":
if m.ctxIdx < len(m.ctxList)-1 {
if m.ctxIdx < len(m.filteredCtxList)-1 {
m.ctxIdx++
}
case "/":
m.ctxFilterActive = true
case "enter":
if len(m.ctxList) > 0 && m.ctxIdx < len(m.ctxList) {
selected := m.ctxList[m.ctxIdx]
if len(m.filteredCtxList) > 0 && m.ctxIdx < len(m.filteredCtxList) {
selected := m.filteredCtxList[m.ctxIdx]
m.pendingContextName = selected.Name
m.screen = screenLoading
return m, m.switchContext(selected.Name)
Expand All @@ -63,6 +91,22 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}

func (m *Model) applyCtxFilter() {
if m.ctxFilterInput == "" {
m.filteredCtxList = m.ctxList
} else {
query := strings.ToLower(m.ctxFilterInput)
var result []config.ContextInfo
for _, ctx := range m.ctxList {
if strings.Contains(ctx.FilterText(), query) {
result = append(result, ctx)
}
}
m.filteredCtxList = result
}
m.ctxIdx = 0
}

func (m Model) switchContext(name string) tea.Cmd {
return func() tea.Msg {
if err := config.SetCurrent(m.configPath, name); err != nil {
Expand Down Expand Up @@ -127,17 +171,28 @@ func (m Model) doFinalizeContextSwitch() tea.Cmd {
func (m Model) viewContextPicker() string {
var b strings.Builder
b.WriteString(titleStyle.Render("Select Context"))
b.WriteString("\n")

// Filter bar
if m.ctxFilterActive {
b.WriteString(filterStyle.Render(fmt.Sprintf("Filter: %s▏", m.ctxFilterInput)))
} else if m.ctxFilterInput != "" {
b.WriteString(dimStyle.Render(fmt.Sprintf("Filter: %s", m.ctxFilterInput)))
}
b.WriteString("\n\n")

if len(m.ctxList) == 0 {
b.WriteString(normalStyle.Render(" No contexts defined."))
b.WriteString("\n\n")
b.WriteString(dimStyle.Render(" Press 'a' to add your first context."))
b.WriteString("\n")
} else if len(m.filteredCtxList) == 0 {
b.WriteString(dimStyle.Render(" No matching contexts"))
b.WriteString("\n")
} else {
// Measure max widths for alignment
maxName, maxRegion := 4, 6 // "NAME", "REGION"
for _, ctx := range m.ctxList {
for _, ctx := range m.filteredCtxList {
if len(ctx.Name) > maxName {
maxName = len(ctx.Name)
}
Expand All @@ -153,16 +208,16 @@ func (m Model) viewContextPicker() string {
b.WriteString(dimStyle.Render(" " + nameCol.Render("NAME") + regionCol.Render("REGION") + "AUTH"))
b.WriteString("\n")

// overhead: title (1) + blank (1) + table header (1) + blank (1) + footer (1) = 5
visibleLines := max(m.height-5, 3)
// overhead: title (1) + filter (1) + blank (1) + table header (1) + blank (1) + footer (1) = 6
visibleLines := max(m.height-6, 3)
start := 0
if m.ctxIdx >= visibleLines {
start = m.ctxIdx - visibleLines + 1
}
end := min(start+visibleLines, len(m.ctxList))
end := min(start+visibleLines, len(m.filteredCtxList))

for i := start; i < end; i++ {
ctx := m.ctxList[i]
ctx := m.filteredCtxList[i]
cursor := " "
style := normalStyle
if i == m.ctxIdx {
Expand All @@ -181,9 +236,9 @@ func (m Model) viewContextPicker() string {

b.WriteString("\n")
if m.cfg.ContextName != "" {
b.WriteString(dimStyle.Render("↑/↓: navigate • enter: select • a: add • esc: back • q: quit"))
b.WriteString(dimStyle.Render("↑/↓: navigate • /: filter • enter: select • a: add • esc: back • q: quit"))
} else {
b.WriteString(dimStyle.Render("↑/↓: navigate • enter: select • a: add • q: quit"))
b.WriteString(dimStyle.Render("↑/↓: navigate • /: filter • enter: select • a: add • q: quit"))
}
return b.String()
}
6 changes: 6 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -80,6 +81,11 @@ type ContextInfo struct {
Current bool
}

// FilterText returns a lowercase string for keyword matching.
func (c ContextInfo) FilterText() string {
return strings.ToLower(fmt.Sprintf("%s %s %s", c.Name, c.Profile, c.Region))
}

// Load resolves config with priority: CLI flags > context > config file defaults > hardcoded defaults.
func Load(cliProfile, cliRegion *string, configPath string) (*Config, error) {
var fc fileConfig
Expand Down
Loading