Skip to content
Merged
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
75 changes: 65 additions & 10 deletions internal/app/context_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,56 @@ func newContextTable() table.Model {
}

func contextTableSelectedStyle() lipgloss.Style {
base := selectedStyle
return base.
return lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("255")).
Background(lipgloss.Color("57"))
Foreground(lipgloss.Color("16")).
Background(lipgloss.Color("220"))
}

func (m *Model) syncContextTable() {
m.contextTable.SetColumns(contextTableColumns(m.width))
m.contextTable.SetWidth(contextTableWidth(m.width))
m.contextTable.SetHeight(contextTableHeight(m.height))
m.contextTable.SetHeight(m.contextPickerTableHeight())
m.contextTable.SetRows(contextTableRows(m.filteredCtxList))
m.contextTable.Focus()
m.contextTable.SetCursor(m.ctxIdx)
m.setContextTableCursor(m.ctxIdx)
if cursor := m.contextTable.Cursor(); cursor >= 0 {
m.ctxIdx = cursor
}
}

func (m *Model) setContextTableCursor(index int) {
m.ctxIdx = clampListIndex(index, len(m.filteredCtxList))
m.contextTable.GotoTop()
if m.ctxIdx > 0 {
m.contextTable.MoveDown(m.ctxIdx)
}
}

func (m *Model) moveContextTableUp() {
if len(m.filteredCtxList) == 0 {
return
}
if m.ctxIdx <= 0 {
m.setContextTableCursor(len(m.filteredCtxList) - 1)
return
}
m.contextTable.MoveUp(1)
m.ctxIdx = m.contextTable.Cursor()
}

func (m *Model) moveContextTableDown() {
if len(m.filteredCtxList) == 0 {
return
}
if m.ctxIdx >= len(m.filteredCtxList)-1 {
m.setContextTableCursor(0)
return
}
m.contextTable.MoveDown(1)
m.ctxIdx = m.contextTable.Cursor()
}

func contextTableRows(contexts []config.ContextInfo) []table.Row {
rows := make([]table.Row, 0, len(contexts))
for _, ctx := range contexts {
Expand Down Expand Up @@ -101,10 +132,34 @@ func contextTableHeight(terminalHeight int) int {
if terminalHeight <= 0 {
return defaultContextTableHeight
}
// Context picker layout overhead:
// title/current/env/filter block (6) + panel border (2) + separator/help bar (2) = 10.
// The table height itself must fit inside the remaining rows.
return max(terminalHeight-10, 3)
return max(terminalHeight-10, 2)
}

func (m Model) contextPickerTableHeight() int {
if m.contextPickerCompact() {
height := max(m.height-6, 2)
if m.contextPickerFilterVisible() {
height -= 2
}
return max(height, 2)
}

height := contextTableHeight(m.height)
if m.contextPickerFilterVisible() {
height -= 2
}
return max(height, 2)
}
Comment on lines +135 to +152
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the required minimum visible window size in table height calculations.

Line 135 and Lines 138-152 use a minimum of 2, but this TUI windowing should clamp to 5 visible lines using max(m.height-N, 5).

💡 Suggested adjustment
 func contextTableHeight(terminalHeight int) int {
 	if terminalHeight <= 0 {
 		return defaultContextTableHeight
 	}
-	return max(terminalHeight-10, 2)
+	return max(terminalHeight-10, 5)
 }
 
 func (m Model) contextPickerTableHeight() int {
-	if m.contextPickerCompact() {
-		height := max(m.height-6, 2)
-		if m.contextPickerFilterVisible() {
-			height -= 2
-		}
-		return max(height, 2)
-	}
-
-	height := contextTableHeight(m.height)
-	if m.contextPickerFilterVisible() {
-		height -= 2
-	}
-	return max(height, 2)
+	offset := 10
+	if m.contextPickerCompact() {
+		offset = 6
+	}
+	if m.contextPickerFilterVisible() {
+		offset += 2
+	}
+	return max(m.height-offset, 5)
 }

As per coding guidelines, Implement scroll windowing with formula: visibleLines := max(m.height-N, 5) in Go TUI implementation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return max(terminalHeight-10, 2)
}
func (m Model) contextPickerTableHeight() int {
if m.contextPickerCompact() {
height := max(m.height-6, 2)
if m.contextPickerFilterVisible() {
height -= 2
}
return max(height, 2)
}
height := contextTableHeight(m.height)
if m.contextPickerFilterVisible() {
height -= 2
}
return max(height, 2)
}
return max(terminalHeight-10, 5)
}
func (m Model) contextPickerTableHeight() int {
offset := 10
if m.contextPickerCompact() {
offset = 6
}
if m.contextPickerFilterVisible() {
offset += 2
}
return max(m.height-offset, 5)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/app/context_table.go` around lines 135 - 152, The table height
calculations currently clamp to a minimum of 2 visible lines; update them to use
the required minimum of 5 visible lines by replacing all occurrences of max(...,
2) with max(..., 5) in the contextPickerTableHeight function and any helper used
(e.g., contextTableHeight), and ensure the intermediate computed height
adjustments (the -6 and -2 offsets and the compact branch using max(m.height-6,
...)) still call max(..., 5) so visibleLines := max(m.height - N, 5) is
enforced; update references to contextPickerCompact() and
contextPickerFilterVisible() logic accordingly so final returned value never
drops below 5.


func (m Model) contextPickerCompact() bool {
return m.height > 0 && m.height <= 14
}

func (m Model) contextPickerFilterVisible() bool {
if m.isFiltering(filterContexts) && m.renderFilterValue(filterContexts) != "" {
return true
}
return false
}

func contextTableColumns(terminalWidth int) []table.Column {
Expand Down
29 changes: 20 additions & 9 deletions internal/app/screen_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,9 @@ func (m Model) updateContextPicker(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "/":
return m, m.activateFilter(filterContexts)
case "up", "k":
m.ctxIdx = previousListIndex(m.ctxIdx, len(m.filteredCtxList))
m.contextTable.SetCursor(m.ctxIdx)
m.moveContextTableUp()
case "down", "j":
m.ctxIdx = nextListIndex(m.ctxIdx, len(m.filteredCtxList))
m.contextTable.SetCursor(m.ctxIdx)
m.moveContextTableDown()
case "enter":
cursor := m.contextTable.Cursor()
if len(m.filteredCtxList) > 0 && cursor >= 0 && cursor < len(m.filteredCtxList) {
Expand Down Expand Up @@ -265,12 +263,17 @@ func (m Model) doFinalizeContextSwitch() tea.Cmd {
func (m Model) viewContextPicker() string {
var b strings.Builder
var panel strings.Builder
compact := m.contextPickerCompact()
b.WriteString(titleStyle.Render("Select Context"))
b.WriteString("\n")
b.WriteString(renderDetailLine("UNIC current", displayContextName(m.cfg.ContextName)))
b.WriteString("\n")
b.WriteString(renderDetailLine("Shell env", m.displayShellEnvContext()))
b.WriteString("\n\n")
if compact {
b.WriteString("\n")
} else {
b.WriteString(renderDetailLine("UNIC current", displayContextName(m.cfg.ContextName)))
b.WriteString("\n")
b.WriteString(renderDetailLine("Shell env", m.displayShellEnvContext()))
b.WriteString("\n\n")
}

if filter := m.renderFilterValue(filterContexts); filter != "" {
b.WriteString(filter)
Expand All @@ -293,7 +296,15 @@ func (m Model) viewContextPicker() string {
b.WriteString(m.renderListPanel(panel.String()))
b.WriteString("\n\n")
if m.isFiltering(filterContexts) {
b.WriteString(m.renderHelpBar("filtering: type search • ↑/↓ choose • enter finish • esc clear • ctrl+y copy env • ctrl+s setup"))
if compact {
b.WriteString(m.renderHelpBar("filter • ↑/↓ choose • enter finish • esc clear"))
} else {
b.WriteString(m.renderHelpBar("filtering: type search • ↑/↓ choose • enter finish • esc clear • ctrl+y copy env • ctrl+s setup"))
}
return b.String()
}
if compact {
b.WriteString(m.renderHelpBar("↑/↓: navigate • enter: switch • /: filter • a: add • q: quit"))
return b.String()
}
if m.cfg.ContextName != "" {
Expand Down
95 changes: 95 additions & 0 deletions internal/app/screen_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ func testContexts() []config.ContextInfo {
}
}

func manyTestContexts(total int) []config.ContextInfo {
contexts := make([]config.ContextInfo, 0, total)
for i := 0; i < total; i++ {
contexts = append(contexts, config.ContextInfo{
Name: fmt.Sprintf("ctx-%02d", i),
Region: "us-east-1",
AuthType: "credential",
})
}
return contexts
}

func writeContextConfig(t *testing.T, contents string) string {
t.Helper()
dir := t.TempDir()
Expand Down Expand Up @@ -115,6 +127,89 @@ func TestContextPickerNavigationWraps(t *testing.T) {
}
}

func TestContextPickerKeepsSelectedTableRowVisibleInSmallWindow(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
m.height = 10

updated, _ := m.Update(contextsLoadedMsg{contexts: manyTestContexts(12)})
model := updated.(Model)
for i := 0; i < 8; i++ {
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
model = updated.(Model)
}

view := stripANSI(model.View())
lines := strings.Split(view, "\n")
if len(lines) != model.height {
t.Fatalf("expected fitted view height %d, got %d lines", model.height, len(lines))
}
if !strings.Contains(view, "ctx-08") {
t.Fatalf("expected compact context picker to keep selected row visible, got %q", view)
}
if strings.Contains(view, "...") {
t.Fatalf("expected compact context picker to fit without middle truncation, got %q", view)
}
}

func TestContextPickerMoveUpFromBottomVisibleRowDoesNotScroll(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
m.height = 10

updated, _ := m.Update(contextsLoadedMsg{contexts: manyTestContexts(12)})
model := updated.(Model)
for i := 0; i < 8; i++ {
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
model = updated.(Model)
}
before := stripANSI(model.View())
if !strings.Contains(before, "ctx-08") {
t.Fatalf("expected ctx-08 visible before moving up, got %q", before)
}

updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}})
model = updated.(Model)
after := stripANSI(model.View())
if model.ctxIdx != 7 {
t.Fatalf("expected selected index 7 after moving up once, got %d", model.ctxIdx)
}
if !strings.Contains(after, "ctx-08") {
t.Fatalf("expected bottom visible row to remain visible after moving up once, got %q", after)
}
}

func TestContextPickerKeepsFilteredSelectedTableRowVisibleInSmallWindow(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
m.height = 10

updated, _ := m.Update(contextsLoadedMsg{contexts: manyTestContexts(12)})
model := updated.(Model)
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
model = updated.(Model)
for _, ch := range []rune("ctx") {
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
model = updated.(Model)
}
for i := 0; i < 8; i++ {
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
model = updated.(Model)
}

view := stripANSI(model.View())
lines := strings.Split(view, "\n")
if len(lines) != model.height {
t.Fatalf("expected fitted view height %d, got %d lines", model.height, len(lines))
}
if !strings.Contains(view, "ctx-08") {
t.Fatalf("expected filtered compact context picker to keep selected row visible, got %q", view)
}
if strings.Contains(view, "...") {
t.Fatalf("expected filtered compact context picker to fit without middle truncation, got %q", view)
}
}

func TestContextPickerFilterUpdatesTableRows(t *testing.T) {
m := New(testConfig(), "", "dev")
m.width = 80
Expand Down
Loading
Loading