Ansi_menu

Wrote this trying NOT to use libraries like ‘(n)curses’ or ‘io/console’.
The key-handling became way to involved.. so I decided to use tty-reader for that.

require 'tty-reader'

# get cursor pos via sending Esc[6n
def pos
  res = ''
  $stdin.raw do |stdin|
    $stdout << "\e[6n"
    $stdout.flush
    while (c = stdin.getc) != 'R'
      res << c if c
    end
  end

  # named matches, return hash
  m = res.match /(?<ypos>\d+);(?<xpos>\d+)/
  {:y => Integer(m[:ypos]), :x => Integer(m[:xpos])}
end

# menu
def select(menu)
  rd = TTY::Reader.new
  row, pos = pos()[:y] - menu.size, 0

  menu.each_with_index {|item, idx|
    puts '%s %s' % [(idx==0) ? '>' : ' ', item]
  }

  # lambdas
  cur = ->(b) { print b ? "\e[?25h" : "\e[?25l"}
  cur_goto = ->(row=0,col=0) { print "\e[#{row};#{col}H" }
  pat = ->(r, c, txt) { cur_goto[r, c]; print txt }
  res = -> { cur_goto[row + menu.size, 0]; cur[true] } 
  
  # keys
  rd.on(:keydown, :keyup) do |ev|
    pat[pos+row, 0, '  %s' % menu[pos]]
    pos = (pos += (ev.key.name == :down) ? 1 : -1) % menu.size
    pat[pos+row, 0, '>']
  end
    .on(:keyescape) { res[]; return nil }
    .on(:keyreturn) { res[]; return menu[pos] }

  cur[false]
  
  # loop
  loop { ch = rd.read_keypress }
end

# -----------------------------------------------------------------------------

# usage :
# move cursor             => arrow keys (up / down)
# make selection          => Return
# quit without selecting  => Esc

arr = %w(one two three four five end)
puts 'Selected: %s' % [select(arr) || 'none']