Search code examples
rubycurses

How to prevent having to get input twice in Curses navigation menu?


I have a menu I made in Curses in which the user can use the 'w' and 's' keys or up and down arrows on the keyboard to move up and down in the Curses menu.

Once the Enter key is pressed, the system command 'ls' is called.

The problem I am having is when the user tries to move to or from the 'Option 0' line, they have to press the arrow key twice. I only want them to have to press Enter once just like they can with the other options.

Is there a better way I can write the below so that the user only has to press the arrow keys once to move to a different option?

  if position == 0
    draw_info menu, 'You selected option 0'
    input = menu.getch
    if input == 13 # Curses recognizes 13 as Enter being pressed

        Curses.close_screen
        system "clear" or system "cls"
        system 'ls'
        puts "\n\nPress Enter to continue."

Here is my code:

require 'curses'
include Curses

Curses.init_screen
Curses.curs_set(0)  # Invisible cursor

Curses.start_color

Curses.noecho # echo or noecho to display user input
Curses.cbreak # do not buffer commands until Enter is pressed
Curses.raw # disable interpretation of keyboard input
Curses.nonl
Curses.stdscr.nodelay = 1


begin


  # Building a static window

    def draw_menu(menu, active_index=nil)
      ["This is option 0.", "This is option 1.", "This is option 2.", "This is option 3."].each_with_index do |element, index|
      # "w" for word array
      # It's a shortcut for arrays
        menu.setpos(index + 1, 1)
        menu.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
        menu.addstr("#{index} - %-10s" % element)   # %-Xs makes sure array words line up evenly if you place index after element
                                                    # you can change 17 to another number
      end
      menu.setpos(5, 1)
    end

    def draw_info(menu, text)
      menu.setpos(6, 1)  # sets the position of move up and down
                         # for example, menu.setpos(1, 10) moves to another
                         # location
      menu.attrset(A_NORMAL)
      menu.addstr text
    end

    position = 0

    menu = Window.new(20, 70, 2, 2)  # (height, width, top, left)
    menu.keypad = true  # enable keypad which allows arrow keys
    #menu.box('|', '-')
    draw_menu(menu, position)
    while ch = menu.getch
      stdscr.keypad = true
      case ch
      when KEY_UP, 'w'
        #draw_info menu, 'move up'
        position -= 1
      when KEY_DOWN, 's'
        #draw_info menu, 'move down'
        position += 1
      when 'x'
        exit
      end
      position = 3 if position < 0
      position = 0 if position > 3
      draw_menu(menu, position)
      if position == 0
        draw_info menu, 'You selected option 0'
        input = menu.getch
        if input == 13 # Curses recognizes 13 as Enter being pressed

            Curses.close_screen
            system "clear" or system "cls"
            system 'ls'
            puts "\n\nPress Enter to continue."

        end
      elsif position == 1
        draw_info menu, 'You selected option 1'
      elsif position == 2
        draw_info menu, 'You selected option 2'
      else position == 3
        draw_info menu, 'You selected option 3'
      end       
    end

rescue => ex
  Curses.close_screen
end

EDIT

This will allow the user to only have to hit the arrow key once but Curses.close_screen doesn't close the screen so I can run 'ls' properly.

require 'curses'
include Curses

Curses.init_screen
Curses.curs_set(0)  # Invisible cursor

Curses.start_color

Curses.noecho # echo or noecho to display user input
Curses.cbreak # do not buffer commands until Enter is pressed
Curses.raw # disable interpretation of keyboard input
Curses.nonl
Curses.stdscr.nodelay = 1


begin


  # Building a static window

    def draw_menu(menu, active_index=nil)
      ["This is option 0.", "This is option 1.", "This is option 2.", "This is option 3."].each_with_index do |element, index|
      # "w" for word array
      # It's a shortcut for arrays
        menu.setpos(index + 1, 1)
        menu.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
        menu.addstr("#{index} - %-10s" % element)   # %-Xs makes sure array words line up evenly if you place index after element
                                                    # you can change 17 to another number
      end
      menu.setpos(5, 1)
    end

    def draw_info(menu, text)
      menu.setpos(6, 1)  # sets the position of move up and down
                         # for example, menu.setpos(1, 10) moves to another
                         # location
      menu.attrset(A_NORMAL)
      menu.addstr text
    end

    position = 0

    menu = Window.new(20, 70, 2, 2)  # (height, width, top, left)
    menu.keypad = true  # enable keypad which allows arrow keys
    #menu.box('|', '-')
    draw_menu(menu, position)
    while ch = menu.getch
      stdscr.keypad = true
      case ch
      when KEY_UP, 'w'
        #draw_info menu, 'move up'
        position -= 1
      when KEY_DOWN, 's'
        #draw_info menu, 'move down'
        position += 1
      when 13
        if position.zero?
          # draw_info menu, "You hit enter."
          Curses.close_screen
          system "clear" or system "cls"
          system 'ls'
          puts "\n\nPress Enter to continue."
        end
      when 'x'
        exit
      end
      position = 3 if position < 0
      position = 0 if position > 3
      draw_menu(menu, position)
      draw_info menu, "#{position}"   
    end

rescue => ex
  Curses.close_screen
end

Solution

  • Why not move the capture for the Enter into the case statement with the rest of the keys?

    while ch = menu.getch
      case ch
      when KEY_UP, 'w'
        position -= 1
      when KEY_DOWN, 's'
        position += 1
      when 13
        if position.zero?
          Curses.close_screen
          system "clear" or system "cls"
          system 'ls'
          puts "\n\nPress Enter to continue."
          gets # waits for the user to press enter
        end
      when 'x'
        exit
      end
    
      position = 3 if position < 0
      position = 0 if position > 3
      draw_menu(menu, position)
    
      if position == 0
        draw_info menu, 'You selected option 0'
      elsif position == 1
        draw_info menu, 'You selected option 1'
      elsif position == 2
        draw_info menu, 'You selected option 2'
      else position == 3
        draw_info menu, 'You selected option 3'
      end
    end
    

    The reason you had to press 2 keys to get off of the first menu option is because when you 'entered' the first menu option (by making position equal to 0), you would then prompt for another character (through menu.getch) that was outside of the main loop. So you needed to essentially 'cancel' off the first option, and then you could move about freely.

    Doing it this way, you move onto the first menu option, continue with the stuff that all menu options needed to do, (the draw_info calls) and then listen for another key in the main loop. Here, they can use an arrow key to move immediately off the option or press Enter. If they ever press Enter, you then check to see if they are on the correct menu item, if they are, then do your ls call, otherwise just ignore the key and go on as usual.

    Also that if/else chain at the end could just be a single call:

    draw_info menu, "You selected option #{position}"