From 12e5d5aa3378e1e715663bb118b80b12a8d5c9c1 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Wed, 21 Oct 2026 12:35:49 +0200 Subject: [PATCH] Library: keep the place after removing a title The list is reloaded after a removal and the selection follows the selected title by id; the removed title is gone, so it fell back to the top. Now a title that has left the library hands its row to the one that moved up into it (or the new last one). A title that is only filtered out still starts the list from the top. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW --- CHANGELOG.md | 2 ++ internal/ui/library.go | 20 ++++++++++++++-- internal/ui/library_test.go | 48 +++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73970f0..85e14b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- **Library:** after removing a title the selection stays at its place (on the title that moved + up into it, or the new last one) instead of jumping to the top of the list. - **Config (F6):** the default quality profile and root folder are chosen from the lists Radarr and Sonarr have, loaded with the address and API key in the fields (when the view opens, and again shortly after either is changed, before saving). A configured value the server does not diff --git a/internal/ui/library.go b/internal/ui/library.go index a601ce0..ff6fc22 100644 --- a/internal/ui/library.go +++ b/internal/ui/library.go @@ -391,9 +391,9 @@ func (v *libraryView) render() { } func (v *libraryView) renderPane(p *libPane) { - selID := -1 + selID, prevRow := -1, 0 if r, _ := p.table.GetSelection(); r >= 1 && r <= len(p.shown) { - selID = p.shown[r-1].id + selID, prevRow = p.shown[r-1].id, r } p.shown = filterItems(p.all, p.filter, p.text, p.attr) @@ -408,6 +408,13 @@ func (v *libraryView) renderPane(p *libPane) { selRow = i + 1 } } + if selRow == 0 && selID != -1 && !containsItem(p.all, selID) { + // The selected title is gone from the library (removed): stay at its + // place, on the title that moved up into it (or the new last one), + // instead of jumping to the top. A title that is only filtered out + // still starts the list from the top. + selRow = min(prevRow, len(p.shown)) + } placeholder := "Nothing matches the filter" switch { case p.err != nil && !p.loaded: @@ -437,6 +444,15 @@ func (v *libraryView) renderPane(p *libPane) { p.table.SetTitle(title + " ") } +func containsItem(items []libItem, id int) bool { + for _, it := range items { + if it.id == id { + return true + } + } + return false +} + func (v *libraryView) renderDetails() { if it, ok := v.selected(); ok { v.details.Show(it.details, it.cover) diff --git a/internal/ui/library_test.go b/internal/ui/library_test.go index 5936a5b..72cbd0a 100644 --- a/internal/ui/library_test.go +++ b/internal/ui/library_test.go @@ -209,3 +209,51 @@ func TestListedValues(t *testing.T) { t.Errorf("empty pool: %v", got) } } + +func TestRemovingATitleKeepsThePlace(t *testing.T) { + a := newTestApp(t) + v := newLibraryView(a, a.radarr, a.sonarr) + p := v.panes[0] + items := func(ids ...int) []libItem { + var out []libItem + for _, id := range ids { + out = append(out, libItem{id: id, title: string(rune('A' + id)), label: string(rune('A' + id))}) + } + return out + } + selectedID := func() int { + r, _ := p.table.GetSelection() + return p.shown[r-1].id + } + p.all, p.loaded = items(1, 2, 3, 4, 5), true + v.renderPane(p) + + // Remove the third title: the fourth moves up into its place and is selected. + p.table.Select(3, 0) + p.all = items(1, 2, 4, 5) + v.renderPane(p) + if r, _ := p.table.GetSelection(); r != 3 || selectedID() != 4 { + t.Errorf("after removing the third: row %d, title %d", r, selectedID()) + } + // Remove the last title: the new last one is selected. + p.table.Select(4, 0) + p.all = items(1, 2, 4) + v.renderPane(p) + if r, _ := p.table.GetSelection(); r != 3 || selectedID() != 4 { + t.Errorf("after removing the last: row %d, title %d", r, selectedID()) + } + // A reload that keeps the title keeps it selected, wherever it is now. + p.table.Select(2, 0) // title 2 + p.all = items(0, 1, 2, 4) + v.renderPane(p) + if selectedID() != 2 { + t.Errorf("kept title not followed: %d", selectedID()) + } + // A title that is only filtered out still starts the list from the top. + p.table.Select(4, 0) // title 4 + p.text = "B" // title 1 only: 4 is filtered out, not removed + v.renderPane(p) + if r, _ := p.table.GetSelection(); r != 1 { + t.Errorf("filtered out: row %d, want the top", r) + } +}