tty-prompt 0.2.0 → 0.3.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -6
  3. data/CHANGELOG.md +40 -3
  4. data/Gemfile +0 -1
  5. data/README.md +246 -65
  6. data/examples/ask.rb +7 -0
  7. data/examples/echo.rb +7 -0
  8. data/examples/in.rb +7 -0
  9. data/examples/mask.rb +9 -0
  10. data/examples/multi_select.rb +8 -0
  11. data/examples/select.rb +8 -0
  12. data/examples/validation.rb +9 -0
  13. data/examples/yes_no.rb +7 -0
  14. data/lib/tty-prompt.rb +6 -4
  15. data/lib/tty/prompt.rb +100 -25
  16. data/lib/tty/prompt/choice.rb +1 -1
  17. data/lib/tty/prompt/converter_dsl.rb +19 -0
  18. data/lib/tty/prompt/converter_registry.rb +56 -0
  19. data/lib/tty/prompt/converters.rb +77 -0
  20. data/lib/tty/prompt/evaluator.rb +29 -0
  21. data/lib/tty/prompt/list.rb +38 -36
  22. data/lib/tty/prompt/mask_question.rb +85 -0
  23. data/lib/tty/prompt/multi_list.rb +21 -32
  24. data/lib/tty/prompt/question.rb +184 -162
  25. data/lib/tty/prompt/question/checks.rb +85 -0
  26. data/lib/tty/prompt/question/modifier.rb +4 -5
  27. data/lib/tty/prompt/question/validation.rb +29 -35
  28. data/lib/tty/prompt/reader.rb +98 -52
  29. data/lib/tty/prompt/reader/codes.rb +63 -0
  30. data/lib/tty/prompt/reader/key_event.rb +67 -0
  31. data/lib/tty/prompt/reader/mode.rb +66 -0
  32. data/lib/tty/prompt/reader/mode/echo.rb +43 -0
  33. data/lib/tty/prompt/reader/mode/raw.rb +43 -0
  34. data/lib/tty/prompt/result.rb +42 -0
  35. data/lib/tty/prompt/statement.rb +9 -14
  36. data/lib/tty/prompt/suggestion.rb +4 -2
  37. data/lib/tty/prompt/symbols.rb +13 -0
  38. data/lib/tty/prompt/test.rb +3 -2
  39. data/lib/tty/prompt/utils.rb +1 -1
  40. data/lib/tty/prompt/version.rb +1 -1
  41. data/spec/unit/ask_spec.rb +31 -48
  42. data/spec/unit/choice/eql_spec.rb +0 -2
  43. data/spec/unit/choice/from_spec.rb +0 -2
  44. data/spec/unit/choices/add_spec.rb +0 -2
  45. data/spec/unit/choices/each_spec.rb +0 -2
  46. data/spec/unit/choices/new_spec.rb +0 -2
  47. data/spec/unit/choices/pluck_spec.rb +0 -2
  48. data/spec/unit/converters/convert_bool_spec.rb +58 -0
  49. data/spec/unit/{response/read_char_spec.rb → converters/convert_char_spec.rb} +2 -4
  50. data/spec/unit/converters/convert_custom_spec.rb +14 -0
  51. data/spec/unit/converters/convert_date_spec.rb +25 -0
  52. data/spec/unit/converters/convert_file_spec.rb +14 -0
  53. data/spec/unit/{response/read_number_spec.rb → converters/convert_number_spec.rb} +5 -7
  54. data/spec/unit/converters/convert_path_spec.rb +15 -0
  55. data/spec/unit/{response/read_range_spec.rb → converters/convert_range_spec.rb} +3 -5
  56. data/spec/unit/converters/convert_regex_spec.rb +12 -0
  57. data/spec/unit/converters/convert_string_spec.rb +21 -0
  58. data/spec/unit/distance/distance_spec.rb +0 -2
  59. data/spec/unit/error_spec.rb +0 -6
  60. data/spec/unit/evaluator_spec.rb +67 -0
  61. data/spec/unit/keypress_spec.rb +19 -0
  62. data/spec/unit/mask_spec.rb +95 -0
  63. data/spec/unit/multi_select_spec.rb +36 -24
  64. data/spec/unit/multiline_spec.rb +19 -0
  65. data/spec/unit/new_spec.rb +18 -0
  66. data/spec/unit/ok_spec.rb +10 -0
  67. data/spec/unit/question/default_spec.rb +17 -4
  68. data/spec/unit/question/echo_spec.rb +31 -0
  69. data/spec/unit/question/in_spec.rb +48 -16
  70. data/spec/unit/question/initialize_spec.rb +2 -9
  71. data/spec/unit/question/modifier/apply_to_spec.rb +9 -16
  72. data/spec/unit/question/modifier/letter_case_spec.rb +0 -2
  73. data/spec/unit/question/modifier/whitespace_spec.rb +12 -20
  74. data/spec/unit/question/modify_spec.rb +3 -7
  75. data/spec/unit/question/required_spec.rb +20 -14
  76. data/spec/unit/question/validate_spec.rb +20 -19
  77. data/spec/unit/question/validation/call_spec.rb +15 -6
  78. data/spec/unit/question/validation/coerce_spec.rb +17 -11
  79. data/spec/unit/reader/publish_keypress_event_spec.rb +81 -0
  80. data/spec/unit/reader/read_keypress_spec.rb +22 -0
  81. data/spec/unit/reader/read_line_spec.rb +31 -0
  82. data/spec/unit/reader/read_multiline_spec.rb +37 -0
  83. data/spec/unit/result_spec.rb +40 -0
  84. data/spec/unit/say_spec.rb +18 -23
  85. data/spec/unit/select_spec.rb +37 -32
  86. data/spec/unit/statement/initialize_spec.rb +4 -4
  87. data/spec/unit/suggest_spec.rb +0 -2
  88. data/spec/unit/warn_spec.rb +0 -5
  89. data/spec/unit/yes_no_spec.rb +70 -0
  90. data/tty-prompt.gemspec +7 -4
  91. metadata +123 -40
  92. data/lib/tty/prompt/codes.rb +0 -32
  93. data/lib/tty/prompt/cursor.rb +0 -131
  94. data/lib/tty/prompt/error.rb +0 -26
  95. data/lib/tty/prompt/mode.rb +0 -64
  96. data/lib/tty/prompt/mode/echo.rb +0 -41
  97. data/lib/tty/prompt/mode/raw.rb +0 -41
  98. data/lib/tty/prompt/response.rb +0 -247
  99. data/lib/tty/prompt/response_delegation.rb +0 -42
  100. data/spec/unit/cursor/new_spec.rb +0 -74
  101. data/spec/unit/question/character_spec.rb +0 -13
  102. data/spec/unit/reader/getc_spec.rb +0 -42
  103. data/spec/unit/response/read_bool_spec.rb +0 -58
  104. data/spec/unit/response/read_date_spec.rb +0 -16
  105. data/spec/unit/response/read_email_spec.rb +0 -45
  106. data/spec/unit/response/read_multiple_spec.rb +0 -21
  107. data/spec/unit/response/read_spec.rb +0 -69
  108. data/spec/unit/response/read_string_spec.rb +0 -14
@@ -1,10 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question, '#modify' do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
6
7
  it 'preserves answer for unkown modification' do
7
- prompt = TTY::TestPrompt.new
8
8
  prompt.input << 'piotr'
9
9
  prompt.input.rewind
10
10
  answer = prompt.ask("What is your name?") { |q| q.modify(:none) }
@@ -12,7 +12,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
12
12
  end
13
13
 
14
14
  it 'converts to upper case' do
15
- prompt = TTY::TestPrompt.new
16
15
  prompt.input << 'piotr'
17
16
  prompt.input.rewind
18
17
  answer = prompt.ask("What is your name?") { |q| q.modify(:upcase) }
@@ -20,7 +19,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
20
19
  end
21
20
 
22
21
  it 'trims whitespace' do
23
- prompt = TTY::TestPrompt.new
24
22
  prompt.input << " Some white\t space\t \there! \n"
25
23
  prompt.input.rewind
26
24
  answer = prompt.ask('Enter some text: ') { |q| q.modify(:trim) }
@@ -28,7 +26,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
28
26
  end
29
27
 
30
28
  it 'collapses whitespace' do
31
- prompt = TTY::TestPrompt.new
32
29
  prompt.input << " Some white\t space\t \there! \n"
33
30
  prompt.input.rewind
34
31
  answer = prompt.ask('Enter some text: ') { |q| q.modify(:collapse) }
@@ -36,7 +33,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
36
33
  end
37
34
 
38
35
  it 'strips and collapses whitespace' do
39
- prompt = TTY::TestPrompt.new
40
36
  prompt.input << " Some white\t space\t \there! \n"
41
37
  prompt.input.rewind
42
38
  answer = prompt.ask('Enter some text: ') { |q| q.modify(:strip, :collapse) }
@@ -1,28 +1,34 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question, '#required' do
6
- it 'requires value to be present with helper' do
7
- prompt = TTY::TestPrompt.new
8
- prompt.input << ''
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
7
+ it 'requires value to be present' do
8
+ prompt.input << "Piotr"
9
9
  prompt.input.rewind
10
- expect {
11
- prompt.ask('What is your name?') { |q| q.required(true) }
12
- }.to raise_error(ArgumentError)
10
+ prompt.ask('What is your name?') { |q| q.required(true) }
11
+ expect(prompt.output.string).to eq([
12
+ "What is your name? ",
13
+ "\e[1000D\e[K\e[1A",
14
+ "\e[1000D\e[K",
15
+ "What is your name? \e[32mPiotr\e[0m\n"
16
+ ].join)
13
17
  end
14
18
 
15
19
  it 'requires value to be present with option' do
16
- prompt = TTY::TestPrompt.new
17
- prompt.input << ''
20
+ prompt.input << "Piotr"
18
21
  prompt.input.rewind
19
- expect {
20
- prompt.ask('What is your name?', required: true)
21
- }.to raise_error(ArgumentError)
22
+ prompt.ask('What is your name?') { |q| q.required(true) }
23
+ expect(prompt.output.string).to eq([
24
+ "What is your name? ",
25
+ "\e[1000D\e[K\e[1A",
26
+ "\e[1000D\e[K",
27
+ "What is your name? \e[32mPiotr\e[0m\n"
28
+ ].join)
22
29
  end
23
30
 
24
31
  it "doesn't require value to be present" do
25
- prompt = TTY::TestPrompt.new
26
32
  prompt.input << ''
27
33
  prompt.input.rewind
28
34
  answer = prompt.ask('What is your name?') { |q| q.required(false) }
@@ -1,38 +1,39 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question, '#validate' do
6
- it 'fails to validate input' do
7
- prompt = TTY::TestPrompt.new
8
- prompt.input << 'piotrmurach'
9
- prompt.input.rewind
10
- expect {
11
- prompt.ask('What is your username?') { |q|
12
- q.validate(/^[^\.]+\.[^\.]+/)
13
- }
14
- }.to raise_error(ArgumentError)
15
- end
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
16
6
 
17
7
  it 'validates input with regex' do
18
- prompt = TTY::TestPrompt.new
19
8
  prompt.input << 'piotr.murach'
20
9
  prompt.input.rewind
21
- answer = prompt.ask('What is your username?') { |q|
10
+ answer = prompt.ask('What is your username?') do |q|
22
11
  q.validate(/^[^\.]+\.[^\.]+/)
23
- }
12
+ end
24
13
  expect(answer).to eq('piotr.murach')
14
+ expect(prompt.output.string).to eq([
15
+ "What is your username? ",
16
+ "\e[1000D\e[K\e[1A",
17
+ "\e[1000D\e[K",
18
+ "What is your username? \e[32mpiotr.murach\e[0m\n"
19
+ ].join)
25
20
  end
26
21
 
27
22
  it 'validates input with proc' do
28
- prompt = TTY::TestPrompt.new
29
23
  prompt.input << 'piotr.murach'
30
24
  prompt.input.rewind
31
- answer = prompt.ask('What is your username?') { |q|
25
+ answer = prompt.ask('What is your username?') do |q|
32
26
  q.validate { |input| input =~ /^[^\.]+\.[^\.]+/ }
33
- }
27
+ end
34
28
  expect(answer).to eq('piotr.murach')
35
29
  end
36
30
 
37
- it 'understands custom validation like :email'
31
+ it 'understands custom validation like :email' do
32
+ prompt.input << 'piotr@example.com'
33
+ prompt.input.rewind
34
+ answer = prompt.ask('What is your email?') do |q|
35
+ q.validate :email
36
+ end
37
+ expect(answer).to eq('piotr@example.com')
38
+ end
38
39
  end
@@ -1,22 +1,31 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question::Validation, '#call' do
6
4
  let(:pattern) { /^[^\.]+\.[^\.]+/ }
7
- let(:validation) { described_class.new(pattern) }
8
5
 
9
6
  it "validates nil input" do
7
+ validation = described_class.new(pattern)
10
8
  expect(validation.(nil)).to eq(false)
11
9
  end
12
10
 
13
11
  it "validates successfully when the value matches pattern" do
12
+ validation = described_class.new(pattern)
14
13
  expect(validation.('piotr.murach')).to eq(true)
15
14
  end
16
15
 
16
+ it "validates with a proc" do
17
+ pat = proc { |input| !pattern.match(input).nil? }
18
+ validation = described_class.new(pat)
19
+ expect(validation.call('piotr.murach')).to eq(true)
20
+ end
21
+
22
+ it "validates with custom name" do
23
+ validation = described_class.new(:email)
24
+ expect(validation.call('piotr@example.com')).to eq(true)
25
+ end
26
+
17
27
  it "fails validation when not maching pattern" do
18
- expect {
19
- validation.('piotrmurach')
20
- }.to raise_error(TTY::Prompt::InvalidArgument)
28
+ validation = described_class.new(pattern)
29
+ expect(validation.('piotrmurach')).to eq(false)
21
30
  end
22
31
  end
@@ -1,24 +1,30 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question::Validation, '#coerce' do
6
4
  let(:instance) { described_class.new }
7
5
 
8
- it "coerces into proc" do
9
- validation = lambda { "^[^\.]+\.[^\.]+" }
10
- expect(instance.coerce(validation)).to be_kind_of(Proc)
6
+ it "coerces lambda into proc" do
7
+ pattern = lambda { "^[^\.]+\.[^\.]+" }
8
+ validation = described_class.new(pattern)
9
+ expect(validation.pattern).to be_a(Proc)
10
+ end
11
+
12
+ it "doesn't coerce symbols" do
13
+ pattern = :email
14
+ validation =described_class.new(pattern)
15
+ expect(validation.pattern).to eq(:email)
11
16
  end
12
17
 
13
- it "cources into regex" do
14
- validation = "^[^\.]+\.[^\.]+"
15
- expect(instance.coerce(validation)).to be_kind_of(Regexp)
18
+ it "coerces into regex" do
19
+ pattern = /^[^\.]+\.[^\.]+/
20
+ validation = described_class.new(pattern)
21
+ expect(validation.pattern).to be_a(Regexp)
16
22
  end
17
23
 
18
- it "fails to coerce validation" do
19
- validation = Object.new
24
+ it "fails to coerce pattern into validation" do
25
+ pattern = Object.new
20
26
  expect {
21
- instance.coerce(validation)
27
+ described_class.new(pattern)
22
28
  }.to raise_error(TTY::Prompt::ValidationCoercion)
23
29
  end
24
30
  end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#publish_keypress_event' do
4
+ let(:input) { StringIO.new }
5
+ let(:out) { StringIO.new }
6
+ let(:reader) { described_class.new(input, out) }
7
+
8
+ it "publishes :keypress events" do
9
+ input << "abc\n"
10
+ input.rewind
11
+ chars = []
12
+ reader.on(:keypress) { |event| chars << event.value }
13
+ answer = reader.read_line
14
+ expect(chars).to eq(%w(a b c))
15
+ expect(answer).to eq("abc")
16
+ end
17
+
18
+ it "publishes :keyup for read_keypress" do
19
+ input << "\e[Aaa"
20
+ input.rewind
21
+ keys = []
22
+ reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
23
+ reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
24
+ reader.on(:keydown) { |event| keys << "keydown_#{event.value}" }
25
+
26
+ answer = reader.read_keypress
27
+ expect(keys).to eq(["keyup_\e[A", "keypress_\e[A"])
28
+ expect(answer).to eq("\e[A")
29
+ end
30
+
31
+ it "publishes :keydown event for read_keypress" do
32
+ input << "\e[Baa"
33
+ input.rewind
34
+ keys = []
35
+ reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
36
+ reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
37
+ reader.on(:keydown) { |event| keys << "keydown_#{event.value}" }
38
+
39
+ answer = reader.read_keypress
40
+ expect(keys).to eq(["keydown_\e[B", "keypress_\e[B"])
41
+ expect(answer).to eq("\e[B")
42
+ end
43
+
44
+ it "publishes :keynum event" do
45
+ input << "5aa"
46
+ input.rewind
47
+ keys = []
48
+ reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
49
+ reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
50
+ reader.on(:keynum) { |event| keys << "keynum_#{event.value}" }
51
+
52
+ answer = reader.read_keypress
53
+ expect(keys).to eq(["keynum_5", "keypress_5"])
54
+ expect(answer).to eq("5")
55
+ end
56
+
57
+ it "publishes :keyreturn event" do
58
+ input << "\r"
59
+ input.rewind
60
+ keys = []
61
+ reader.on(:keypress) { |event| keys << "keypress" }
62
+ reader.on(:keyup) { |event| keys << "keyup" }
63
+ reader.on(:keyreturn) { |event| keys << "keyreturn" }
64
+
65
+ answer = reader.read_keypress
66
+ expect(keys).to eq(["keyreturn", "keypress"])
67
+ expect(answer).to eq("\r")
68
+ end
69
+
70
+ it "subscribes to multiple events" do
71
+ input << "\n"
72
+ input.rewind
73
+ keys = []
74
+ reader.on(:keyenter) { |event| keys << "keyenter" }
75
+ .on(:keypress) { |event| keys << "keypress" }
76
+
77
+ answer = reader.read_keypress
78
+ expect(keys).to eq(["keyenter", "keypress"])
79
+ expect(answer).to eq("\n")
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#read_keypress' do
4
+ let(:input) { StringIO.new }
5
+ let(:out) { StringIO.new }
6
+ let(:reader) { described_class.new(input, out) }
7
+
8
+ it "reads single key press" do
9
+ input << "\e[Aaaaaaa\n"
10
+ input.rewind
11
+ answer = reader.read_keypress
12
+ expect(answer).to eq("\e[A")
13
+ end
14
+
15
+ it "stops reading when ctrl-c pressed" do
16
+ input << "\x03"
17
+ input.rewind
18
+ expect {
19
+ reader.read_keypress
20
+ }.to raise_error(SystemExit)
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#read_line' do
4
+ let(:input) { StringIO.new }
5
+ let(:output) { StringIO.new }
6
+
7
+ subject(:reader) { described_class.new(input, output) }
8
+
9
+ it 'masks characters' do
10
+ mask = '*'
11
+ input << "password\n"
12
+ input.rewind
13
+ answer = reader.read_line(mask)
14
+ expect(answer).to eq("password")
15
+ end
16
+
17
+ it "echoes characters back" do
18
+ input << "password\n"
19
+ input.rewind
20
+ answer = reader.read_line
21
+ expect(answer).to eq("password")
22
+ expect(output.string).to eq("")
23
+ end
24
+
25
+ it 'deletes characters when backspace pressed' do
26
+ input << "aa\ba\bcc"
27
+ input.rewind
28
+ answer = reader.read_line
29
+ expect(answer).to eq('acc')
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#read_multiline' do
4
+ let(:input) { StringIO.new }
5
+ let(:output) { StringIO.new }
6
+
7
+ subject(:reader) { described_class.new(input, output) }
8
+
9
+ it 'reads no lines' do
10
+ input << ''
11
+ input.rewind
12
+ answer = reader.read_multiline
13
+ expect(answer).to eq([])
14
+ end
15
+
16
+ it "reads a line" do
17
+ input << "Single line"
18
+ input.rewind
19
+ answer = reader.read_multiline
20
+ expect(answer).to eq(['Single line'])
21
+ end
22
+
23
+ it 'reads few lines' do
24
+ input << "First line\nSecond line\nThird line"
25
+ input.rewind
26
+ answer = reader.read_multiline
27
+ expect(answer).to eq(['First line', 'Second line', 'Third line'])
28
+ end
29
+
30
+ it 'reads and yiels every line' do
31
+ input << "First line\nSecond line\nThird line"
32
+ input.rewind
33
+ lines = []
34
+ reader.read_multiline { |line| lines << line }
35
+ expect(lines).to eq(['First line', 'Second line', 'Third line'])
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Result do
4
+ it "checks value to be invalid" do
5
+ question = double(:question)
6
+ result = TTY::Prompt::Result.new(question, nil)
7
+
8
+ answer = result.with { |quest, value|
9
+ if value.nil?
10
+ [value, ["`#{value}` provided cannot be empty"]]
11
+ else
12
+ value
13
+ end
14
+ }
15
+ expect(answer).to be_a(TTY::Prompt::Result::Failure)
16
+ expect(answer.success?).to eq(false)
17
+ expect(answer.errors).to eq(["`` provided cannot be empty"])
18
+ end
19
+
20
+ it "checks value to be valid" do
21
+ question = double(:question)
22
+ result = TTY::Prompt::Result.new(question, 'Piotr')
23
+
24
+ CheckRequired = Class.new do
25
+ def self.call(quest, value)
26
+ if value.nil?
27
+ [value, ["`#{value}` provided cannot be empty"]]
28
+ else
29
+ value
30
+ end
31
+ end
32
+ end
33
+
34
+ answer = result.with(CheckRequired)
35
+ expect(answer).to be_a(TTY::Prompt::Result::Success)
36
+ expect(answer.success?).to eq(true)
37
+ expect(answer.value).to eq('Piotr')
38
+ expect(answer.errors).to eq([])
39
+ end
40
+ end