feat: add hjkl navigation alongside the arrow keys (#29)

Layer vim-style h/j/k/l onto the existing arrow navigation across the sidebar, results, downloads, and seeding. Arrows unchanged.
This commit is contained in:
Avinash Gundimeda
2026-07-01 02:36:45 -04:00
committed by GitHub
parent 1b53ef3094
commit 6ba3e4292b
8 changed files with 30 additions and 26 deletions
+14 -10
View File
@@ -107,15 +107,19 @@
<text x="742.4" y="458" textLength="28.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6" fill-opacity="0.55">SUB</text>
<rect x="784.9" y="442" width="1.4" height="22" fill="#a78bfa"/>
<text x="204.8" y="480" textLength="585.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#a78bfa">╰───────────────────────────────────────────────────────────╯</text>
<text x="41.6" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">d</text>
<text x="60.8" y="502" textLength="76.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Download</text>
<text x="166.4" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">/</text>
<text x="185.6" y="502" textLength="57.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Search</text>
<text x="272" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">m</text>
<text x="291.2" y="502" textLength="115.2" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Paste magnet</text>
<text x="435.2" y="502" textLength="28.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">tab</text>
<text x="473.6" y="502" textLength="105.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Switch pane</text>
<text x="608" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">?</text>
<text x="627.2" y="502" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Keys</text>
<text x="41.6" y="502" textLength="86.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">↑↓←→ hjkl</text>
<text x="137.6" y="502" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Move</text>
<text x="204.8" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">d</text>
<text x="224" y="502" textLength="76.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Download</text>
<text x="329.6" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">y</text>
<text x="348.8" y="502" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Copy</text>
<text x="416" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">s</text>
<text x="435.2" y="502" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Sort</text>
<text x="502.4" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">/</text>
<text x="521.6" y="502" textLength="57.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Search</text>
<text x="608" y="502" textLength="28.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">tab</text>
<text x="646.4" y="502" textLength="57.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Switch</text>
<text x="732.8" y="502" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">?</text>
<text x="752" y="502" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Keys</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+3 -3
View File
@@ -105,9 +105,9 @@
<text x="137.6" y="414" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">c</text>
<text x="156.8" y="414" textLength="57.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Cancel</text>
<text x="243.2" y="414" textLength="28.8" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">tab</text>
<text x="281.6" y="414" textLength="105.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Switch pane</text>
<text x="416" y="414" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">?</text>
<text x="435.2" y="414" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Keys</text>
<text x="281.6" y="414" textLength="57.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Switch</text>
<text x="368" y="414" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">?</text>
<text x="387.2" y="414" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Keys</text>
</g>
<rect x="262.4" y="222" width="9.6" height="22" fill="#7c5cd6">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#7c5cd6;#7f5fd7;#8669da;#9277de;#a18ae3;#b29ee8;#c3b3ee;#d2c5f3;#ded4f7;#e5ddfa;#e8e0fb;#e5ddfa;#ded4f7;#d2c5f3;#c3b3ee;#b29ee8;#a18ae3;#9277de;#8669da;#7f5fd7;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6" repeatCount="indefinite"/>

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+2 -2
View File
@@ -351,11 +351,11 @@ export function App({
setRegion(region === "sidebar" ? "content" : "sidebar");
return;
}
if (key.rightArrow) {
if (key.rightArrow || input === "l") {
if (region === "sidebar") setRegion("content");
return;
}
if (key.leftArrow) {
if (key.leftArrow || input === "h") {
if (region === "content") setRegion("sidebar");
return;
}
+2 -2
View File
@@ -57,8 +57,8 @@ export function Downloads() {
useInput(
(input, key) => {
if (key.upArrow) setCursor(wrapStep(clamped, -1, total));
else if (key.downArrow) setCursor(wrapStep(clamped, 1, total));
if (key.upArrow || input === "k") setCursor(wrapStep(clamped, -1, total));
else if (key.downArrow || input === "j") setCursor(wrapStep(clamped, 1, total));
else if (input === "f") queue.retryFailed();
else if (input === "x") queue.clearHistory();
else if (inActive) {
+2 -2
View File
@@ -177,13 +177,13 @@ export function Results() {
setMode("search");
return;
}
if (key.upArrow) {
if (key.upArrow || input === "k") {
if (results.length > 0 && clamped > 0) setCursor(clamped - 1);
else setMode("search");
return;
}
if (results.length === 0) return;
if (key.downArrow) setCursor(wrapStep(clamped, 1, results.length));
if (key.downArrow || input === "j") setCursor(wrapStep(clamped, 1, results.length));
else if (key.pageUp) setCursor(Math.max(0, clamped - pageJump));
else if (key.pageDown) setCursor(Math.min(results.length - 1, clamped + pageJump));
else if (key.return) {
+2 -2
View File
@@ -48,8 +48,8 @@ export function Seeding() {
useInput(
(input, key) => {
if (key.upArrow) setCursor(wrapStep(clamped, -1, total));
else if (key.downArrow) setCursor(wrapStep(clamped, 1, total));
if (key.upArrow || input === "k") setCursor(wrapStep(clamped, -1, total));
else if (key.downArrow || input === "j") setCursor(wrapStep(clamped, 1, total));
else if (input === "p") {
const h = history[clamped];
if (!h) return;
+3 -3
View File
@@ -37,9 +37,9 @@ export function Sidebar() {
const seeding = queue.seedingCount;
useInput(
(_input, key) => {
if (key.upArrow) setSection(NAV[wrapStep(idx, -1, NAV.length)]!.key);
else if (key.downArrow) setSection(NAV[wrapStep(idx, 1, NAV.length)]!.key);
(input, key) => {
if (key.upArrow || input === "k") setSection(NAV[wrapStep(idx, -1, NAV.length)]!.key);
else if (key.downArrow || input === "j") setSection(NAV[wrapStep(idx, 1, NAV.length)]!.key);
else if (key.return) setRegion("content");
},
{ isActive: focused },
+2 -2
View File
@@ -14,7 +14,7 @@ export const HELP_GROUPS: HelpGroup[] = [
{
title: "Navigate",
hints: [
{ keys: "↑ ↓ ← →", label: "Navigate content and panes" },
{ keys: "↑ ↓ ← →, h j k l", label: "Navigate content and panes" },
{ keys: "↵", label: "Open" },
{ keys: "tab", label: "Switch pane" },
{ keys: "esc", label: "Back" },
@@ -53,7 +53,7 @@ export const HELP_GROUPS: HelpGroup[] = [
// Footer labels stay terse so the contextual hint row never wraps; the `?`
// overlay (HELP_GROUPS) carries the full, descriptive list.
const NAVIGATE: Hint = { keys: "↑↓←→", label: "Move" };
const NAVIGATE: Hint = { keys: "↑↓←→ hjkl", label: "Move" };
const ALWAYS: Hint = { keys: "?", label: "Keys" };