feat(ui): add search result navigation keys

Adds Ctrl+N/Ctrl+P single-step navigation and Ctrl+U/Ctrl+D page navigation in search result lists. Updates docs and site keybinding references.
This commit is contained in:
Evan Tetzlaff
2026-06-15 12:25:24 -06:00
committed by GitHub
parent 5fcd5aaabe
commit aecf2e0bf1
5 changed files with 136 additions and 10 deletions
+4 -2
View File
@@ -29,7 +29,7 @@ Press `?` or `Ctrl+K` in the player to see all keybindings.
| `Shift+Up` `Shift+Down` | Move track up/down in playlist/queue |
| `h` `l` | EQ cursor left/right |
| `Enter` | Play selected track |
| `/` | Search playlist |
| `/` | Search playlist (navigate results with `↑` `↓` / `Ctrl+N` `Ctrl+P`; page scroll with `Ctrl+U` `Ctrl+D`) |
| `Ctrl+X` | Expand/collapse playlist |
| `o` | Open file browser |
| `b` `Esc` | Back to provider |
@@ -117,6 +117,7 @@ The playlists pane (visible when focus is on a provider — Spotify, Navidrome,
| Key | Action |
|---|---|
| `↑` `↓` / `j` `k` | Move cursor (wraps) |
| `Ctrl+U` `Ctrl+D` | Scroll by page |
| `Enter` | Load the highlighted playlist's tracks into the queue |
| `/` | Filter the playlist list |
| `Ctrl+F` | Online/server search (Spotify/Navidrome/NetEase/etc.'s own search) |
@@ -133,7 +134,8 @@ When `Ctrl+F` opens provider search or YouTube/SoundCloud net search and you're
| Key | Action |
|---|---|
| `↑` `↓` / `j` `k` | Move cursor |
| `↑` `↓` / `j` `k` / `Ctrl+N` `Ctrl+P` | Move cursor (single item) |
| `Ctrl+U` `Ctrl+D` | Scroll results by page |
| `Enter` | Play the selected track now |
| `a` | Append the selected track to the playlist |
| `q` | Queue the selected track to play next |
+5 -2
View File
@@ -1958,7 +1958,7 @@ user_id = "your-account-user-id"</code></pre>
<div class="key-row"><kbd>j / k</kbd><span>Scroll or adjust EQ (wraps)</span></div>
<div class="key-row"><kbd>h / l</kbd><span>EQ band left / right</span></div>
<div class="key-row"><kbd>PgUp / PgDn</kbd><span>Page up / down</span></div>
<div class="key-row"><kbd>Ctrl+U / Ctrl+D</kbd><span>Half-page scroll</span></div>
<div class="key-row"><kbd>Ctrl+U / Ctrl+D</kbd><span>Page up / down</span></div>
<div class="key-row"><kbd>Home / End</kbd><span>Jump to top / end</span></div>
<div class="key-row"><kbd>g / G</kbd><span>Vim-style top / bottom</span></div>
<div class="key-row"><kbd>Enter</kbd><span>Play selected</span></div>
@@ -1970,7 +1970,7 @@ user_id = "your-account-user-id"</code></pre>
<div class="keys-group">
<div class="keys-group-title">Search &amp; Browse</div>
<div class="keys-group-body">
<div class="key-row"><kbd>/</kbd><span>Search playlist (fuzzy, ranked by relevance)</span></div>
<div class="key-row"><kbd>/</kbd><span>Search playlist (fuzzy; ↑ / ↓ or Ctrl+N / Ctrl+P move results; Ctrl+U / Ctrl+D page results)</span></div>
<div class="key-row"><kbd>Ctrl+F</kbd><span>Search active provider (Spotify, Navidrome, Jellyfin, Emby, Plex, NetEase, Local) or YouTube fallback; Local search is fuzzy</span></div>
<div class="key-row"><kbd>f</kbd><span>Toggle bookmark &#9733; / radio favorite</span></div>
<div class="key-row"><kbd>u</kbd><span>Load URL (stream / playlist)</span></div>
@@ -2000,6 +2000,7 @@ user_id = "your-account-user-id"</code></pre>
<div class="keys-group-title">Provider Playlists Pane</div>
<div class="keys-group-body">
<div class="key-row"><kbd>↑ / ↓&nbsp;&nbsp;&nbsp;j / k</kbd><span>Move cursor (wraps)</span></div>
<div class="key-row"><kbd>Ctrl+U / Ctrl+D</kbd><span>Scroll by page</span></div>
<div class="key-row"><kbd>Enter</kbd><span>Load the highlighted playlist's tracks into the queue</span></div>
<div class="key-row"><kbd>/</kbd><span>Filter the visible playlists</span></div>
<div class="key-row"><kbd>Ctrl+F</kbd><span>Online / server search via the provider's own search</span></div>
@@ -2028,6 +2029,8 @@ user_id = "your-account-user-id"</code></pre>
<div class="keys-group">
<div class="keys-group-title">Search Results</div>
<div class="keys-group-body">
<div class="key-row"><kbd>&uarr; / &darr;&nbsp;&nbsp;&nbsp;j / k&nbsp;&nbsp;&nbsp;Ctrl+N / Ctrl+P</kbd><span>Move cursor (single item, wraps)</span></div>
<div class="key-row"><kbd>Ctrl+U / Ctrl+D</kbd><span>Scroll results by page</span></div>
<div class="key-row"><kbd>Enter</kbd><span>Play selected track now</span></div>
<div class="key-row"><kbd>a</kbd><span>Append selected to playlist</span></div>
<div class="key-row"><kbd>q</kbd><span>Queue selected to play next</span></div>
+1 -1
View File
@@ -93,7 +93,7 @@ var coreReservedKeys = []string{
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "j",
// Navigation and focus.
"up", "k", "down",
"up", "k", "down", "ctrl+n", "ctrl+p",
"shift+up", "shift+down",
"pgup", "pgdown", "ctrl+u", "ctrl+d",
"g", "G", "home", "end",
+103 -3
View File
@@ -965,10 +965,50 @@ func (m *Model) provSearchMaybeAdjustScroll() {
// For the radio provider, Enter fires an API search; for others, Enter loads the
// selected result. Esc cancels and restores the normal catalog view.
func (m *Model) handleProvSearchKey(msg tea.KeyPressMsg) tea.Cmd {
if msg.String() == "ctrl+x" {
switch msg.String() {
case "ctrl+x":
m.toggleExpandedView()
m.provSearchMaybeAdjustScroll()
return nil
case "ctrl+n":
if m.provSearch.cursor < len(m.provSearch.results)-1 {
m.provSearch.cursor++
} else if len(m.provSearch.results) > 0 {
m.provSearch.cursor = 0
}
m.provSearchMaybeAdjustScroll()
return nil
case "ctrl+p":
if m.provSearch.cursor > 0 {
m.provSearch.cursor--
} else if len(m.provSearch.results) > 0 {
m.provSearch.cursor = len(m.provSearch.results) - 1
}
m.provSearchMaybeAdjustScroll()
return nil
case "ctrl+u":
step := m.providerScrollStep() - 1
if step < 1 {
step = 1
}
if m.provSearch.cursor >= step {
m.provSearch.cursor -= step
} else {
m.provSearch.cursor = 0
}
m.provSearchMaybeAdjustScroll()
return nil
case "ctrl+d":
step := m.providerScrollStep() - 1
if step < 1 {
step = 1
}
m.provSearch.cursor += step
if m.provSearch.cursor >= len(m.provSearch.results) {
m.provSearch.cursor = max(0, len(m.provSearch.results)-1)
}
m.provSearchMaybeAdjustScroll()
return nil
}
// Catalog search: API-based search (no live client-side filtering).
@@ -1180,6 +1220,45 @@ func (m *Model) handleSearchKey(msg tea.KeyPressMsg) tea.Cmd {
m.toggleExpandedView()
m.searchMaybeAdjustScroll(m.searchVisible())
return nil
case "ctrl+n":
if m.search.cursor < len(m.search.results)-1 {
m.search.cursor++
} else if len(m.search.results) > 0 {
m.search.cursor = 0
}
m.searchMaybeAdjustScroll(m.searchVisible())
return nil
case "ctrl+p":
if m.search.cursor > 0 {
m.search.cursor--
} else if len(m.search.results) > 0 {
m.search.cursor = len(m.search.results) - 1
}
m.searchMaybeAdjustScroll(m.searchVisible())
return nil
case "ctrl+u":
step := m.searchVisible()
if step < 1 {
step = 1
}
if m.search.cursor >= step {
m.search.cursor -= step
} else {
m.search.cursor = 0
}
m.searchMaybeAdjustScroll(m.searchVisible())
return nil
case "ctrl+d":
step := m.searchVisible()
if step < 1 {
step = 1
}
m.search.cursor += step
if m.search.cursor >= len(m.search.results) {
m.search.cursor = max(0, len(m.search.results)-1)
}
m.searchMaybeAdjustScroll(m.searchVisible())
return nil
}
switch msg.Code {
@@ -1305,14 +1384,14 @@ func (m *Model) handleNetSearchResultsKey(msg tea.KeyPressMsg) tea.Cmd {
case "ctrl+x":
m.toggleExpandedView()
m.netSearchResultsMaybeAdjustScroll(m.netSearchResultsVisible())
case "up", "k":
case "up", "k", "ctrl+p":
if m.netSearch.cursor > 0 {
m.netSearch.cursor--
} else if count > 0 {
m.netSearch.cursor = count - 1
}
m.netSearchResultsMaybeAdjustScroll(m.netSearchResultsVisible())
case "down", "j":
case "down", "j", "ctrl+n":
if m.netSearch.cursor < count-1 {
m.netSearch.cursor++
} else if count > 0 {
@@ -1343,6 +1422,27 @@ func (m *Model) handleNetSearchResultsKey(msg tea.KeyPressMsg) tea.Cmd {
m.netSearch.cursor = 0
m.netSearch.scroll = 0
m.netSearch.err = ""
case "ctrl+u":
step := m.netSearchResultsVisible()
if step < 1 {
step = 1
}
if m.netSearch.cursor >= step {
m.netSearch.cursor -= step
} else {
m.netSearch.cursor = 0
}
m.netSearchResultsMaybeAdjustScroll(m.netSearchResultsVisible())
case "ctrl+d":
step := m.netSearchResultsVisible()
if step < 1 {
step = 1
}
m.netSearch.cursor += step
if m.netSearch.cursor >= count {
m.netSearch.cursor = max(0, count-1)
}
m.netSearchResultsMaybeAdjustScroll(m.netSearchResultsVisible())
}
return nil
}
+23 -2
View File
@@ -68,14 +68,14 @@ func (m *Model) handleSpotSearchResultsKey(msg tea.KeyPressMsg) tea.Cmd {
case "ctrl+x":
m.toggleExpandedView()
m.spotSearchResultsMaybeAdjustScroll(m.spotSearchResultsVisible())
case "up", "k":
case "up", "k", "ctrl+p":
if m.spotSearch.cursor > 0 {
m.spotSearch.cursor--
} else if count > 0 {
m.spotSearch.cursor = count - 1
}
m.spotSearchResultsMaybeAdjustScroll(m.spotSearchResultsVisible())
case "down", "j":
case "down", "j", "ctrl+n":
if m.spotSearch.cursor < count-1 {
m.spotSearch.cursor++
} else if count > 0 {
@@ -110,6 +110,27 @@ func (m *Model) handleSpotSearchResultsKey(msg tea.KeyPressMsg) tea.Cmd {
case "esc", "backspace":
m.spotSearch.screen = spotSearchInput
m.spotSearch.err = ""
case "ctrl+u":
step := m.spotSearchResultsVisible()
if step < 1 {
step = 1
}
if m.spotSearch.cursor >= step {
m.spotSearch.cursor -= step
} else {
m.spotSearch.cursor = 0
}
m.spotSearchResultsMaybeAdjustScroll(m.spotSearchResultsVisible())
case "ctrl+d":
step := m.spotSearchResultsVisible()
if step < 1 {
step = 1
}
m.spotSearch.cursor += step
if m.spotSearch.cursor >= count {
m.spotSearch.cursor = max(0, count-1)
}
m.spotSearchResultsMaybeAdjustScroll(m.spotSearchResultsVisible())
}
return nil
}