@@ -64,20 +64,30 @@ type paginatedModel struct {
6464 widths []int
6565
6666 // Search
67- searching bool
68- searchLoading bool
69- searchInput string
70- debounceSeq int
71- hasSearchState bool
72- savedRows [][]string
73- savedIter RowIterator
74- savedExhaust bool
67+ search searchState
7568
7669 // Limits
7770 maxItems int
7871 limitReached bool
7972}
8073
74+ // searchState groups the server-side search / debounce state.
75+ // When a search replaces the original iterator, saved holds the
76+ // pre-search snapshot so it can be restored on cancel/clear.
77+ type searchState struct {
78+ active bool
79+ loading bool
80+ input string
81+ debounceSeq int
82+ saved * savedSearch
83+ }
84+
85+ type savedSearch struct {
86+ rows [][]string
87+ iter RowIterator
88+ exhausted bool
89+ }
90+
8191// Err returns the error recorded during data fetching, if any.
8292func (m paginatedModel ) Err () error {
8393 return m .err
@@ -166,7 +176,7 @@ func (m paginatedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
166176 switch msg := msg .(type ) {
167177 case tea.WindowSizeMsg :
168178 fh := footerHeight
169- if m .searching {
179+ if m .search . active {
170180 fh = searchFooterHeight
171181 }
172182 if ! m .ready {
@@ -197,9 +207,9 @@ func (m paginatedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
197207 m .err = nil
198208
199209 isFirstBatch := len (m .rows ) == 0
200- if m .searchLoading {
210+ if m .search . loading {
201211 m .rows = msg .rows
202- m .searchLoading = false
212+ m .search . loading = false
203213 isFirstBatch = true
204214 } else {
205215 m .rows = append (m .rows , msg .rows ... )
@@ -224,13 +234,13 @@ func (m paginatedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
224234 return m , nil
225235
226236 case searchDebounceMsg :
227- if msg .seq != m .debounceSeq || ! m .searching {
237+ if msg .seq != m .search . debounceSeq || ! m .search . active {
228238 return m , nil
229239 }
230- return m .executeSearch (m .searchInput )
240+ return m .executeSearch (m .search . input )
231241
232242 case tea.KeyMsg :
233- if m .searching {
243+ if m .search . active {
234244 return m .updateSearch (msg )
235245 }
236246 return m .updateNormal (msg )
@@ -301,8 +311,8 @@ func (m paginatedModel) updateNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
301311 return m , tea .Quit
302312 case "/" :
303313 if m .cfg .Search != nil {
304- m .searching = true
305- m .searchInput = ""
314+ m .search . active = true
315+ m .search . input = ""
306316 // Shrink viewport by one row to make room for the search input bar.
307317 m .viewport .Height --
308318 return m , nil
@@ -352,7 +362,7 @@ func (m *paginatedModel) moveCursor(delta int) {
352362}
353363
354364func maybeFetch (m paginatedModel ) (paginatedModel , tea.Cmd ) {
355- if m .loading || m .exhausted || m .searching {
365+ if m .loading || m .exhausted || m .search . active {
356366 return m , nil
357367 }
358368 if len (m .rows )- m .cursor <= fetchThresholdFromBottom {
@@ -364,8 +374,8 @@ func maybeFetch(m paginatedModel) (paginatedModel, tea.Cmd) {
364374
365375// scheduleSearchDebounce returns a command that sends a searchDebounceMsg after the delay.
366376func (m * paginatedModel ) scheduleSearchDebounce () tea.Cmd {
367- m .debounceSeq ++
368- seq := m .debounceSeq
377+ m .search . debounceSeq ++
378+ seq := m .search . debounceSeq
369379 return tea .Tick (searchDebounceDelay , func (_ time.Time ) tea.Msg {
370380 return searchDebounceMsg {seq : seq }
371381 })
@@ -375,20 +385,17 @@ func (m *paginatedModel) scheduleSearchDebounce() tea.Cmd {
375385// loading so that maybeFetch is unblocked. Safe to call even when there is
376386// no saved search state.
377387func (m * paginatedModel ) restorePreSearchState () {
378- if m .hasSearchState {
388+ if m .search . saved != nil {
379389 // Bump generation to discard any in-flight search fetch, since we're
380390 // switching back to the original iterator.
381391 m .fetchGeneration ++
382- m .rows = m .savedRows
383- m .rowIter = m .savedIter
384- m .exhausted = m .savedExhaust
385- m .hasSearchState = false
386- m .savedRows = nil
387- m .savedIter = nil
388- m .savedExhaust = false
392+ m .rows = m .search .saved .rows
393+ m .rowIter = m .search .saved .iter
394+ m .exhausted = m .search .saved .exhausted
395+ m .search .saved = nil
389396 m .limitReached = false
390397 m .loading = false
391- m .searchLoading = false
398+ m .search . loading = false
392399 }
393400 m .cursor = 0
394401 if m .ready {
@@ -406,18 +413,19 @@ func (m paginatedModel) executeSearch(query string) (tea.Model, tea.Cmd) {
406413 return m , nil
407414 }
408415
409- if ! m .hasSearchState {
410- m .hasSearchState = true
411- m .savedRows = m .rows
412- m .savedIter = m .rowIter
413- m .savedExhaust = m .exhausted
416+ if m .search .saved == nil {
417+ m .search .saved = & savedSearch {
418+ rows : m .rows ,
419+ iter : m .rowIter ,
420+ exhausted : m .exhausted ,
421+ }
414422 }
415423
416424 m .fetchGeneration ++
417425 m .exhausted = false
418426 m .limitReached = false
419427 m .loading = true
420- m .searchLoading = true
428+ m .search . loading = true
421429 m .cursor = 0
422430 m .rowIter = m .makeSearchIter (query )
423431 return m , m .makeFetchCmd (m )
@@ -426,33 +434,33 @@ func (m paginatedModel) executeSearch(query string) (tea.Model, tea.Cmd) {
426434func (m paginatedModel ) updateSearch (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
427435 switch msg .String () {
428436 case "enter" :
429- m .searching = false
437+ m .search . active = false
430438 // Restore viewport height now that search bar is hidden.
431439 m .viewport .Height ++
432440 // Execute final search immediately (bypass debounce).
433- return m .executeSearch (m .searchInput )
441+ return m .executeSearch (m .search . input )
434442 case "ctrl+c" :
435443 return m , tea .Quit
436444 case "esc" :
437- m .searching = false
438- m .searchInput = ""
445+ m .search . active = false
446+ m .search . input = ""
439447 // Restore viewport height now that search bar is hidden.
440448 m .viewport .Height ++
441449 m .restorePreSearchState ()
442450 return m , nil
443451 case "backspace" :
444- if len (m .searchInput ) > 0 {
445- _ , size := utf8 .DecodeLastRuneInString (m .searchInput )
446- m .searchInput = m .searchInput [:len (m .searchInput )- size ]
452+ if len (m .search . input ) > 0 {
453+ _ , size := utf8 .DecodeLastRuneInString (m .search . input )
454+ m .search . input = m .search . input [:len (m .search . input )- size ]
447455 }
448456 return m , m .scheduleSearchDebounce ()
449457 default :
450458 if msg .Type == tea .KeyRunes {
451- m .searchInput += msg .String ()
459+ m .search . input += msg .String ()
452460 return m , m .scheduleSearchDebounce ()
453461 }
454462 if msg .Type == tea .KeySpace {
455- m .searchInput += " "
463+ m .search . input += " "
456464 return m , m .scheduleSearchDebounce ()
457465 }
458466 return m , nil
@@ -464,7 +472,7 @@ func (m paginatedModel) View() string {
464472 return "Loading..."
465473 }
466474 if len (m .rows ) == 0 && m .loading {
467- if m .searchLoading {
475+ if m .search . loading {
468476 return "Searching..."
469477 }
470478 return "Fetching results..."
@@ -484,18 +492,18 @@ func (m paginatedModel) View() string {
484492}
485493
486494func (m paginatedModel ) renderFooter () string {
487- if m .searching {
495+ if m .search . active {
488496 placeholder := ""
489497 if m .cfg .Search != nil {
490498 placeholder = m .cfg .Search .Placeholder
491499 }
492- input := m .searchInput
500+ input := m .search . input
493501 if input == "" && placeholder != "" {
494502 input = footerStyle .Render (placeholder )
495503 }
496504 prompt := searchStyle .Render ("/ " + input + "█" )
497505 status := fmt .Sprintf ("%d rows loaded" , len (m .rows ))
498- if m .searchLoading {
506+ if m .search . loading {
499507 status = "Searching..."
500508 }
501509 return footerStyle .Render (status ) + "\n " + prompt
0 commit comments