tty-prompt 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -1
  3. data/CHANGELOG.md +30 -0
  4. data/README.md +39 -9
  5. data/examples/echo.rb +5 -1
  6. data/examples/inputs.rb +10 -0
  7. data/examples/mask.rb +6 -2
  8. data/examples/multi_select.rb +1 -1
  9. data/examples/multi_select_paged.rb +9 -0
  10. data/examples/select.rb +5 -5
  11. data/examples/slider.rb +1 -1
  12. data/lib/tty-prompt.rb +2 -36
  13. data/lib/tty/prompt.rb +49 -8
  14. data/lib/tty/prompt/choices.rb +2 -0
  15. data/lib/tty/prompt/confirm_question.rb +6 -1
  16. data/lib/tty/prompt/converter_dsl.rb +9 -6
  17. data/lib/tty/prompt/converter_registry.rb +27 -19
  18. data/lib/tty/prompt/converters.rb +16 -22
  19. data/lib/tty/prompt/enum_list.rb +8 -4
  20. data/lib/tty/prompt/enum_paginator.rb +2 -0
  21. data/lib/tty/prompt/evaluator.rb +1 -1
  22. data/lib/tty/prompt/expander.rb +1 -1
  23. data/lib/tty/prompt/list.rb +21 -11
  24. data/lib/tty/prompt/mask_question.rb +15 -6
  25. data/lib/tty/prompt/multi_list.rb +12 -10
  26. data/lib/tty/prompt/question.rb +38 -36
  27. data/lib/tty/prompt/question/modifier.rb +2 -0
  28. data/lib/tty/prompt/question/validation.rb +5 -4
  29. data/lib/tty/prompt/reader.rb +104 -58
  30. data/lib/tty/prompt/reader/codes.rb +103 -63
  31. data/lib/tty/prompt/reader/console.rb +57 -0
  32. data/lib/tty/prompt/reader/key_event.rb +51 -88
  33. data/lib/tty/prompt/reader/mode.rb +5 -5
  34. data/lib/tty/prompt/reader/win_api.rb +29 -0
  35. data/lib/tty/prompt/reader/win_console.rb +49 -0
  36. data/lib/tty/prompt/slider.rb +10 -6
  37. data/lib/tty/prompt/suggestion.rb +1 -1
  38. data/lib/tty/prompt/symbols.rb +52 -10
  39. data/lib/tty/prompt/version.rb +1 -1
  40. data/lib/tty/{prompt/test.rb → test_prompt.rb} +2 -1
  41. data/spec/unit/ask_spec.rb +8 -16
  42. data/spec/unit/converters/convert_bool_spec.rb +1 -2
  43. data/spec/unit/converters/on_error_spec.rb +9 -0
  44. data/spec/unit/enum_paginator_spec.rb +16 -0
  45. data/spec/unit/enum_select_spec.rb +69 -25
  46. data/spec/unit/expand_spec.rb +14 -14
  47. data/spec/unit/mask_spec.rb +66 -29
  48. data/spec/unit/multi_select_spec.rb +120 -74
  49. data/spec/unit/new_spec.rb +5 -3
  50. data/spec/unit/paginator_spec.rb +16 -0
  51. data/spec/unit/question/default_spec.rb +2 -4
  52. data/spec/unit/question/echo_spec.rb +2 -3
  53. data/spec/unit/question/in_spec.rb +9 -14
  54. data/spec/unit/question/modifier/letter_case_spec.rb +32 -11
  55. data/spec/unit/question/modifier/whitespace_spec.rb +41 -15
  56. data/spec/unit/question/required_spec.rb +9 -13
  57. data/spec/unit/question/validate_spec.rb +7 -10
  58. data/spec/unit/reader/key_event_spec.rb +36 -50
  59. data/spec/unit/reader/publish_keypress_event_spec.rb +5 -3
  60. data/spec/unit/reader/read_keypress_spec.rb +8 -7
  61. data/spec/unit/reader/read_line_spec.rb +9 -9
  62. data/spec/unit/reader/read_multiline_spec.rb +8 -7
  63. data/spec/unit/select_spec.rb +85 -25
  64. data/spec/unit/slider_spec.rb +43 -16
  65. data/spec/unit/yes_no_spec.rb +14 -28
  66. data/tasks/console.rake +1 -0
  67. data/tty-prompt.gemspec +2 -2
  68. metadata +14 -7
@@ -48,6 +48,7 @@ module TTY
48
48
  #
49
49
  # @api public
50
50
  def self.letter_case(mod, value)
51
+ return value unless value.is_a?(String)
51
52
  case mod
52
53
  when :up, :upcase, :uppercase
53
54
  value.upcase
@@ -73,6 +74,7 @@ module TTY
73
74
  #
74
75
  # @api public
75
76
  def self.whitespace(mod, value)
77
+ return value unless value.is_a?(String)
76
78
  case mod
77
79
  when :trim, :strip
78
80
  value.strip
@@ -5,11 +5,12 @@ module TTY
5
5
  class Question
6
6
  # A class representing question validation.
7
7
  class Validation
8
- attr_reader :pattern
9
-
8
+ # Available validator names
10
9
  VALIDATORS = {
11
10
  email: /^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i
12
- }
11
+ }.freeze
12
+
13
+ attr_reader :pattern
13
14
 
14
15
  # Initialize a Validation
15
16
  #
@@ -37,7 +38,7 @@ module TTY
37
38
  when Regexp
38
39
  Regexp.new(pattern.to_s)
39
40
  else
40
- fail ValidationCoercion, "Wrong type, got #{pattern.class}"
41
+ raise ValidationCoercion, "Wrong type, got #{pattern.class}"
41
42
  end
42
43
  end
43
44
 
@@ -1,8 +1,11 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'wisper'
4
- require 'tty/prompt/reader/key_event'
5
- require 'tty/prompt/reader/mode'
4
+ require 'rbconfig'
5
+
6
+ require_relative 'reader/key_event'
7
+ require_relative 'reader/console'
8
+ require_relative 'reader/win_console'
6
9
 
7
10
  module TTY
8
11
  # A class responsible for shell prompt interactions.
@@ -14,92 +17,101 @@ module TTY
14
17
  # @api private
15
18
  class Reader
16
19
  include Wisper::Publisher
20
+
17
21
  # Raised when the user hits the interrupt key(Control-C)
18
22
  #
19
23
  # @api public
20
24
  InputInterrupt = Class.new(StandardError)
21
25
 
22
- attr_reader :mode
23
-
24
26
  attr_reader :input
25
27
 
26
28
  attr_reader :output
27
29
 
28
- # Key input constants for decimal codes
30
+ attr_reader :env
31
+
32
+ # Key codes
29
33
  CARRIAGE_RETURN = 13
30
34
  NEWLINE = 10
31
35
  BACKSPACE = 127
32
36
  DELETE = 8
33
37
 
34
- CSI = "\e[".freeze
35
-
36
38
  # Initialize a Reader
37
39
  #
38
40
  # @api public
39
- def initialize(input, output, options = {})
41
+ def initialize(input = $stdin, output = $stdout, options = {})
40
42
  @input = input
41
43
  @output = output
42
- @mode = Mode.new
43
44
  @interrupt = options.fetch(:interrupt) { :error }
45
+ @env = options.fetch(:env) { ENV }
46
+ @console = windows? ? WinConsole.new(input) : Console.new(input)
44
47
  end
45
48
 
46
49
  # Get input in unbuffered mode.
47
50
  #
48
51
  # @example
49
- # buffer do
52
+ # unbufferred do
50
53
  # ...
51
54
  # end
52
55
  #
53
- # @return [String]
54
- #
55
56
  # @api public
56
- def buffer(&block)
57
+ def unbufferred(&block)
57
58
  bufferring = output.sync
58
59
  # Immediately flush output
59
60
  output.sync = true
60
- value = block.call if block_given?
61
+ block[] if block_given?
62
+ ensure
61
63
  output.sync = bufferring
62
- value
63
64
  end
64
65
 
65
- # Read a single keypress that may include
66
- # 2 or 3 escape characters.
66
+ # Read a keypress including invisible multibyte codes
67
+ # and return a character as a string.
68
+ # Nothing is echoed to the console. This call will block for a
69
+ # single keypress, but will not wait for Enter to be pressed.
67
70
  #
68
- # @param [Boolean] echo
71
+ # @param [Hash[Symbol]] options
72
+ # @option options [Boolean] echo
69
73
  # whether to echo chars back or not, defaults to false
74
+ # @option options [Boolean] raw
75
+ # whenther raw mode enabled, defaults to true
70
76
  #
71
77
  # @return [String]
72
78
  #
73
79
  # @api public
74
- def read_keypress(echo = false)
75
- buffer do
76
- mode.echo(echo) do
77
- mode.raw(true) do
78
- key = read_char
79
- emit_key_event(key) if key
80
- handle_interrupt if key == Codes::CTRL_C
81
- key
82
- end
83
- end
84
- end
80
+ def read_keypress(options = {})
81
+ opts = { echo: false, raw: true }.merge(options)
82
+ codes = unbufferred { get_codes(opts) }
83
+ char = codes ? codes.pack('U*') : nil
84
+
85
+ trigger_key_event(char) if char
86
+ handle_interrupt if char == @console.keys[:ctrl_c]
87
+ char
85
88
  end
89
+ alias read_char read_keypress
86
90
 
87
- # Reads single character including invisible multibyte codes
91
+ # Get input code points
88
92
  #
89
- # @params [Integer] bytes
90
- # the number of bytes to read
93
+ # @param [Hash[Symbol]] options
94
+ # @param [Array[Integer]] codes
91
95
  #
92
- # @return [String]
96
+ # @return [Array[Integer]]
93
97
  #
94
- # @api public
95
- def read_char(bytes = 1)
96
- chars = input.getc
97
- return if chars.nil?
98
- while CSI.start_with?(chars) || chars.start_with?(CSI) &&
99
- !(64..126).include?(chars.each_codepoint.to_a.last)
100
- chars << read_char(bytes + 1)
98
+ # @api private
99
+ def get_codes(options = {}, codes = [])
100
+ opts = { echo: true, raw: false }.merge(options)
101
+ char = @console.get_char(opts)
102
+ return if char.nil?
103
+ codes << char.ord
104
+
105
+ condition = proc { |escape|
106
+ (codes - escape).empty? ||
107
+ (escape - codes).empty? &&
108
+ !(64..126).include?(codes.last)
109
+ }
110
+
111
+ while @console.escape_codes.any?(&condition)
112
+ get_codes(options, codes)
101
113
  end
102
- chars
114
+ codes
103
115
  end
104
116
 
105
117
  # Get a single line from STDIN. Each key pressed is echoed
@@ -112,19 +124,28 @@ module TTY
112
124
  # @return [String]
113
125
  #
114
126
  # @api public
115
- def read_line(echo = true)
127
+ def read_line(options = {})
128
+ opts = { echo: true, raw: false }.merge(options)
116
129
  line = ''
117
- buffer do
118
- mode.echo(echo) do
119
- while (char = read_char) && (char_byte = char.unpack('c*')[0]) &&
120
- !(char_byte == CARRIAGE_RETURN || char_byte == NEWLINE)
121
- emit_key_event(char)
122
- if char_byte == BACKSPACE || char_byte == DELETE
123
- line = line.slice(-1, 1) unless line.empty?
124
- else
125
- line << char
126
- end
127
- end
130
+ backspaces = 0
131
+ delete_char = proc { |c| c == BACKSPACE || c == DELETE }
132
+
133
+ while (codes = get_codes(opts)) && (code = codes[0])
134
+ char = codes.pack('U*')
135
+ trigger_key_event(char)
136
+
137
+ if delete_char[code]
138
+ line.slice!(-1, 1)
139
+ backspaces -= 1
140
+ else
141
+ line << char
142
+ backspaces = line.size
143
+ end
144
+
145
+ break if (code == CARRIAGE_RETURN || code == NEWLINE)
146
+
147
+ if delete_char[code] && opts[:echo]
148
+ output.print(' ' + (backspaces >= 0 ? "\b" : ''))
128
149
  end
129
150
  end
130
151
  line
@@ -152,18 +173,33 @@ module TTY
152
173
  response
153
174
  end
154
175
 
176
+ # Expose event broadcasting
177
+ #
178
+ # @api public
179
+ def trigger(event, *args)
180
+ publish(event, *args)
181
+ end
182
+
155
183
  # Publish event
156
184
  #
157
- # @param [String] key
185
+ # @param [String] char
158
186
  # the key pressed
159
187
  #
160
188
  # @return [nil]
161
189
  #
162
190
  # @api public
163
- def emit_key_event(key)
164
- event = KeyEvent.from(key)
165
- publish(:"key#{event.key.name}", event) if event.emit?
166
- publish(:keypress, event)
191
+ def trigger_key_event(char)
192
+ event = KeyEvent.from(@console.keys, char)
193
+ trigger(:"key#{event.key.name}", event) if event.trigger?
194
+ trigger(:keypress, event)
195
+ end
196
+
197
+ # Inspect class name and public attributes
198
+ # @return [String]
199
+ #
200
+ # @api public
201
+ def inspect
202
+ "#<#{self.class}: @input=#{input}, @output=#{output}>"
167
203
  end
168
204
 
169
205
  private
@@ -185,6 +221,16 @@ module TTY
185
221
  raise InputInterrupt
186
222
  end
187
223
  end
224
+
225
+ # Check if Windowz mode
226
+ #
227
+ # @return [Boolean]
228
+ #
229
+ # @api public
230
+ def windows?
231
+ return false if env["TTY_TEST"] == true
232
+ ::File::ALT_SEPARATOR == "\\"
233
+ end
188
234
  end # Reader
189
235
  end # Prompt
190
236
  end # TTY
@@ -4,77 +4,117 @@ module TTY
4
4
  class Prompt
5
5
  class Reader
6
6
  module Codes
7
- BACKSPACE = "\x7f"
8
- DELETE = "\004"
9
- ESCAPE = "\e"
10
- LINEFEED = "\n"
11
- RETURN = "\r"
12
- SPACE = " "
13
- TAB = "\t"
7
+ def ctrl_keys
8
+ {
9
+ ctrl_a: ?\C-a,
10
+ ctrl_b: ?\C-b,
11
+ ctrl_c: ?\C-c,
12
+ ctrl_d: ?\C-d,
13
+ ctrl_e: ?\C-e,
14
+ ctrl_f: ?\C-f,
15
+ ctrl_g: ?\C-g,
16
+ ctrl_h: ?\C-h,
17
+ ctrl_i: ?\C-i,
18
+ ctrl_j: ?\C-j,
19
+ ctrl_k: ?\C-k,
20
+ ctrl_l: ?\C-l,
21
+ ctrl_m: ?\C-m,
22
+ ctrl_n: ?\C-n,
23
+ ctrl_o: ?\C-o,
24
+ ctrl_p: ?\C-p,
25
+ ctrl_q: ?\C-q,
26
+ ctrl_r: ?\C-r,
27
+ ctrl_s: ?\C-s,
28
+ ctrl_t: ?\C-t,
29
+ ctrl_u: ?\C-u,
30
+ ctrl_v: ?\C-v,
31
+ ctrl_w: ?\C-w,
32
+ ctrl_x: ?\C-x,
33
+ ctrl_y: ?\C-y,
34
+ ctrl_z: ?\C-z
35
+ }
36
+ end
37
+ module_function :ctrl_keys
14
38
 
15
- KEY_UP = "[A"
16
- KEY_DOWN = "[B"
17
- KEY_RIGHT = "[C"
18
- KEY_LEFT = "[D"
19
- KEY_CLEAR = "[E"
20
- KEY_END = "[F"
21
- KEY_HOME = "[H"
22
- KEY_DELETE = "[3"
39
+ def keys
40
+ {
41
+ tab: "\t",
42
+ enter: "\n",
43
+ return: "\r",
44
+ escape: "\e",
45
+ space: " ",
46
+ backspace: ?\C-?,
47
+ insert: "\e[2~",
48
+ delete: "\e[3~",
49
+ page_up: "\e[5~",
50
+ page_down: "\e[6~",
23
51
 
24
- KEY_UP_XTERM = "OA"
25
- KEY_DOWN_XTERM = "OB"
26
- KEY_RIGHT_XTERM = "OC"
27
- KEY_LEFT_XTERM = "OD"
28
- KEY_CLEAR_XTERM = "OE"
29
- KEY_END_XTERM = "OF"
30
- KEY_HOME_XTERM = "OH"
31
- KEY_DELETE_XTERM = "O3"
52
+ up: "\e[A",
53
+ down: "\e[B",
54
+ right: "\e[C",
55
+ left: "\e[D",
56
+ clear: "\e[E",
57
+ end: "\e[F",
58
+ home: "\e[H",
32
59
 
33
- KEY_UP_SHIFT = "[a"
34
- KEY_DOWN_SHIFT = "[b"
35
- KEY_RIGHT_SHIFT = "[c"
36
- KEY_LEFT_SHIFT = "[d"
37
- KEY_CLEAR_SHIFT = "[e"
60
+ f1_xterm: "\eOP",
61
+ f2_xterm: "\eOQ",
62
+ f3_xterm: "\eOR",
63
+ f4_xterm: "\eOS",
38
64
 
39
- KEY_UP_CTRL = "0a"
40
- KEY_DOWN_CTRL = "0b"
41
- KEY_RIGHT_CTRL = "0c"
42
- KEY_LEFT_CTRL = "0d"
43
- KEY_CLEAR_CTRL = "0e"
65
+ f1: "\e[11~",
66
+ f2: "\e[12~",
67
+ f3: "\e[13~",
68
+ f4: "\e[14~",
69
+ f5: "\e[15~",
70
+ f6: "\e[17~",
71
+ f7: "\e[18~",
72
+ f8: "\e[19~",
73
+ f9: "\e[20~",
74
+ f10: "\e[21~",
75
+ f11: "\e[23~",
76
+ f12: "\e[24~"
77
+ }.merge(ctrl_keys)
78
+ end
79
+ module_function :keys
44
80
 
45
- CTRL_J = "\x0A"
46
- CTRL_N = "\x0E"
47
- CTRL_K = "\x0B"
48
- CTRL_P = "\x10"
49
- SIGINT = "\x03"
50
- CTRL_C = "\x03"
51
- CTRL_H = "\b"
52
- CTRL_L = "\f"
81
+ def win_keys
82
+ {
83
+ tab: "\t",
84
+ enter: "\r",
85
+ return: "\r",
86
+ escape: "\e",
87
+ space: " ",
88
+ backspace: "\b",
89
+ insert: [224, 82].pack('U*'),
90
+ delete: [224, 83].pack('U*'),
91
+ page_up: [224, 73].pack('U*'),
92
+ page_down: [224, 81].pack('U*'),
53
93
 
54
- F1_XTERM = "OP"
55
- F2_XTERM = "OQ"
56
- F3_XTERM = "OR"
57
- F4_XTERM = "OS"
94
+ up: [224, 72].pack('U*'),
95
+ down: [224, 80].pack('U*'),
96
+ right: [224, 77].pack('U*'),
97
+ left: [224, 75].pack('U*'),
98
+ clear: [224, 83].pack('U*'),
99
+ end: [224, 79].pack('U*'),
100
+ home: [224, 71].pack('U*'),
58
101
 
59
- F1_GNOME = "[11~"
60
- F2_GNOME = "[12~"
61
- F3_GNOME = "[13~"
62
- F4_GNOME = "[14~"
102
+ f1: "\x00;",
103
+ f2: "\x00<",
104
+ f3: "\x00",
105
+ f4: "\x00=",
106
+ f5: "\x00?",
107
+ f6: "\x00@",
108
+ f7: "\x00A",
109
+ f8: "\x00B",
110
+ f9: "\x00C",
111
+ f10: "\x00D",
112
+ f11: "\x00\x85",
113
+ f12: "\x00\x86"
114
+ }.merge(ctrl_keys)
115
+ end
116
+ module_function :win_keys
63
117
 
64
- F1_WIN = "[[A"
65
- F2_WIN = "[[B"
66
- F3_WIN = "[[C"
67
- F4_WIN = "[[D"
68
- F5_WIN = "[[E"
69
-
70
- F5 = "[15~"
71
- F6 = "[17~"
72
- F7 = "[18~"
73
- F8 = "[19~"
74
- F9 = "[20~"
75
- F10 = "[21~"
76
- F11 = "[23~"
77
- F12 = "[24~"
78
118
  end # Codes
79
119
  end # Reader
80
120
  end # Prompt