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 |