tty-prompt 0.1.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/.ruby-version +1 -0
- data/.travis.yml +24 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +199 -0
- data/Rakefile +8 -0
- data/lib/tty-prompt.rb +15 -0
- data/lib/tty/prompt.rb +206 -0
- data/lib/tty/prompt/distance.rb +49 -0
- data/lib/tty/prompt/error.rb +26 -0
- data/lib/tty/prompt/history.rb +16 -0
- data/lib/tty/prompt/mode.rb +64 -0
- data/lib/tty/prompt/mode/echo.rb +40 -0
- data/lib/tty/prompt/mode/raw.rb +40 -0
- data/lib/tty/prompt/question.rb +338 -0
- data/lib/tty/prompt/question/modifier.rb +93 -0
- data/lib/tty/prompt/question/validation.rb +92 -0
- data/lib/tty/prompt/reader.rb +113 -0
- data/lib/tty/prompt/response.rb +252 -0
- data/lib/tty/prompt/response_delegation.rb +41 -0
- data/lib/tty/prompt/statement.rb +60 -0
- data/lib/tty/prompt/suggestion.rb +113 -0
- data/lib/tty/prompt/utils.rb +16 -0
- data/lib/tty/prompt/version.rb +7 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/unit/ask_spec.rb +77 -0
- data/spec/unit/distance/distance_spec.rb +75 -0
- data/spec/unit/error_spec.rb +30 -0
- data/spec/unit/question/argument_spec.rb +30 -0
- data/spec/unit/question/character_spec.rb +24 -0
- data/spec/unit/question/default_spec.rb +25 -0
- data/spec/unit/question/in_spec.rb +23 -0
- data/spec/unit/question/initialize_spec.rb +24 -0
- data/spec/unit/question/modifier/apply_to_spec.rb +31 -0
- data/spec/unit/question/modifier/letter_case_spec.rb +22 -0
- data/spec/unit/question/modifier/whitespace_spec.rb +33 -0
- data/spec/unit/question/modify_spec.rb +44 -0
- data/spec/unit/question/valid_spec.rb +46 -0
- data/spec/unit/question/validate_spec.rb +30 -0
- data/spec/unit/question/validation/coerce_spec.rb +24 -0
- data/spec/unit/question/validation/valid_value_spec.rb +22 -0
- data/spec/unit/reader/getc_spec.rb +42 -0
- data/spec/unit/response/read_bool_spec.rb +47 -0
- data/spec/unit/response/read_char_spec.rb +16 -0
- data/spec/unit/response/read_date_spec.rb +20 -0
- data/spec/unit/response/read_email_spec.rb +42 -0
- data/spec/unit/response/read_multiple_spec.rb +23 -0
- data/spec/unit/response/read_number_spec.rb +28 -0
- data/spec/unit/response/read_range_spec.rb +26 -0
- data/spec/unit/response/read_spec.rb +68 -0
- data/spec/unit/response/read_string_spec.rb +19 -0
- data/spec/unit/say_spec.rb +66 -0
- data/spec/unit/statement/initialize_spec.rb +19 -0
- data/spec/unit/suggest_spec.rb +33 -0
- data/spec/unit/warn_spec.rb +30 -0
- data/tasks/console.rake +10 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-prompt.gemspec +26 -0
- metadata +194 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
module ResponseDelegation
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :dispatch, :read,
|
11
|
+
:read_bool,
|
12
|
+
:read_char,
|
13
|
+
:read_choice,
|
14
|
+
:read_date,
|
15
|
+
:read_datetime,
|
16
|
+
:read_email,
|
17
|
+
:read_file,
|
18
|
+
:read_float,
|
19
|
+
:read_input,
|
20
|
+
:read_int,
|
21
|
+
:read_keypress,
|
22
|
+
:read_multiple,
|
23
|
+
:read_password,
|
24
|
+
:read_range,
|
25
|
+
:read_regex,
|
26
|
+
:read_string,
|
27
|
+
:read_symbol,
|
28
|
+
:read_text
|
29
|
+
|
30
|
+
# Create response instance when question readed is invoked
|
31
|
+
#
|
32
|
+
# @param [Response] response
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
def dispatch(response = Response.new(self, shell))
|
36
|
+
@response ||= response
|
37
|
+
end
|
38
|
+
|
39
|
+
end # ResponseDelegation
|
40
|
+
end # Prompt
|
41
|
+
end # TTY
|
@@ -0,0 +1,60 @@
|
|
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 shell.
|
7
|
+
class Statement
|
8
|
+
# @api private
|
9
|
+
attr_reader :shell
|
10
|
+
private :shell
|
11
|
+
|
12
|
+
# Flag to display newline
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
attr_reader :newline
|
16
|
+
|
17
|
+
# Color used to display statement
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
attr_reader :color
|
21
|
+
|
22
|
+
# Initialize a Statement
|
23
|
+
#
|
24
|
+
# @param [TTY::Shell] shell
|
25
|
+
#
|
26
|
+
# @param [Hash] options
|
27
|
+
#
|
28
|
+
# @option options [Symbol] :newline
|
29
|
+
# force a newline break after the message
|
30
|
+
#
|
31
|
+
# @option options [Symbol] :color
|
32
|
+
# change the message display to color
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def initialize(shell = Prompt.new, options = {})
|
36
|
+
@shell = shell
|
37
|
+
@pastel = Pastel.new
|
38
|
+
@newline = options.fetch(:newline, true)
|
39
|
+
@color = options.fetch(:color, false)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Output the message to the shell
|
43
|
+
#
|
44
|
+
# @param [String] message
|
45
|
+
# the message to be printed to stdout
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def declare(message)
|
49
|
+
message = @pastel.decorate message, *color if color
|
50
|
+
|
51
|
+
if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message
|
52
|
+
shell.output.puts message
|
53
|
+
else
|
54
|
+
shell.output.print message
|
55
|
+
shell.output.flush
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end # Statement
|
59
|
+
end # Prompt
|
60
|
+
end # TTY
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty/prompt/distance'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
# A class responsible for shell prompt interactions.
|
7
|
+
class Prompt
|
8
|
+
# A class representing a suggestion
|
9
|
+
class Suggestion
|
10
|
+
DEFAULT_INDENT = 8
|
11
|
+
|
12
|
+
SINGLE_TEXT = 'Did you mean this?'
|
13
|
+
|
14
|
+
PLURAL_TEXT = 'Did you mean one of these?'
|
15
|
+
|
16
|
+
# Number of spaces
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
attr_reader :indent
|
20
|
+
|
21
|
+
# Text for a single suggestion
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
attr_reader :single_text
|
25
|
+
|
26
|
+
# Text for multiple suggestions
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
attr_reader :plural_text
|
30
|
+
|
31
|
+
# Initialize a Suggestion
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def initialize(options = {})
|
35
|
+
@indent = options.fetch(:indent) { DEFAULT_INDENT }
|
36
|
+
@single_text = options.fetch(:single_text) { SINGLE_TEXT }
|
37
|
+
@plural_text = options.fetch(:plural_text) { PLURAL_TEXT }
|
38
|
+
@suggestions = []
|
39
|
+
@comparator = Distance.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# Suggest matches out of possibile strings
|
43
|
+
#
|
44
|
+
# @param [String] message
|
45
|
+
#
|
46
|
+
# @param [Array[String]] possibilities
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def suggest(message, possibilities)
|
50
|
+
distances = measure_distances(message, possibilities)
|
51
|
+
minimum_distance = distances.keys.min
|
52
|
+
max_distance = distances.keys.max
|
53
|
+
|
54
|
+
if minimum_distance < max_distance
|
55
|
+
@suggestions = distances[minimum_distance].sort
|
56
|
+
end
|
57
|
+
evaluate
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Measure distances between messag and possibilities
|
63
|
+
#
|
64
|
+
# @param [String] message
|
65
|
+
#
|
66
|
+
# @param [Array[String]] possibilities
|
67
|
+
#
|
68
|
+
# @return [Hash]
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
def measure_distances(message, possibilities)
|
72
|
+
distances = Hash.new { |hash, key| hash[key] = [] }
|
73
|
+
|
74
|
+
possibilities.each do |possibility|
|
75
|
+
distances[@comparator.distance(message, possibility)] << possibility
|
76
|
+
end
|
77
|
+
distances
|
78
|
+
end
|
79
|
+
|
80
|
+
# Build up a suggestion string
|
81
|
+
#
|
82
|
+
# @param [Array[String]] suggestions
|
83
|
+
#
|
84
|
+
# @return [String]
|
85
|
+
#
|
86
|
+
# @api private
|
87
|
+
def evaluate
|
88
|
+
return @suggestions if @suggestions.empty?
|
89
|
+
if @suggestions.one?
|
90
|
+
build_single_suggestion
|
91
|
+
else
|
92
|
+
build_multiple_suggestions
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# @api private
|
97
|
+
def build_single_suggestion
|
98
|
+
suggestion = ''
|
99
|
+
suggestion << single_text + "\n"
|
100
|
+
suggestion << (' ' * indent + @suggestions.first)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def build_multiple_suggestions
|
105
|
+
suggestion = ''
|
106
|
+
suggestion << plural_text + "\n"
|
107
|
+
suggestion << @suggestions.map do |sugest|
|
108
|
+
' ' * indent + sugest
|
109
|
+
end.join("\n")
|
110
|
+
end
|
111
|
+
end # Suggestion
|
112
|
+
end # Prompt
|
113
|
+
end # TTY
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
module Utils
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def extract_options!(args)
|
8
|
+
args.last.respond_to?(:to_hash) ? args.pop : {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def extract_options(args)
|
12
|
+
options = args.last
|
13
|
+
options.respond_to?(:to_hash) ? options.to_hash.dup : {}
|
14
|
+
end
|
15
|
+
end # Utils
|
16
|
+
end # TTY
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS'])
|
4
|
+
require 'simplecov'
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
9
|
+
Coveralls::SimpleCov::Formatter
|
10
|
+
]
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
command_name 'spec'
|
14
|
+
add_filter 'spec'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'tty-prompt'
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.expect_with :rspec do |expectations|
|
22
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
23
|
+
end
|
24
|
+
|
25
|
+
config.mock_with :rspec do |mocks|
|
26
|
+
mocks.verify_partial_doubles = true
|
27
|
+
end
|
28
|
+
|
29
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
30
|
+
config.disable_monkey_patching!
|
31
|
+
|
32
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
33
|
+
# be too noisy due to issues in dependencies.
|
34
|
+
config.warnings = true
|
35
|
+
|
36
|
+
if config.files_to_run.one?
|
37
|
+
config.default_formatter = 'doc'
|
38
|
+
end
|
39
|
+
|
40
|
+
config.profile_examples = 2
|
41
|
+
|
42
|
+
config.order = :random
|
43
|
+
|
44
|
+
Kernel.srand config.seed
|
45
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe TTY::Prompt, '#ask' do
|
6
|
+
let(:input) { StringIO.new }
|
7
|
+
let(:output) { StringIO.new }
|
8
|
+
let(:prefix) { '' }
|
9
|
+
let(:options) { { prefix: prefix } }
|
10
|
+
|
11
|
+
subject(:prompt) { TTY::Prompt.new(input, output, options) }
|
12
|
+
|
13
|
+
it 'prints message' do
|
14
|
+
prompt.ask "What is your name?"
|
15
|
+
expect(output.string).to eql "What is your name?\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'prints an empty message ' do
|
19
|
+
prompt.ask ""
|
20
|
+
expect(output.string).to eql ""
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'prints an empty message and returns nil if EOF is sent to stdin' do
|
24
|
+
input << nil
|
25
|
+
input.rewind
|
26
|
+
q = prompt.ask ""
|
27
|
+
expect(q.read).to eql nil
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with a prompt prefix' do
|
31
|
+
let(:prefix) { ' > ' }
|
32
|
+
|
33
|
+
it "asks a question with '>'" do
|
34
|
+
input << ''
|
35
|
+
input.rewind
|
36
|
+
prompt.ask "Are you Polish?"
|
37
|
+
expect(output.string).to eql " > Are you Polish?\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'asks a question with block' do
|
42
|
+
input << ''
|
43
|
+
input.rewind
|
44
|
+
q = prompt.ask "What is your name?" do
|
45
|
+
default 'Piotr'
|
46
|
+
end
|
47
|
+
expect(q.read).to eql "Piotr"
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'yes?' do
|
51
|
+
it 'agrees' do
|
52
|
+
input << 'yes'
|
53
|
+
input.rewind
|
54
|
+
expect(prompt.yes?("Are you a human?")).to eq(true)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'disagrees' do
|
58
|
+
input << 'no'
|
59
|
+
input.rewind
|
60
|
+
expect(prompt.yes?("Are you a human?")).to eq(false)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'no?' do
|
65
|
+
it 'agrees' do
|
66
|
+
input << 'no'
|
67
|
+
input.rewind
|
68
|
+
expect(prompt.no?("Are you a human?")).to eq(true)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'disagrees' do
|
72
|
+
input << 'yes'
|
73
|
+
input.rewind
|
74
|
+
expect(prompt.no?("Are you a human?")).to eq(false)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe TTY::Prompt::Distance, '.distance' do
|
6
|
+
let(:object) { described_class.new }
|
7
|
+
|
8
|
+
subject(:distance) { object.distance(*strings) }
|
9
|
+
|
10
|
+
context 'when nil' do
|
11
|
+
let(:strings) { [nil, nil] }
|
12
|
+
|
13
|
+
it { is_expected.to eql(0) }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when empty' do
|
17
|
+
let(:strings) { ['', ''] }
|
18
|
+
|
19
|
+
it { is_expected.to eql(0) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'with one non empty' do
|
23
|
+
let(:strings) { ['abc', ''] }
|
24
|
+
|
25
|
+
it { is_expected.to eql(3) }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when single char' do
|
29
|
+
let(:strings) { ['a', 'abc'] }
|
30
|
+
|
31
|
+
it { is_expected.to eql(2) }
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when similar' do
|
35
|
+
let(:strings) { ['abc', 'abc'] }
|
36
|
+
|
37
|
+
it { is_expected.to eql(0) }
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when similar' do
|
41
|
+
let(:strings) { ['abc', 'acb'] }
|
42
|
+
|
43
|
+
it { is_expected.to eql(1) }
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when end similar' do
|
47
|
+
let(:strings) { ['saturday', 'sunday'] }
|
48
|
+
|
49
|
+
it { is_expected.to eql(3) }
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when contain similar' do
|
53
|
+
let(:strings) { ['which', 'witch'] }
|
54
|
+
|
55
|
+
it { is_expected.to eql(2) }
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when prefix' do
|
59
|
+
let(:strings) { ['sta', 'status'] }
|
60
|
+
|
61
|
+
it { is_expected.to eql(3) }
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when similar' do
|
65
|
+
let(:strings) { ['smellyfish','jellyfish'] }
|
66
|
+
|
67
|
+
it { is_expected.to eql(2) }
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when unicode' do
|
71
|
+
let(:strings) { ['マラソン五輪代表', 'ララソン五輪代表'] }
|
72
|
+
|
73
|
+
it { is_expected.to eql(1) }
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe TTY::Prompt, '.error' do
|
6
|
+
let(:input) { StringIO.new }
|
7
|
+
let(:output) { StringIO.new }
|
8
|
+
let(:color) { Pastel.new(enabled: true) }
|
9
|
+
|
10
|
+
subject(:prompt) { described_class.new(input, output) }
|
11
|
+
|
12
|
+
before { allow(Pastel).to receive(:new).and_return(color) }
|
13
|
+
|
14
|
+
after { output.rewind }
|
15
|
+
|
16
|
+
it 'displays one message' do
|
17
|
+
prompt.error "Nothing is fine!"
|
18
|
+
expect(output.string).to eql "\e[31mNothing is fine!\e[0m\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'displays many messages' do
|
22
|
+
prompt.error "Nothing is fine!", "All is broken!"
|
23
|
+
expect(output.string).to eql "\e[31mNothing is fine!\e[0m\n\e[31mAll is broken!\e[0m\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'displays message with option' do
|
27
|
+
prompt.error "Nothing is fine!", newline: false
|
28
|
+
expect(output.string).to eql "\e[31mNothing is fine!\e[0m"
|
29
|
+
end
|
30
|
+
end
|