@@ -370,3 +370,110 @@ func redrawMultiSelect(title string, items []MultiSelectItem, cursor int, totalL
370370 }
371371 fmt .Print (color .New (color .Faint ).Sprint ("↑/k up ↓/j down space select enter confirm q cancel" ))
372372}
373+
374+ // promptSingleSelect displays a navigable single-select list
375+ // Navigation: arrow keys or hjkl, enter to select, q to cancel
376+ // Returns selected item, whether user confirmed (false if cancelled), error
377+ func promptSingleSelect (title string , items []string ) (string , bool , error ) {
378+ if len (items ) == 0 {
379+ return "" , false , fmt .Errorf ("no items provided" )
380+ }
381+
382+ cursor := 0
383+ totalLines := len (items ) + 2 // title + items + help line
384+
385+ // Print initial display
386+ printSingleSelect (title , items , cursor )
387+
388+ for {
389+ // Get terminal into raw mode for reading input
390+ oldState , err := term .MakeRaw (int (os .Stdin .Fd ()))
391+ if err != nil {
392+ return "" , false , fmt .Errorf ("failed to set raw mode: %w" , err )
393+ }
394+
395+ // Read input
396+ b := make ([]byte , 3 ) // Up to 3 bytes for arrow keys
397+ n , err := os .Stdin .Read (b )
398+
399+ // Restore terminal before processing
400+ term .Restore (int (os .Stdin .Fd ()), oldState )
401+
402+ if err != nil {
403+ return "" , false , fmt .Errorf ("failed to read input: %w" , err )
404+ }
405+
406+ needsRedraw := false
407+
408+ // Handle input
409+ if n == 1 {
410+ switch b [0 ] {
411+ case 'q' , 27 , 3 : // q, ESC, or Ctrl+C
412+ fmt .Println ()
413+ return "" , false , nil
414+ case '\r' , '\n' : // Enter - select current item
415+ fmt .Println ()
416+ return items [cursor ], true , nil
417+ case 'k' : // Vim up
418+ if cursor > 0 {
419+ cursor --
420+ needsRedraw = true
421+ }
422+ case 'j' : // Vim down
423+ if cursor < len (items )- 1 {
424+ cursor ++
425+ needsRedraw = true
426+ }
427+ }
428+ } else if n == 3 && b [0 ] == 27 && b [1 ] == 91 {
429+ // Arrow keys: ESC [ A/B/C/D
430+ switch b [2 ] {
431+ case 65 : // Up arrow
432+ if cursor > 0 {
433+ cursor --
434+ needsRedraw = true
435+ }
436+ case 66 : // Down arrow
437+ if cursor < len (items )- 1 {
438+ cursor ++
439+ needsRedraw = true
440+ }
441+ }
442+ }
443+
444+ if needsRedraw {
445+ redrawSingleSelect (title , items , cursor , totalLines )
446+ }
447+ }
448+ }
449+
450+ // printSingleSelect prints the initial single-select display
451+ func printSingleSelect (title string , items []string , cursor int ) {
452+ fmt .Println (title )
453+ for i , item := range items {
454+ prefix := " "
455+ if i == cursor {
456+ prefix = color .CyanString ("> " )
457+ }
458+ fmt .Printf ("%s%s\n " , prefix , item )
459+ }
460+ fmt .Print (color .New (color .Faint ).Sprint ("↑/k up ↓/j down enter select q cancel" ))
461+ }
462+
463+ // redrawSingleSelect redraws the single-select display (called with terminal in normal mode)
464+ func redrawSingleSelect (title string , items []string , cursor int , totalLines int ) {
465+ // Move cursor up to the title line and clear to end of screen
466+ fmt .Printf ("\033 [%dF" , totalLines - 1 )
467+ fmt .Print ("\033 [J" )
468+
469+ // Redraw everything
470+ fmt .Println (title )
471+ for i , item := range items {
472+ prefix := " "
473+ if i == cursor {
474+ prefix = color .CyanString ("> " )
475+ }
476+ fmt .Printf ("%s%s\n " , prefix , item )
477+ }
478+ fmt .Print (color .New (color .Faint ).Sprint ("↑/k up ↓/j down enter select q cancel" ))
479+ }
0 commit comments