austb-tty-prompt 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|