Skip to content

Commit dfb024d

Browse files
mpluspclaude
andcommitted
feat: Add full navigation support to PackMenu
- Implement arrow key (↑↓) and j/k navigation with visual cursor - Add visual cursor indicator (► prefix) showing current selection - Support Enter key selection alongside number keys - Update help text to show navigation instructions - Ensure consistent behavior across all floating window dialogs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7009b44 commit dfb024d

1 file changed

Lines changed: 69 additions & 36 deletions

File tree

lua/pack-manager/ui.lua

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)