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.
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