Skip to content

Commit 3c6618a

Browse files
nrjdalalclaude
andcommitted
fix: file preview cursor navigation, dynamic line padding, no empty lines
- Navigate lines with up/down, current line has dark gray background - Line number width adapts to total line count - No empty padding lines when file is shorter than viewport Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 93a1f1b commit 3c6618a

1 file changed

Lines changed: 27 additions & 12 deletions

File tree

bin/utils/interactive-picker.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,10 @@ export function interactivePicker(
421421
content = dim("(unable to read file)")
422422
}
423423

424+
let previewCursor = 0
424425
let previewScrollOffset = 0
425426
const lines = content.split("\n")
427+
const lineNumWidth = String(lines.length).length
426428

427429
function renderPreview() {
428430
const rows = stream.rows || 24
@@ -431,19 +433,34 @@ export function interactivePicker(
431433
const footerLines = 3
432434
const viewportHeight = Math.max(1, rows - headerLines - footerLines)
433435

436+
// Adjust scroll to follow cursor
437+
if (previewCursor < previewScrollOffset) previewScrollOffset = previewCursor
438+
if (previewCursor >= previewScrollOffset + viewportHeight)
439+
previewScrollOffset = previewCursor - viewportHeight + 1
440+
if (previewScrollOffset < 0) previewScrollOffset = 0
441+
434442
let out = "\x1B[H\x1B[2J"
435443
const nameStr =
436444
node.type === "symlink" ? yellow(node.name) + dim(" -> ") + node.linkTarget : node.name
437445
out += `\n ${bold(nameStr)} ${dim(formatSize(node.size))}\n\n`
438446

439-
const visible = lines.slice(previewScrollOffset, previewScrollOffset + viewportHeight)
440-
for (let i = 0; i < viewportHeight; i++) {
441-
if (i < visible.length) {
442-
const lineNum = dim(`${String(previewScrollOffset + i + 1).padStart(4)} `)
443-
out += `${lineNum}${visible[i].slice(0, cols - 5)}\n`
444-
} else {
445-
out += "\n"
447+
const visibleCount = Math.min(viewportHeight, lines.length - previewScrollOffset)
448+
for (let i = 0; i < visibleCount; i++) {
449+
const lineIdx = previewScrollOffset + i
450+
const isCursorLine = lineIdx === previewCursor
451+
const lineNum = dim(`${String(lineIdx + 1).padStart(lineNumWidth)} `)
452+
const lineContent = lines[lineIdx].slice(0, cols - lineNumWidth - 3)
453+
let line = `${lineNum}${lineContent}`
454+
if (isCursorLine) {
455+
const pad = Math.max(0, cols - stripAnsi(line).length)
456+
line = `\x1B[48;5;236m${line}${" ".repeat(pad)}\x1B[49m`
446457
}
458+
out += line + "\n"
459+
}
460+
461+
// Pad remaining
462+
for (let i = visibleCount; i < viewportHeight; i++) {
463+
out += "\n"
447464
}
448465

449466
out += "\n"
@@ -453,7 +470,7 @@ export function interactivePicker(
453470
`${previewScrollOffset + 1}-${Math.min(previewScrollOffset + viewportHeight, lines.length)}/${lines.length}`,
454471
)
455472
: ""
456-
const previewInstructions = dim("↑↓:scroll esc/q:back")
473+
const previewInstructions = dim("↑↓:navigate esc/q:back")
457474
out += scrollInfo
458475
? ` ${scrollInfo} ${previewInstructions}\n`
459476
: ` ${previewInstructions}\n`
@@ -463,8 +480,6 @@ export function interactivePicker(
463480

464481
function onPreviewKey(buf: Buffer) {
465482
const key = buf.toString()
466-
const rows = stream.rows || 24
467-
const viewportHeight = Math.max(1, rows - 6)
468483

469484
if (key === "\x1B" || key === "q" || key === "Q" || key === "\r") {
470485
stdin.removeListener("data", onPreviewKey)
@@ -474,10 +489,10 @@ export function interactivePicker(
474489
}
475490

476491
if (key === "\x1B[A" || key === "k") {
477-
if (previewScrollOffset > 0) previewScrollOffset--
492+
if (previewCursor > 0) previewCursor--
478493
}
479494
if (key === "\x1B[B" || key === "j") {
480-
if (previewScrollOffset < lines.length - viewportHeight) previewScrollOffset++
495+
if (previewCursor < lines.length - 1) previewCursor++
481496
}
482497

483498
renderPreview()

0 commit comments

Comments
 (0)