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:
+4
-2
@@ -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
@@ -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 & 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 ★ / 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>↑ / ↓ 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>↑ / ↓ j / k 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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user