Skip to content

Commit 5925f44

Browse files
authored
feat: Add pagination to the repository list view with page indicators… (#10)
* feat: Add pagination to the repository list view with page indicators and navigation controls. * feat: document pagination feature and its corresponding keybindings in the README
1 parent 054f8df commit 5925f44

File tree

4 files changed

+98
-3
lines changed

4 files changed

+98
-3
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ git-scope -h # Show help
8383
* **📁 Workspace Switch** — Switch root directories without quitting (`w`). Supports `~`, relative paths, and **symlinks**.
8484
* **🔍 Fuzzy Search** — Find any repo by name, path, or branch (`/`).
8585
* **🛡️ Dirty Filter** — Instantly show only repos with uncommitted changes (`f`).
86+
* **📄 Pagination** — Navigate large repo lists with page-by-page browsing (`[` / `]`). Shows 15 repos per page with a dynamic page indicator.
8687
* **🚀 Editor Jump** — Open the selected repo in VSCode, Neovim, Vim, or Helix (`Enter`).
8788
* **⚡ Blazing Fast** — JSON caching ensures \~10ms launch time even with 50+ repos.
8889
* **📊 Dashboard Stats** — See branch name, staged/unstaged counts, and last commit time.
@@ -102,6 +103,7 @@ git-scope -h # Show help
102103
| `f` | **Filter** (Cycle: All / Dirty / Clean) |
103104
| `s` | Cycle **Sort** Mode |
104105
| `1``4` | Sort by: Dirty / Name / Branch / Recent |
106+
| `[` / `]` | **Page Navigation** (Previous / Next) |
105107
| `Enter` | **Open** repo in Editor |
106108
| `c` | **Clear** search & filters |
107109
| `r` | **Rescan** directories |

internal/tui/model.go

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ type Model struct {
7474
// Star nudge state
7575
showStarNudge bool
7676
nudgeShownThisSession bool
77+
// Pagination state
78+
currentPage int
79+
pageSize int
7780
}
7881

7982
// NewModel creates a new TUI model
@@ -143,6 +146,8 @@ func NewModel(cfg *config.Config) Model {
143146
state: StateLoading,
144147
sortMode: SortByDirty,
145148
filterMode: FilterAll,
149+
currentPage: 0,
150+
pageSize: 15,
146151
}
147152
}
148153

@@ -157,9 +162,13 @@ func (m Model) GetSelectedRepo() *model.Repo {
157162
return nil
158163
}
159164

165+
// Get the cursor position within the current page
160166
cursor := m.table.Cursor()
161-
if cursor >= 0 && cursor < len(m.sortedRepos) {
162-
return &m.sortedRepos[cursor]
167+
// Calculate the actual index in sortedRepos
168+
actualIndex := m.currentPage*m.pageSize + cursor
169+
170+
if actualIndex >= 0 && actualIndex < len(m.sortedRepos) {
171+
return &m.sortedRepos[actualIndex]
163172
}
164173
return nil
165174
}
@@ -230,7 +239,50 @@ func (m *Model) sortRepos() {
230239
func (m *Model) updateTable() {
231240
m.applyFilter()
232241
m.sortRepos()
233-
m.table.SetRows(reposToRows(m.sortedRepos))
242+
m.table.SetRows(reposToRows(m.getCurrentPageRepos()))
243+
}
244+
245+
// getTotalPages returns the total number of pages
246+
func (m Model) getTotalPages() int {
247+
if len(m.sortedRepos) == 0 {
248+
return 1
249+
}
250+
return (len(m.sortedRepos) + m.pageSize - 1) / m.pageSize
251+
}
252+
253+
// getCurrentPageRepos returns repos for the current page
254+
func (m Model) getCurrentPageRepos() []model.Repo {
255+
if len(m.sortedRepos) == 0 {
256+
return []model.Repo{}
257+
}
258+
259+
start := m.currentPage * m.pageSize
260+
end := start + m.pageSize
261+
262+
if start >= len(m.sortedRepos) {
263+
start = 0
264+
end = m.pageSize
265+
}
266+
if end > len(m.sortedRepos) {
267+
end = len(m.sortedRepos)
268+
}
269+
270+
return m.sortedRepos[start:end]
271+
}
272+
273+
// canGoPrev returns true if there's a previous page
274+
func (m Model) canGoPrev() bool {
275+
return m.currentPage > 0
276+
}
277+
278+
// canGoNext returns true if there's a next page
279+
func (m Model) canGoNext() bool {
280+
return m.currentPage < m.getTotalPages()-1
281+
}
282+
283+
// resetPage resets pagination to first page
284+
func (m *Model) resetPage() {
285+
m.currentPage = 0
234286
}
235287

236288
// GetSortModeName returns the display name of current sort mode

internal/tui/update.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3737
case scanCompleteMsg:
3838
m.repos = msg.repos
3939
m.state = StateReady
40+
m.resetPage()
4041
m.updateTable()
4142

4243
// Show helpful message if no repos found
@@ -57,6 +58,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5758
case workspaceScanCompleteMsg:
5859
m.repos = msg.repos
5960
m.state = StateReady
61+
m.resetPage()
6062
m.updateTable()
6163

6264
// Show helpful message about switched workspace
@@ -186,6 +188,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
186188
// Cycle through filter modes
187189
if m.state == StateReady {
188190
m.filterMode = (m.filterMode + 1) % 3
191+
m.resetPage()
189192
m.updateTable()
190193
m.statusMsg = "Filter: " + m.GetFilterModeName()
191194
return m, nil
@@ -194,6 +197,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
194197
case "s":
195198
if m.state == StateReady {
196199
m.sortMode = (m.sortMode + 1) % 4
200+
m.resetPage()
197201
m.updateTable()
198202
m.statusMsg = "Sorted by: " + m.GetSortModeName()
199203
return m, nil
@@ -202,6 +206,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
202206
case "1":
203207
if m.state == StateReady {
204208
m.sortMode = SortByDirty
209+
m.resetPage()
205210
m.updateTable()
206211
m.statusMsg = "Sorted by: Dirty First"
207212
return m, nil
@@ -210,6 +215,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
210215
case "2":
211216
if m.state == StateReady {
212217
m.sortMode = SortByName
218+
m.resetPage()
213219
m.updateTable()
214220
m.statusMsg = "Sorted by: Name"
215221
return m, nil
@@ -218,6 +224,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
218224
case "3":
219225
if m.state == StateReady {
220226
m.sortMode = SortByBranch
227+
m.resetPage()
221228
m.updateTable()
222229
m.statusMsg = "Sorted by: Branch"
223230
return m, nil
@@ -226,6 +233,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
226233
case "4":
227234
if m.state == StateReady {
228235
m.sortMode = SortByLastCommit
236+
m.resetPage()
229237
m.updateTable()
230238
m.statusMsg = "Sorted by: Recent"
231239
return m, nil
@@ -237,6 +245,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
237245
m.searchQuery = ""
238246
m.textInput.SetValue("") // Also reset the text input
239247
m.filterMode = FilterAll
248+
m.resetPage()
240249
m.resizeTable()
241250
m.updateTable()
242251
m.statusMsg = "Filters cleared"
@@ -316,6 +325,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
316325
m.workspaceError = ""
317326
return m, textinput.Blink
318327
}
328+
329+
case "[":
330+
// Previous page
331+
if m.state == StateReady && m.canGoPrev() {
332+
m.currentPage--
333+
m.updateTable()
334+
m.statusMsg = fmt.Sprintf("Page %d of %d", m.currentPage+1, m.getTotalPages())
335+
return m, nil
336+
}
337+
338+
case "]":
339+
// Next page
340+
if m.state == StateReady && m.canGoNext() {
341+
m.currentPage++
342+
m.updateTable()
343+
m.statusMsg = fmt.Sprintf("Page %d of %d", m.currentPage+1, m.getTotalPages())
344+
return m, nil
345+
}
319346
}
320347
}
321348

@@ -347,6 +374,7 @@ func (m Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
347374
m.state = StateReady
348375
m.resizeTable()
349376
m.textInput.Blur()
377+
m.resetPage()
350378
m.updateTable()
351379
if m.searchQuery != "" {
352380
m.statusMsg = "Searching: " + m.searchQuery

internal/tui/view.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,18 @@ func (m Model) renderStats() string {
253253
sortHint := hintStyle.Render(" (s)")
254254
stats = append(stats, sortBadge+sortHint)
255255

256+
// Pagination indicator (only show if more than one page)
257+
totalPages := m.getTotalPages()
258+
if totalPages > 1 {
259+
pageBadge := lipgloss.NewStyle().
260+
Foreground(lipgloss.Color("#FFFFFF")).
261+
Background(lipgloss.Color("#10B981")).
262+
Padding(0, 1).
263+
Render(fmt.Sprintf("📄 %d/%d", m.currentPage+1, totalPages))
264+
pageHint := hintStyle.Render(" ([])")
265+
stats = append(stats, pageBadge+pageHint)
266+
}
267+
256268
return lipgloss.JoinHorizontal(lipgloss.Center, stats...)
257269
}
258270

@@ -299,6 +311,7 @@ func (m Model) renderHelp() string {
299311
// Normal mode help - Tuimorphic style
300312
items = []string{
301313
keyBinding("↑↓", "nav"),
314+
keyBinding("[]", "page"),
302315
keyBinding("enter", "open"),
303316
keyBinding("/", "search"),
304317
keyBinding("w", "workspace"),

0 commit comments

Comments
 (0)