Usage:
puts "What is your favourite colour?"
InteractivePrompt.call(%w(red green blue yellow purple orange))Renders this:
| class Cursor | |
| CONTROL_SEQ = "\e[".freeze | |
| class << self | |
| def show | |
| "#{CONTROL_SEQ}?25h" | |
| end | |
| def hide | |
| "#{CONTROL_SEQ}?25l" | |
| end | |
| def move(dir, n = 1) | |
| mappings = { | |
| up: 'A', | |
| down: 'B', | |
| left: 'D', | |
| right: 'C', | |
| } | |
| [CONTROL_SEQ, n.to_s, mappings[dir]].join | |
| end | |
| def next_line | |
| move(:down, '') + CONTROL_SEQ + "1G" | |
| end | |
| def previous_line | |
| move(:up, '') + CONTROL_SEQ + "1G" | |
| end | |
| def end_of_previous_line | |
| previous_line + move(:right, "\033[") | |
| end | |
| def clear(n = 1) | |
| n.times.reduce('') do |acc, idx| | |
| mover = idx == n - 1 ? '' : move(:up) | |
| clear = CONTROL_SEQ + '2K' + CONTROL_SEQ + "1G" | |
| acc << clear + mover | |
| end | |
| end | |
| end | |
| end |
| require 'io/console' | |
| require_relative 'cursor' | |
| class InteractivePrompt | |
| def self.call(options) | |
| list = self.new(options) | |
| options[list.call - 1] | |
| end | |
| def initialize(options) | |
| @options = options | |
| @active = 1 | |
| @marker = '>' | |
| @answer = nil | |
| end | |
| def call | |
| print(Cursor.hide) | |
| while @answer.nil? | |
| render_options | |
| get_input | |
| print(Cursor.clear(@options.size + 1)) | |
| print(Cursor.end_of_previous_line + "\n") | |
| end | |
| render_options | |
| @answer | |
| ensure | |
| print(Cursor.show) | |
| print(Cursor.end_of_previous_line + "\n") | |
| end | |
| private | |
| def get_input | |
| case read_char | |
| when "\e[A", 'h' # up | |
| @active = @active - 1 % @options.length | |
| when "\e[B", 'j' # down | |
| @active = @active + 1 % @options.length | |
| when " ", "\r" # enter/select | |
| @answer = @active | |
| when "\u0003", "\e" # Control-C or escape | |
| exit 0 | |
| end | |
| end | |
| # Will handle 2-3 character sequences like arrow keys and control-c | |
| def read_char | |
| STDIN.echo = false | |
| STDIN.raw! | |
| input = STDIN.getc.chr | |
| return input unless input == "\e" | |
| input << STDIN.read_nonblock(3) rescue nil | |
| input << STDIN.read_nonblock(2) rescue nil | |
| ensure | |
| STDIN.echo = true | |
| STDIN.cooked! | |
| return input | |
| end | |
| def render_options | |
| @options.each_with_index do |choice, index| | |
| num = index + 1 | |
| message = " #{num.to_s}. #{choice}" | |
| message = "\x1b[36> #{message.strip}\x1b[0" if num == @active | |
| puts message | |
| end | |
| end | |
| end |