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
137 changes: 137 additions & 0 deletions internal/commands/navigation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package commands

import (
"testing"
"time"

"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"

"github.com/guyfedwards/nom/v2/internal/config"
"github.com/guyfedwards/nom/v2/internal/store"
)

// navModel returns a model over three articles, set up the way Render does.
func navModel(t *testing.T, titles ...string) model {
t.Helper()

s, err := store.NewInMemorySQLiteStore()
if err != nil {
t.Fatal(err)
}
for _, title := range titles {
if err := s.UpsertItem(&store.Item{Title: title, FeedURL: "https://example.com/feed", Link: "https://example.com/" + title, GUID: title}); err != nil {
t.Fatal(err)
}
}
items, err := s.GetAllItems("")
if err != nil {
t.Fatal(err)
}

// Opening an article reloads the list, which drops items of feeds
// the config does not name.
cfg := &config.Config{Feeds: []config.Feed{{URL: "https://example.com/feed"}}}
l := list.New(convertItems(items), itemDelegate{theme: cfg.Theme}, 40, 20)
l.Filter = CustomFilter(*cfg)
ListKeyMap.SetOverrides(&l)
// Keep the commands the pump runs from sleeping: a blinking cursor
// waits half a second, a status message a second.
l.FilterInput.Cursor.SetMode(cursor.CursorStatic)
l.StatusMessageLifetime = 0

return model{
cfg: cfg,
commands: New(cfg, s),
list: l,
help: help.New(),
viewport: viewport.New(78, 10),
}
}

// press sends keys to the model as the program would, and runs the commands
// they return, feeding their messages back in, until none are left.
func press(t *testing.T, m model, keys ...tea.KeyMsg) model {
t.Helper()
for _, k := range keys {
next, cmd := m.Update(k)
m = pump(t, next.(model), cmd, 0)
}
return m
}

func pump(t *testing.T, m model, cmd tea.Cmd, depth int) model {
t.Helper()
if cmd == nil || depth > 20 {
return m
}
done := make(chan tea.Msg, 1)
go func() { done <- cmd() }()
var msg tea.Msg
select {
case msg = <-done:
case <-time.After(2 * time.Second):
t.Fatal("a command did not return")
}
switch msg := msg.(type) {
case nil:
return m
case tea.BatchMsg:
for _, c := range msg {
m = pump(t, m, c, depth+1)
}
return m
}
next, cmd := m.Update(msg)
return pump(t, next.(model), cmd, depth+1)
}

func keys(s string) []tea.KeyMsg {
var ks []tea.KeyMsg
for _, r := range s {
ks = append(ks, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
}
return ks
}

var enter = tea.KeyMsg{Type: tea.KeyEnter}

func openTitle(t *testing.T, m model) string {
t.Helper()
if m.selectedArticle == nil {
t.Fatal("no article open")
}
item, err := m.commands.store.GetItemByID(*m.selectedArticle)
if err != nil {
t.Fatal(err)
}
return item.Title
}

func TestNextAndPrevFollowTheFilteredList(t *testing.T) {
m := navModel(t, "rust news", "golang one", "golang two")

// Filter the way a user does, then open the first row.
m = press(t, m, keys("/golang")...)
m = press(t, m, enter)
if len(m.list.VisibleItems()) != 2 {
t.Fatalf("filter shows %d rows, want 2", len(m.list.VisibleItems()))
}
m = press(t, m, enter)
if got := openTitle(t, m); got != "golang one" {
t.Fatalf("opened %q, want the first filtered row, %q", got, "golang one")
}

m = press(t, m, keys("l")...)
if got := openTitle(t, m); got != "golang two" {
t.Errorf("next opened %q, want %q", got, "golang two")
}

m = press(t, m, keys("h")...)
if got := openTitle(t, m); got != "golang one" {
t.Errorf("prev opened %q, want %q", got, "golang one")
}
}
8 changes: 8 additions & 0 deletions internal/commands/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.Height = msg.Height - footerHeight

return m, nil

case list.FilterMatchesMsg:
// Opening an article reloads the list, and a filtered list then
// refilters in the background. The matches belong to the list
// whichever view is showing, or next and prev find no rows.
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}

if m.selectedArticle != nil {
Expand Down
12 changes: 7 additions & 5 deletions internal/commands/viewport.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func updateViewport(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
case key.Matches(msg, ViewportKeyMap.Escape):
// reset cursor if last post is read and quit
index := m.list.Index()
length := len(m.list.Items())
length := len(m.list.VisibleItems())
if index >= length && length >= 1 {
m.list.Select(index - 1)
}
Expand Down Expand Up @@ -75,7 +75,9 @@ func updateViewport(msg tea.Msg, m model) (tea.Model, tea.Cmd) {

case key.Matches(msg, ViewportKeyMap.Prev):
navIndex := m.getPrevIndex()
items := m.list.Items()
// Index counts the filtered rows when a filter is applied, so
// index the rows the list shows, not all of them.
items := m.list.VisibleItems()
if m.isPrevOutOfBounds(navIndex) {
return m, nil
}
Expand All @@ -99,7 +101,7 @@ func updateViewport(msg tea.Msg, m model) (tea.Model, tea.Cmd) {

case key.Matches(msg, ViewportKeyMap.Next):
navIndex := m.getNextIndex()
items := m.list.Items()
items := m.list.VisibleItems()
if m.isNextOutOfBounds(navIndex, len(items)) {
return m, nil
}
Expand Down Expand Up @@ -141,7 +143,7 @@ func updateViewport(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
}

func (m *model) isPrevOutOfBounds(i int) bool {
if len(m.list.Items()) == 0 {
if len(m.list.VisibleItems()) == 0 {
return true
}
return i < 0
Expand Down Expand Up @@ -180,7 +182,7 @@ func (m *model) getNextIndex() int {

func (m *model) getPrevIndex() int {
current := m.list.Index()
if m.commands.config.AutoRead && !m.commands.config.ShowRead && current < len(m.list.Items()) {
if m.commands.config.AutoRead && !m.commands.config.ShowRead && current < len(m.list.VisibleItems()) {
return m.list.Index()
}

Expand Down