@@ -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