diff --git a/internal/tui/ui.go b/internal/tui/ui.go index 6f161ffe..be3e738c 100644 --- a/internal/tui/ui.go +++ b/internal/tui/ui.go @@ -237,40 +237,50 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case key.Matches(msg, m.keys.Down): - prevRow := currSection.CurrRow() - nextRow := currSection.NextRow() - if prevRow != nextRow && nextRow == currSection.NumRows()-1 && - m.ctx.View != config.RepoView { - cmds = append(cmds, currSection.FetchNextPageSectionRows()...) + if currSection != nil { + prevRow := currSection.CurrRow() + nextRow := currSection.NextRow() + if prevRow != nextRow && nextRow == currSection.NumRows()-1 && + m.ctx.View != config.RepoView { + cmds = append(cmds, currSection.FetchNextPageSectionRows()...) + } + cmd = m.onViewedRowChanged() } - cmd = m.onViewedRowChanged() case key.Matches(msg, m.keys.Up): - currSection.PrevRow() - cmd = m.onViewedRowChanged() + if currSection != nil { + currSection.PrevRow() + cmd = m.onViewedRowChanged() + } case key.Matches(msg, m.keys.FirstLine): - currSection.FirstItem() - cmd = m.onViewedRowChanged() + if currSection != nil { + currSection.FirstItem() + cmd = m.onViewedRowChanged() + } case key.Matches(msg, m.keys.LastLine): - if currSection.CurrRow()+1 < currSection.NumRows() { - cmds = append(cmds, currSection.FetchNextPageSectionRows()...) + if currSection != nil { + if currSection.CurrRow()+1 < currSection.NumRows() { + cmds = append(cmds, currSection.FetchNextPageSectionRows()...) + } + currSection.LastItem() + cmd = m.onViewedRowChanged() } - currSection.LastItem() - cmd = m.onViewedRowChanged() case key.Matches(msg, m.keys.TogglePreview): m.sidebar.IsOpen = !m.sidebar.IsOpen m.syncMainContentWidth() case key.Matches(msg, m.keys.Refresh): - data.ClearEnrichmentCache() - currSection.ResetFilters() - currSection.ResetRows() - m.syncSidebar() - currSection.SetIsLoading(true) - cmds = append(cmds, currSection.FetchNextPageSectionRows()...) + if currSection != nil { + data.ClearEnrichmentCache() + currSection.ResetFilters() + currSection.ResetRows() + m.syncSidebar() + currSection.SetIsLoading(true) + cmds = append(cmds, currSection.FetchNextPageSectionRows()...) + } case key.Matches(msg, m.keys.RefreshAll): data.ClearEnrichmentCache() diff --git a/internal/tui/ui_test.go b/internal/tui/ui_test.go index d8dd21fc..e7201dfa 100644 --- a/internal/tui/ui_test.go +++ b/internal/tui/ui_test.go @@ -646,6 +646,54 @@ func TestNotificationView_BackKeyClearsIssueSubject(t *testing.T) { ) } +func TestNavigationKeysWithNilSection(t *testing.T) { + cfg, err := config.ParseConfig(config.Location{ + ConfigFlag: "../config/testdata/test-config.yml", + SkipGlobalConfig: true, + }) + require.NoError(t, err) + + ctx := &context.ProgramContext{ + Config: &cfg, + View: config.IssuesView, + } + ctx.Theme = theme.ParseTheme(ctx.Config) + ctx.Styles = context.InitStyles(ctx.Theme) + + sidebarModel := sidebar.NewModel() + sidebarModel.UpdateProgramContext(ctx) + + m := Model{ + ctx: ctx, + keys: keys.Keys, + footer: footer.NewModel(ctx), + prView: prview.NewModel(ctx), + issueSidebar: issueview.NewModel(ctx), + sidebar: sidebarModel, + tabs: tabs.NewModel(ctx), + } + // No sections added — currSection will be nil + + navKeys := []struct { + name string + msg tea.KeyPressMsg + }{ + {"down", tea.KeyPressMsg{Text: "j"}}, + {"up", tea.KeyPressMsg{Text: "k"}}, + {"firstLine", tea.KeyPressMsg{Text: "g"}}, + {"lastLine", tea.KeyPressMsg{Text: "G"}}, + {"refresh", tea.KeyPressMsg{Text: "r"}}, + } + + for _, tc := range navKeys { + t.Run(tc.name, func(t *testing.T) { + require.NotPanics(t, func() { + m.Update(tc.msg) + }, "pressing %s with nil section should not panic", tc.name) + }) + } +} + // executeCommandTemplate mimics the template execution logic from runCustomCommand // to allow testing template variable substitution without executing shell commands. func executeCommandTemplate(