austb-tty-prompt 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +218 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +1132 -0
- data/Rakefile +8 -0
- data/appveyor.yml +23 -0
- data/benchmarks/speed.rb +27 -0
- data/examples/ask.rb +15 -0
- data/examples/collect.rb +19 -0
- data/examples/echo.rb +11 -0
- data/examples/enum.rb +8 -0
- data/examples/enum_paged.rb +9 -0
- data/examples/enum_select.rb +7 -0
- data/examples/expand.rb +29 -0
- data/examples/in.rb +9 -0
- data/examples/inputs.rb +10 -0
- data/examples/key_events.rb +11 -0
- data/examples/keypress.rb +9 -0
- data/examples/mask.rb +13 -0
- data/examples/multi_select.rb +8 -0
- data/examples/multi_select_paged.rb +9 -0
- data/examples/multiline.rb +9 -0
- data/examples/pause.rb +7 -0
- data/examples/select.rb +18 -0
- data/examples/select_paginated.rb +9 -0
- data/examples/slider.rb +6 -0
- data/examples/validation.rb +9 -0
- data/examples/yes_no.rb +7 -0
- data/lib/tty-prompt.rb +4 -0
- data/lib/tty/prompt.rb +535 -0
- data/lib/tty/prompt/answers_collector.rb +59 -0
- data/lib/tty/prompt/choice.rb +90 -0
- data/lib/tty/prompt/choices.rb +110 -0
- data/lib/tty/prompt/confirm_question.rb +129 -0
- data/lib/tty/prompt/converter_dsl.rb +22 -0
- data/lib/tty/prompt/converter_registry.rb +64 -0
- data/lib/tty/prompt/converters.rb +77 -0
- data/lib/tty/prompt/distance.rb +49 -0
- data/lib/tty/prompt/enum_list.rb +337 -0
- data/lib/tty/prompt/enum_paginator.rb +56 -0
- data/lib/tty/prompt/evaluator.rb +29 -0
- data/lib/tty/prompt/expander.rb +292 -0
- data/lib/tty/prompt/keypress.rb +94 -0
- data/lib/tty/prompt/list.rb +317 -0
- data/lib/tty/prompt/mask_question.rb +91 -0
- data/lib/tty/prompt/multi_list.rb +108 -0
- data/lib/tty/prompt/multiline.rb +71 -0
- data/lib/tty/prompt/paginator.rb +88 -0
- data/lib/tty/prompt/question.rb +333 -0
- data/lib/tty/prompt/question/checks.rb +87 -0
- data/lib/tty/prompt/question/modifier.rb +94 -0
- data/lib/tty/prompt/question/validation.rb +72 -0
- data/lib/tty/prompt/reader.rb +352 -0
- data/lib/tty/prompt/reader/codes.rb +121 -0
- data/lib/tty/prompt/reader/console.rb +57 -0
- data/lib/tty/prompt/reader/history.rb +145 -0
- data/lib/tty/prompt/reader/key_event.rb +91 -0
- data/lib/tty/prompt/reader/line.rb +162 -0
- data/lib/tty/prompt/reader/mode.rb +44 -0
- data/lib/tty/prompt/reader/win_api.rb +29 -0
- data/lib/tty/prompt/reader/win_console.rb +53 -0
- data/lib/tty/prompt/result.rb +42 -0
- data/lib/tty/prompt/slider.rb +182 -0
- data/lib/tty/prompt/statement.rb +55 -0
- data/lib/tty/prompt/suggestion.rb +115 -0
- data/lib/tty/prompt/symbols.rb +61 -0
- data/lib/tty/prompt/timeout.rb +69 -0
- data/lib/tty/prompt/utils.rb +44 -0
- data/lib/tty/prompt/version.rb +7 -0
- data/lib/tty/test_prompt.rb +20 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-prompt.gemspec +32 -0
- metadata +243 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
class Reader
|
8
|
+
class Mode
|
9
|
+
# Initialize a Terminal
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
def initialize(input = $stdin)
|
13
|
+
@input = input
|
14
|
+
end
|
15
|
+
|
16
|
+
# Echo given block
|
17
|
+
#
|
18
|
+
# @param [Boolean] is_on
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
def echo(is_on = true, &block)
|
22
|
+
if is_on || !@input.tty?
|
23
|
+
yield
|
24
|
+
else
|
25
|
+
@input.noecho(&block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Use raw mode in the given block
|
30
|
+
#
|
31
|
+
# @param [Boolean] is_on
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def raw(is_on = true, &block)
|
35
|
+
if is_on && @input.tty?
|
36
|
+
@input.raw(&block)
|
37
|
+
else
|
38
|
+
yield
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end # Mode
|
42
|
+
end # Reader
|
43
|
+
end # Prompt
|
44
|
+
end # TTY
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'fiddle'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
class Reader
|
8
|
+
module WinAPI
|
9
|
+
include Fiddle
|
10
|
+
|
11
|
+
Handle = RUBY_VERSION >= "2.0.0" ? Fiddle::Handle : DL::Handle
|
12
|
+
|
13
|
+
CRT_HANDLE = Handle.new("msvcrt") rescue Handle.new("crtdll")
|
14
|
+
|
15
|
+
def getch
|
16
|
+
@@getch ||= Fiddle::Function.new(CRT_HANDLE["_getch"], [], TYPE_INT)
|
17
|
+
@@getch.call
|
18
|
+
end
|
19
|
+
module_function :getch
|
20
|
+
|
21
|
+
def getche
|
22
|
+
@@getche ||= Fiddle::Function.new(CRT_HANDLE["_getche"], [], TYPE_INT)
|
23
|
+
@@getche.call
|
24
|
+
end
|
25
|
+
module_function :getche
|
26
|
+
end # WinAPI
|
27
|
+
end # Reader
|
28
|
+
end # Prompt
|
29
|
+
end # TTY
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'codes'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
class Reader
|
8
|
+
class WinConsole
|
9
|
+
ESC = "\e".freeze
|
10
|
+
NUL_HEX = "\x00".freeze
|
11
|
+
EXT_HEX = "\xE0".freeze
|
12
|
+
|
13
|
+
# Key codes
|
14
|
+
#
|
15
|
+
# @return [Hash[Symbol]]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
attr_reader :keys
|
19
|
+
|
20
|
+
# Escape codes
|
21
|
+
#
|
22
|
+
# @return [Array[Integer]]
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
attr_reader :escape_codes
|
26
|
+
|
27
|
+
def initialize(input)
|
28
|
+
require_relative 'win_api'
|
29
|
+
@input = input
|
30
|
+
@keys = Codes.win_keys
|
31
|
+
@escape_codes = [[NUL_HEX.ord], [ESC.ord], EXT_HEX.bytes.to_a]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a character from console with echo
|
35
|
+
#
|
36
|
+
# @param [Hash[Symbol]] options
|
37
|
+
# @option options [Symbol] :echo
|
38
|
+
# the echo toggle
|
39
|
+
#
|
40
|
+
# @return [String]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def get_char(options)
|
44
|
+
if options[:raw]
|
45
|
+
WinAPI.getch.chr
|
46
|
+
else
|
47
|
+
options[:echo] ? @input.getc : WinAPI.getch.chr
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end # Console
|
51
|
+
end # Reader
|
52
|
+
end # Prompt
|
53
|
+
end # TTY
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
# Accumulates errors
|
6
|
+
class Result
|
7
|
+
attr_reader :question, :value, :errors
|
8
|
+
|
9
|
+
def initialize(question, value, errors = [])
|
10
|
+
@question = question
|
11
|
+
@value = value
|
12
|
+
@errors = errors
|
13
|
+
end
|
14
|
+
|
15
|
+
def with(condition = nil, &block)
|
16
|
+
validator = (condition || block)
|
17
|
+
(new_value, validation_error) = validator.call(question, value)
|
18
|
+
accumulated_errors = errors + Array(validation_error)
|
19
|
+
|
20
|
+
if accumulated_errors.empty?
|
21
|
+
Success.new(question, new_value)
|
22
|
+
else
|
23
|
+
Failure.new(question, new_value, accumulated_errors)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def success?
|
28
|
+
is_a?(Success)
|
29
|
+
end
|
30
|
+
|
31
|
+
def failure?
|
32
|
+
is_a?(Failure)
|
33
|
+
end
|
34
|
+
|
35
|
+
class Success < Result
|
36
|
+
end
|
37
|
+
|
38
|
+
class Failure < Result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end # Prompt
|
42
|
+
end # TTY
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'symbols'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
# A class responsible for shell prompt interactions.
|
7
|
+
class Prompt
|
8
|
+
# A class responsible for gathering numeric input from range
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Slider
|
12
|
+
include Symbols
|
13
|
+
|
14
|
+
HELP = '(Use arrow keys, press Enter to select)'.freeze
|
15
|
+
|
16
|
+
# Initailize a Slider
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(prompt, options = {})
|
20
|
+
@prompt = prompt
|
21
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
22
|
+
@min = options.fetch(:min) { 0 }
|
23
|
+
@max = options.fetch(:max) { 10 }
|
24
|
+
@step = options.fetch(:step) { 1 }
|
25
|
+
@default = options[:default]
|
26
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
27
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
28
|
+
@first_render = true
|
29
|
+
@done = false
|
30
|
+
|
31
|
+
@prompt.subscribe(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Setup initial active position
|
35
|
+
#
|
36
|
+
# @return [Integer]
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
def initial
|
40
|
+
if @default.nil?
|
41
|
+
range.size / 2
|
42
|
+
else
|
43
|
+
range.index(@default)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Range of numbers to render
|
48
|
+
#
|
49
|
+
# @return [Array[Integer]]
|
50
|
+
#
|
51
|
+
# @apip private
|
52
|
+
def range
|
53
|
+
(@min..@max).step(@step).to_a
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api public
|
57
|
+
def default(value)
|
58
|
+
@default = value
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api public
|
62
|
+
def min(value)
|
63
|
+
@min = value
|
64
|
+
end
|
65
|
+
|
66
|
+
# @api public
|
67
|
+
def max(value)
|
68
|
+
@max = value
|
69
|
+
end
|
70
|
+
|
71
|
+
# @api public
|
72
|
+
def step(value)
|
73
|
+
@step = value
|
74
|
+
end
|
75
|
+
|
76
|
+
# Call the slider by passing question
|
77
|
+
#
|
78
|
+
# @param [String] question
|
79
|
+
# the question to ask
|
80
|
+
#
|
81
|
+
# @apu public
|
82
|
+
def call(question, &block)
|
83
|
+
@question = question
|
84
|
+
block.call(self) if block
|
85
|
+
@active = initial
|
86
|
+
render
|
87
|
+
end
|
88
|
+
|
89
|
+
def keyleft(*)
|
90
|
+
@active -= 1 if @active > 0
|
91
|
+
end
|
92
|
+
alias_method :keydown, :keyleft
|
93
|
+
|
94
|
+
def keyright(*)
|
95
|
+
@active += 1 if (@active + @step) < range.size
|
96
|
+
end
|
97
|
+
alias_method :keyup, :keyright
|
98
|
+
|
99
|
+
def keyreturn(*)
|
100
|
+
@done = true
|
101
|
+
end
|
102
|
+
alias_method :keyspace, :keyreturn
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Render an interactive range slider.
|
107
|
+
#
|
108
|
+
# @api private
|
109
|
+
def render
|
110
|
+
@prompt.print(@prompt.hide)
|
111
|
+
until @done
|
112
|
+
question = render_question
|
113
|
+
@prompt.print(question)
|
114
|
+
@prompt.read_keypress
|
115
|
+
refresh(question.lines.count)
|
116
|
+
end
|
117
|
+
@prompt.print(render_question)
|
118
|
+
answer
|
119
|
+
ensure
|
120
|
+
@prompt.print(@prompt.show)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Clear screen
|
124
|
+
#
|
125
|
+
# @param [Integer] lines
|
126
|
+
# the lines to clear
|
127
|
+
#
|
128
|
+
# @api private
|
129
|
+
def refresh(lines)
|
130
|
+
@prompt.print(@prompt.clear_lines(lines))
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Integer]
|
134
|
+
#
|
135
|
+
# @api private
|
136
|
+
def answer
|
137
|
+
range[@active]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Render question with the slider
|
141
|
+
#
|
142
|
+
# @return [String]
|
143
|
+
#
|
144
|
+
# @api private
|
145
|
+
def render_question
|
146
|
+
header = "#{@prefix}#{@question} #{render_header}\n"
|
147
|
+
@first_render = false
|
148
|
+
header << render_slider unless @done
|
149
|
+
header
|
150
|
+
end
|
151
|
+
|
152
|
+
# Render actual answer or help
|
153
|
+
#
|
154
|
+
# @return [String]
|
155
|
+
#
|
156
|
+
# @api private
|
157
|
+
def render_header
|
158
|
+
if @done
|
159
|
+
@prompt.decorate(answer.to_s, @active_color)
|
160
|
+
elsif @first_render
|
161
|
+
@prompt.decorate(HELP, @help_color)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Render slider representation
|
166
|
+
#
|
167
|
+
# @return [String]
|
168
|
+
#
|
169
|
+
# @api private
|
170
|
+
def render_slider
|
171
|
+
output = ''
|
172
|
+
output << symbols[:pipe]
|
173
|
+
output << symbols[:line] * @active
|
174
|
+
output << @prompt.decorate(symbols[:handle], @active_color)
|
175
|
+
output << symbols[:line] * (range.size - @active - 1)
|
176
|
+
output << symbols[:pipe]
|
177
|
+
output << " #{range[@active]}"
|
178
|
+
output
|
179
|
+
end
|
180
|
+
end # Slider
|
181
|
+
end # Prompt
|
182
|
+
end # TTY
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
# A class responsible for shell prompt interactions.
|
5
|
+
class Prompt
|
6
|
+
# A class representing a statement output to prompt.
|
7
|
+
class Statement
|
8
|
+
# Flag to display newline
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
attr_reader :newline
|
12
|
+
|
13
|
+
# Color used to display statement
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
attr_reader :color
|
17
|
+
|
18
|
+
# Initialize a Statement
|
19
|
+
#
|
20
|
+
# @param [TTY::Prompt] prompt
|
21
|
+
#
|
22
|
+
# @param [Hash] options
|
23
|
+
#
|
24
|
+
# @option options [Symbol] :newline
|
25
|
+
# force a newline break after the message
|
26
|
+
#
|
27
|
+
# @option options [Symbol] :color
|
28
|
+
# change the message display to color
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def initialize(prompt, options = {})
|
32
|
+
@prompt = prompt
|
33
|
+
@newline = options.fetch(:newline) { true }
|
34
|
+
@color = options.fetch(:color) { false }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Output the message to the prompt
|
38
|
+
#
|
39
|
+
# @param [String] message
|
40
|
+
# the message to be printed to stdout
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def call(message)
|
44
|
+
message = @prompt.decorate(message, *color) if color
|
45
|
+
|
46
|
+
if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message
|
47
|
+
@prompt.puts message
|
48
|
+
else
|
49
|
+
@prompt.print message
|
50
|
+
@prompt.flush
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end # Statement
|
54
|
+
end # Prompt
|
55
|
+
end # TTY
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'distance'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
# A class responsible for terminal prompt interactions.
|
7
|
+
class Prompt
|
8
|
+
# A class representing a suggestion out of possible choices
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Suggestion
|
12
|
+
DEFAULT_INDENT = 8
|
13
|
+
|
14
|
+
SINGLE_TEXT = 'Did you mean this?'
|
15
|
+
|
16
|
+
PLURAL_TEXT = 'Did you mean one of these?'
|
17
|
+
|
18
|
+
# Number of spaces
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
attr_reader :indent
|
22
|
+
|
23
|
+
# Text for a single suggestion
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
attr_reader :single_text
|
27
|
+
|
28
|
+
# Text for multiple suggestions
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
attr_reader :plural_text
|
32
|
+
|
33
|
+
# Initialize a Suggestion
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def initialize(options = {})
|
37
|
+
@indent = options.fetch(:indent) { DEFAULT_INDENT }
|
38
|
+
@single_text = options.fetch(:single_text) { SINGLE_TEXT }
|
39
|
+
@plural_text = options.fetch(:plural_text) { PLURAL_TEXT }
|
40
|
+
@suggestions = []
|
41
|
+
@comparator = Distance.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# Suggest matches out of possibile strings
|
45
|
+
#
|
46
|
+
# @param [String] message
|
47
|
+
#
|
48
|
+
# @param [Array[String]] possibilities
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def suggest(message, possibilities)
|
52
|
+
distances = measure_distances(message, possibilities)
|
53
|
+
minimum_distance = distances.keys.min
|
54
|
+
max_distance = distances.keys.max
|
55
|
+
|
56
|
+
if minimum_distance < max_distance
|
57
|
+
@suggestions = distances[minimum_distance].sort
|
58
|
+
end
|
59
|
+
evaluate
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Measure distances between messag and possibilities
|
65
|
+
#
|
66
|
+
# @param [String] message
|
67
|
+
#
|
68
|
+
# @param [Array[String]] possibilities
|
69
|
+
#
|
70
|
+
# @return [Hash]
|
71
|
+
#
|
72
|
+
# @api private
|
73
|
+
def measure_distances(message, possibilities)
|
74
|
+
distances = Hash.new { |hash, key| hash[key] = [] }
|
75
|
+
|
76
|
+
possibilities.each do |possibility|
|
77
|
+
distances[@comparator.distance(message, possibility)] << possibility
|
78
|
+
end
|
79
|
+
distances
|
80
|
+
end
|
81
|
+
|
82
|
+
# Build up a suggestion string
|
83
|
+
#
|
84
|
+
# @param [Array[String]] suggestions
|
85
|
+
#
|
86
|
+
# @return [String]
|
87
|
+
#
|
88
|
+
# @api private
|
89
|
+
def evaluate
|
90
|
+
return @suggestions if @suggestions.empty?
|
91
|
+
if @suggestions.one?
|
92
|
+
build_single_suggestion
|
93
|
+
else
|
94
|
+
build_multiple_suggestions
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @api private
|
99
|
+
def build_single_suggestion
|
100
|
+
suggestion = ''
|
101
|
+
suggestion << single_text + "\n"
|
102
|
+
suggestion << (' ' * indent + @suggestions.first)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @api private
|
106
|
+
def build_multiple_suggestions
|
107
|
+
suggestion = ''
|
108
|
+
suggestion << plural_text + "\n"
|
109
|
+
suggestion << @suggestions.map do |sugest|
|
110
|
+
' ' * indent + sugest
|
111
|
+
end.join("\n")
|
112
|
+
end
|
113
|
+
end # Suggestion
|
114
|
+
end # Prompt
|
115
|
+
end # TTY
|