From 6ba3e4292b3369e439573e397e805ec86824cd22 Mon Sep 17 00:00:00 2001
From: Avinash Gundimeda <23102a020054@mbu.asia>
Date: Wed, 1 Jul 2026 12:06:45 +0530
Subject: [PATCH] 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.
---
preview/browse.svg | 24 ++++++++++++++----------
preview/downloads.svg | 6 +++---
src/ui/App.tsx | 4 ++--
src/ui/components/Downloads.tsx | 4 ++--
src/ui/components/Results.tsx | 4 ++--
src/ui/components/Seeding.tsx | 4 ++--
src/ui/components/Sidebar.tsx | 6 +++---
src/ui/keymap.ts | 4 ++--
8 files changed, 30 insertions(+), 26 deletions(-)
diff --git a/preview/browse.svg b/preview/browse.svg
index d400c2e..a410adc 100644
--- a/preview/browse.svg
+++ b/preview/browse.svg
@@ -107,15 +107,19 @@
SUB
╰───────────────────────────────────────────────────────────╯
- d
- Download
- /
- Search
- m
- Paste magnet
- tab
- Switch pane
- ?
- Keys
+ ↑↓←→ hjkl
+ Move
+ d
+ Download
+ y
+ Copy
+ s
+ Sort
+ /
+ Search
+ tab
+ Switch
+ ?
+ Keys
\ No newline at end of file
diff --git a/preview/downloads.svg b/preview/downloads.svg
index 3f7c54f..7c8c339 100644
--- a/preview/downloads.svg
+++ b/preview/downloads.svg
@@ -105,9 +105,9 @@
c
Cancel
tab
- Switch pane
- ?
- Keys
+ Switch
+ ?
+ Keys
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index f028f9e..b9122b5 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -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;
}
diff --git a/src/ui/components/Downloads.tsx b/src/ui/components/Downloads.tsx
index 1a10d2a..99bc965 100644
--- a/src/ui/components/Downloads.tsx
+++ b/src/ui/components/Downloads.tsx
@@ -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) {
diff --git a/src/ui/components/Results.tsx b/src/ui/components/Results.tsx
index 1ef3b78..7e13349 100644
--- a/src/ui/components/Results.tsx
+++ b/src/ui/components/Results.tsx
@@ -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) {
diff --git a/src/ui/components/Seeding.tsx b/src/ui/components/Seeding.tsx
index 0dab2a3..258b74a 100644
--- a/src/ui/components/Seeding.tsx
+++ b/src/ui/components/Seeding.tsx
@@ -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;
diff --git a/src/ui/components/Sidebar.tsx b/src/ui/components/Sidebar.tsx
index d3681a3..5216250 100644
--- a/src/ui/components/Sidebar.tsx
+++ b/src/ui/components/Sidebar.tsx
@@ -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 },
diff --git a/src/ui/keymap.ts b/src/ui/keymap.ts
index 8729cd0..b6719e8 100644
--- a/src/ui/keymap.ts
+++ b/src/ui/keymap.ts
@@ -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" };