@@ -38,8 +38,11 @@ type pagerModel[T any] struct {
3838 spinner bubblespinner.Model
3939 // fetch is bound at construction with the caller's context captured
4040 // so we don't have to stash ctx on the struct (tea.Cmd has no ctx
41- // parameter of its own).
42- fetch func () tea.Msg
41+ // parameter of its own). It takes the paging state as arguments and
42+ // returns a pure Cmd: bubbletea runs Cmds on their own goroutine
43+ // concurrently with Update, so the returned closure must not read or
44+ // write the model.
45+ fetch func (pageSize , limit , total int ) tea.Cmd
4346 pageSize int
4447 limit int
4548 total int
@@ -70,7 +73,9 @@ func newPagerModel[T any](
7073 pageSize : pageSize ,
7174 limit : limit ,
7275 }
73- m .fetch = func () tea.Msg { return m .doFetch (ctx ) }
76+ m .fetch = func (pageSize , limit , total int ) tea.Cmd {
77+ return func () tea.Msg { return m .doFetch (ctx , pageSize , limit , total ) }
78+ }
7479 return m
7580}
7681
@@ -86,27 +91,32 @@ func newPagerSpinner() bubblespinner.Model {
8691 return s
8792}
8893
89- // batchMsg carries the rendered lines from one fetch. done is true when
90- // the iterator is exhausted or the limit is reached.
94+ // batchMsg carries the rendered lines from one fetch. count is the number
95+ // of rows fetched, added to m.total in Update. done is true when the
96+ // iterator is exhausted or the limit is reached.
9197type batchMsg struct {
9298 lines []string
99+ count int
93100 done bool
94101 err error
95102}
96103
97104func (m * pagerModel [T ]) Init () tea.Cmd {
98105 m .fetching = true
99- return tea .Batch (m .fetch , m .spinner .Tick )
106+ return tea .Batch (m .fetch ( m . pageSize , m . limit , m . total ) , m .spinner .Tick )
100107}
101108
102109// doFetch reads one page from the iterator and renders it into lines.
103110// It runs off the update loop so a slow network fetch doesn't stall
104- // key handling.
105- func (m * pagerModel [T ]) doFetch (ctx context.Context ) tea.Msg {
106- buf := make ([]any , 0 , m .pageSize )
111+ // key handling. The paging state is passed in rather than read off the
112+ // model: bubbletea runs this on its own goroutine concurrently with
113+ // Update, so it must not touch shared model fields. m.iter and m.pager
114+ // are exempt because only one fetch is ever in flight at a time.
115+ func (m * pagerModel [T ]) doFetch (ctx context.Context , pageSize , limit , total int ) tea.Msg {
116+ buf := make ([]any , 0 , pageSize )
107117 done := false
108- for len (buf ) < m . pageSize {
109- if m . limit > 0 && m . total + len (buf ) >= m . limit {
118+ for len (buf ) < pageSize {
119+ if limit > 0 && total + len (buf ) >= limit {
110120 done = true
111121 break
112122 }
@@ -124,8 +134,11 @@ func (m *pagerModel[T]) doFetch(ctx context.Context) tea.Msg {
124134 if err != nil {
125135 return batchMsg {err : err }
126136 }
127- m .total += len (buf )
128- return batchMsg {lines : lines , done : done }
137+ // The loop exits on len(buf) == pageSize before re-checking the limit,
138+ // so a page that lands exactly on the limit leaves done false. Re-check
139+ // here to avoid scheduling one more (empty) fetch on drain/advance.
140+ done = done || (limit > 0 && total + len (buf ) >= limit )
141+ return batchMsg {lines : lines , count : len (buf ), done : done }
129142}
130143
131144func (m * pagerModel [T ]) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
@@ -145,6 +158,7 @@ func (m *pagerModel[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145158 m .err = msg .err
146159 return m , tea .Quit
147160 }
161+ m .total += msg .count
148162 m .hasPrinted = true
149163 // One Println cmd (not N) keeps the batch ordered even though
150164 // tea.Sequence dispatches each cmd on its own goroutine.
@@ -158,7 +172,7 @@ func (m *pagerModel[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
158172 return m , tea .Sequence (printCmd , tea .Quit )
159173 case m .drainAll :
160174 m .fetching = true
161- return m , tea .Sequence (printCmd , m .fetch )
175+ return m , tea .Sequence (printCmd , m .fetch ( m . pageSize , m . limit , m . total ) )
162176 default :
163177 return m , printCmd
164178 }
@@ -196,7 +210,7 @@ func (m *pagerModel[T]) startAdvance() tea.Cmd {
196210 return nil
197211 }
198212 m .fetching = true
199- return m .fetch
213+ return m .fetch ( m . pageSize , m . limit , m . total )
200214}
201215
202216func (m * pagerModel [T ]) startDrain () tea.Cmd {
@@ -210,7 +224,7 @@ func (m *pagerModel[T]) startDrain() tea.Cmd {
210224 return nil
211225 }
212226 m .fetching = true
213- return m .fetch
227+ return m .fetch ( m . pageSize , m . limit , m . total )
214228}
215229
216230func (m * pagerModel [T ]) View () string {
0 commit comments