tty-prompt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +24 -0
  6. data/Gemfile +16 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +199 -0
  9. data/Rakefile +8 -0
  10. data/lib/tty-prompt.rb +15 -0
  11. data/lib/tty/prompt.rb +206 -0
  12. data/lib/tty/prompt/distance.rb +49 -0
  13. data/lib/tty/prompt/error.rb +26 -0
  14. data/lib/tty/prompt/history.rb +16 -0
  15. data/lib/tty/prompt/mode.rb +64 -0
  16. data/lib/tty/prompt/mode/echo.rb +40 -0
  17. data/lib/tty/prompt/mode/raw.rb +40 -0
  18. data/lib/tty/prompt/question.rb +338 -0
  19. data/lib/tty/prompt/question/modifier.rb +93 -0
  20. data/lib/tty/prompt/question/validation.rb +92 -0
  21. data/lib/tty/prompt/reader.rb +113 -0
  22. data/lib/tty/prompt/response.rb +252 -0
  23. data/lib/tty/prompt/response_delegation.rb +41 -0
  24. data/lib/tty/prompt/statement.rb +60 -0
  25. data/lib/tty/prompt/suggestion.rb +113 -0
  26. data/lib/tty/prompt/utils.rb +16 -0
  27. data/lib/tty/prompt/version.rb +7 -0
  28. data/spec/spec_helper.rb +45 -0
  29. data/spec/unit/ask_spec.rb +77 -0
  30. data/spec/unit/distance/distance_spec.rb +75 -0
  31. data/spec/unit/error_spec.rb +30 -0
  32. data/spec/unit/question/argument_spec.rb +30 -0
  33. data/spec/unit/question/character_spec.rb +24 -0
  34. data/spec/unit/question/default_spec.rb +25 -0
  35. data/spec/unit/question/in_spec.rb +23 -0
  36. data/spec/unit/question/initialize_spec.rb +24 -0
  37. data/spec/unit/question/modifier/apply_to_spec.rb +31 -0
  38. data/spec/unit/question/modifier/letter_case_spec.rb +22 -0
  39. data/spec/unit/question/modifier/whitespace_spec.rb +33 -0
  40. data/spec/unit/question/modify_spec.rb +44 -0
  41. data/spec/unit/question/valid_spec.rb +46 -0
  42. data/spec/unit/question/validate_spec.rb +30 -0
  43. data/spec/unit/question/validation/coerce_spec.rb +24 -0
  44. data/spec/unit/question/validation/valid_value_spec.rb +22 -0
  45. data/spec/unit/reader/getc_spec.rb +42 -0
  46. data/spec/unit/response/read_bool_spec.rb +47 -0
  47. data/spec/unit/response/read_char_spec.rb +16 -0
  48. data/spec/unit/response/read_date_spec.rb +20 -0
  49. data/spec/unit/response/read_email_spec.rb +42 -0
  50. data/spec/unit/response/read_multiple_spec.rb +23 -0
  51. data/spec/unit/response/read_number_spec.rb +28 -0
  52. data/spec/unit/response/read_range_spec.rb +26 -0
  53. data/spec/unit/response/read_spec.rb +68 -0
  54. data/spec/unit/response/read_string_spec.rb +19 -0
  55. data/spec/unit/say_spec.rb +66 -0
  56. data/spec/unit/statement/initialize_spec.rb +19 -0
  57. data/spec/unit/suggest_spec.rb +33 -0
  58. data/spec/unit/warn_spec.rb +30 -0
  59. data/tasks/console.rake +10 -0
  60. data/tasks/coverage.rake +11 -0
  61. data/tasks/spec.rake +29 -0
  62. data/tty-prompt.gemspec +26 -0
  63. 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
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -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