@@ -328,32 +328,43 @@ function M.menu()
328328 max_desc_width = math.max (max_desc_width , # option .desc )
329329 end
330330
331- local content_width = 4 + max_label_width + 3 + max_desc_width -- "1. " + label + " - " + desc
331+ local content_width = 6 + max_label_width + 3 + max_desc_width -- "► 1. " + label + " - " + desc
332332 local width = math.max (60 , content_width + 6 ) -- padding
333333 local height = # menu_options + 6 -- options + header + footer + padding
334334
335335 local buf , win = create_centered_window (width , height , " Pack Manager" )
336336
337- -- Build content
338- local content = {
339- " Choose an action:" ,
340- " " ,
341- }
342-
343- for _ , option in ipairs (menu_options ) do
344- local line = string.format (" %s. %-" .. max_label_width .. " s - %s" ,
345- option .key , option .label , option .desc )
346- table.insert (content , line )
337+ -- Header lines
338+ local header_lines = {" Choose an action:" , " " }
339+
340+ -- Current selection
341+ local current_index = 1
342+
343+ -- Update function to show cursor
344+ local function update_display ()
345+ local content = {}
346+ vim .list_extend (content , header_lines )
347+
348+ for i , option in ipairs (menu_options ) do
349+ local prefix = i == current_index and " ► " or " "
350+ local line = string.format (" %s%s. %-" .. max_label_width .. " s - %s" ,
351+ prefix , option .key , option .label , option .desc )
352+ table.insert (content , line )
353+ end
354+
355+ table.insert (content , " " )
356+ table.insert (content , " Use ↑↓ or j/k to navigate, Enter to select, number key for quick select, q/Esc to quit" )
357+
358+ vim .api .nvim_buf_set_option (buf , ' modifiable' , true )
359+ vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , content )
360+ vim .api .nvim_buf_set_option (buf , ' modifiable' , false )
361+
362+ -- Position cursor on current option
363+ vim .api .nvim_win_set_cursor (win , {2 + current_index , 0 })
347364 end
348-
349- table.insert (content , " " )
350- table.insert (content , " Press number key to select, q/Esc to quit" )
351-
352- vim .api .nvim_buf_set_lines (buf , 0 , - 1 , false , content )
353- vim .api .nvim_buf_set_option (buf , ' modifiable' , false )
354-
355- -- Position cursor on first option
356- vim .api .nvim_win_set_cursor (win , {3 , 0 })
365+
366+ -- Initial display
367+ update_display ()
357368
358369 local result = nil -- luacheck: ignore 311
359370
@@ -364,34 +375,56 @@ function M.menu()
364375 end
365376
366377 -- Use vim.fn.getchar() to wait for input properly
378+ local done = false
367379 local key
368380 repeat
369381 vim .cmd (' redraw' )
370382 key = vim .fn .getchar ()
371383
372384 -- Handle different key types
373385 if type (key ) == " string" then
374- -- For menu dialog, handle single ESC but ignore arrow keys
386+ -- Handle special keys (arrow keys come as strings)
375387 local bytes = {key :byte (1 , - 1 )}
376- if # bytes == 1 and bytes [1 ] == 27 then
388+ if # bytes == 3 and bytes [1 ] == 27 and bytes [2 ] == 91 then
389+ -- ESC[A = Up, ESC[B = Down
390+ if bytes [3 ] == 65 then -- Up arrow
391+ current_index = math.max (current_index - 1 , 1 )
392+ update_display ()
393+ elseif bytes [3 ] == 66 then -- Down arrow
394+ current_index = math.min (current_index + 1 , # menu_options )
395+ update_display ()
396+ end
397+ elseif # bytes == 1 and bytes [1 ] == 27 then
398+ -- Single ESC
377399 result = nil
378- break
400+ done = true
379401 end
380- -- Ignore other escape sequences like arrow keys
381- elseif type (key ) == " number" and key >= string.byte (' 1' ) and key <= string.byte (' 8' ) then
382- local selected = key - string.byte (' 0' )
383- if selected <= # menu_options then
384- result = menu_options [selected ].action
385- break
402+ elseif type (key ) == " number" then
403+ -- Handle regular keys
404+ if key == string.byte (' j' ) or key == string.byte (' J' ) then
405+ current_index = math.min (current_index + 1 , # menu_options )
406+ update_display ()
407+ elseif key == string.byte (' k' ) or key == string.byte (' K' ) then
408+ current_index = math.max (current_index - 1 , 1 )
409+ update_display ()
410+ elseif key >= string.byte (' 1' ) and key <= string.byte (' 8' ) then
411+ local selected = key - string.byte (' 0' )
412+ if selected <= # menu_options then
413+ result = menu_options [selected ].action
414+ done = true
415+ end
416+ elseif key == 13 then -- Enter key
417+ result = menu_options [current_index ].action
418+ done = true
419+ elseif key == 27 then -- Escape key
420+ result = nil
421+ done = true
422+ elseif key == string.byte (' q' ) or key == string.byte (' Q' ) then
423+ result = nil
424+ done = true
386425 end
387- elseif type (key ) == " number" and key == 27 then -- Escape key
388- result = nil
389- break
390- elseif type (key ) == " number" and (key == string.byte (' q' ) or key == string.byte (' Q' )) then
391- result = nil
392- break
393426 end
394- until false
427+ until done
395428
396429 close_window (win )
397430 return result
0 commit comments