tty-prompt 0.10.1 → 0.11.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 (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
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'codes'
4
+ require_relative 'mode'
5
+
6
+ module TTY
7
+ class Prompt
8
+ class Reader
9
+ class Console
10
+ ESC = "\e".freeze
11
+ CSI = "\e[".freeze
12
+
13
+ # Key codes
14
+ #
15
+ # @return [Hash[Symbol]]
16
+ #
17
+ # @api public
18
+ attr_reader :keys
19
+
20
+ # Escape codes
21
+ #
22
+ # @return [Array[Integer]]
23
+ #
24
+ # @api public
25
+ attr_reader :escape_codes
26
+
27
+ def initialize(input)
28
+ @input = input
29
+ @mode = Mode.new
30
+ @keys = Codes.keys
31
+ @escape_codes = [[ESC.ord], CSI.bytes.to_a]
32
+ end
33
+
34
+ # Get a character from console with echo
35
+ #
36
+ # @param [Hash[Symbol]] options
37
+ # @option options [Symbol] :echo
38
+ # the echo toggle
39
+ #
40
+ # @return [String]
41
+ #
42
+ # @api private
43
+ def get_char(options)
44
+ mode.raw(options[:raw]) do
45
+ mode.echo(options[:echo]) { input.getc }
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ attr_reader :mode
52
+
53
+ attr_reader :input
54
+ end # Console
55
+ end # Reader
56
+ end # Prompt
57
+ end # TTY
@@ -1,7 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/prompt/reader/codes'
4
-
5
3
  module TTY
6
4
  class Prompt
7
5
  class Reader
@@ -10,10 +8,7 @@ module TTY
10
8
  # @api private
11
9
  class Key < Struct.new(:name, :ctrl, :meta, :shift)
12
10
  def initialize(*)
13
- super
14
- @ctrl = false
15
- @meta = false
16
- @shift = false
11
+ super(nil, false, false, false)
17
12
  end
18
13
  end
19
14
 
@@ -21,101 +16,69 @@ module TTY
21
16
  #
22
17
  # @api public
23
18
  class KeyEvent < Struct.new(:value, :key)
24
- META_KEY_CODE_RE = /^(?:\e+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/
25
-
26
- def self.from(char)
19
+ # Create key event from read input codes
20
+ #
21
+ # @param [Hash[Symbol]] keys
22
+ # the keys and codes mapping
23
+ # @param [Array[Integer]] codes
24
+ #
25
+ # @return [KeyEvent]
26
+ #
27
+ # @api public
28
+ def self.from(keys, char)
27
29
  key = Key.new
30
+ ctrls = keys.keys.grep(/ctrl/)
28
31
 
29
32
  case char
30
- when Codes::RETURN then key.name = :return
31
- when Codes::LINEFEED then key.name = :enter
32
- when Codes::TAB then key.name = :tab
33
- when Codes::BACKSPACE, Codes::CTRL_H,
34
- "#{Codes::ESCAPE}#{Codes::BACKSPACE}",
35
- "#{Codes::ESCAPE}#{Codes::CTRL_H}"
36
- key.name = :backspace
37
- key.meta = (char.chars.to_a[0] == Codes::ESCAPE)
38
- when Codes::DELETE then key.name = :delete
39
- when Codes::SPACE, "#{Codes::ESCAPE}#{Codes::SPACE}"
40
- key.name = :space
41
- key.meta = (char.size == 2)
42
- when Codes::ESCAPE, "#{Codes::ESCAPE}#{Codes::ESCAPE}"
43
- key.name = :escape
44
- key.meta = (char.size == 2)
45
- when proc { |c| c.length == 1 && c =~ /[a-z]/ }
46
- key.name = char
47
- when proc { |c| c.length == 1 && c =~ /[A-Z]/ }
48
- key.name = char.downcase
33
+ when keys[:return] then key.name = :return
34
+ when keys[:enter] then key.name = :enter
35
+ when keys[:tab] then key.name = :tab
36
+ when keys[:backspace] then key.name = :backspace
37
+ when keys[:delete] then key.name = :delete
38
+ when keys[:space] then key.name = :space
39
+ when keys[:escape] then key.name = :escape
40
+ when proc { |c| c =~ /^[a-z]{1}$/ }
41
+ key.name = :alpha
42
+ when proc { |c| c =~ /^[A-Z]{1}$/ }
43
+ key.name = :alpha
49
44
  key.shift = true
50
- when /^\d+$/
45
+ when proc { |c| c =~ /^\d+$/ }
51
46
  key.name = :num
52
- when META_KEY_CODE_RE # ansi escape
53
- parts = META_KEY_CODE_RE.match(char)
54
- code = "#{parts[1]}#{parts[2]}#{parts[4]}#{parts[6]}"
55
- modifier = (parts[3] || parts[5] || 1) - 1
56
-
57
- key.ctrl = (modifier & 4) != 0
58
- key.meta = (modifier & 10) != 0
59
- key.shift = (modifier & 1) != 0
60
-
61
- case code
62
- # f1 - f12
63
- when Codes::F1_XTERM, Codes::F1_GNOME, Codes::F1_WIN then key.name = :f1
64
- when Codes::F2_XTERM, Codes::F2_GNOME, Codes::F2_WIN then key.name = :f2
65
- when Codes::F3_XTERM, Codes::F3_GNOME, Codes::F3_WIN then key.name = :f3
66
- when Codes::F4_XTERM, Codes::F4_GNOME, Codes::F4_WIN then key.name = :f4
67
- when Codes::F5, Codes::F5_WIN then key.name = :f5
68
- when Codes::F6 then key.name = :f6
69
- when Codes::F7 then key.name = :f7
70
- when Codes::F8 then key.name = :f8
71
- when Codes::F9 then key.name = :f9
72
- when Codes::F10 then key.name = :f10
73
- when Codes::F11 then key.name = :f11
74
- when Codes::F12 then key.name = :f12
75
- # navigation
76
- when Codes::KEY_UP, Codes::KEY_UP_XTERM,
77
- Codes::CTRL_K, Codes::CTRL_P
78
- key.name = :up
79
- when Codes::KEY_UP_SHIFT then key.name = :up; key.shift = true
80
- when Codes::KEY_UP_CTRL then key.name = :up; key.ctrl = true
81
-
82
- when Codes::KEY_DOWN, Codes::KEY_DOWN_XTERM,
83
- Codes::CTRL_J, Codes::CTRL_N
84
- key.name = :down
85
- when Codes::KEY_DOWN_SHIFT then key.name = :down; key.shift = true
86
- when Codes::KEY_DOWN_CTRL then key.name = :down; key.ctrl = true
87
-
88
- when Codes::KEY_RIGHT, Codes::KEY_RIGHT_XTERM, Codes::CTRL_L
89
- key.name = :right
90
- when Codes::KEY_RIGHT_SHIFT then key.name = :right; key.shift = true
91
- when Codes::KEY_RIGHT_CTRL then key.name = :right; key.ctrl = true
92
-
93
- when Codes::KEY_LEFT, Codes::KEY_LEFT_XTERM, Codes::CTRL_H
94
- key.name = :left
95
- when Codes::KEY_LEFT_SHIFT then key.name = :left; key.shift = true
96
- when Codes::KEY_LEFT_CTRL then key.name = :left; key.ctrl = true
97
-
98
- when Codes::KEY_CLEAR, Codes::KEY_CLEAR_XTERM
99
- key.name = :clear
100
- when Codes::KEY_CLEAR_SHIFT then key.name = :clear; key.shift = true
101
- when Codes::KEY_CLEAR_CTRL then key.name = :clear; key.ctrl = true
102
-
103
- when Codes::KEY_END, Codes::KEY_END_XTERM
104
- key.name = :end
105
-
106
- when Codes::KEY_HOME, Codes::KEY_HOME_XTERM
107
- key.name = :home
108
- end
47
+ # arrows
48
+ when keys[:up] then key.name = :up
49
+ when keys[:down] then key.name = :down
50
+ when keys[:left] then key.name = :left
51
+ when keys[:right] then key.name = :right
52
+ when keys[:clear] then key.name = :clear
53
+ when keys[:end] then key.name = :end
54
+ when keys[:home] then key.name = :home
55
+ when proc { |cs| ctrls.any? { |name| keys[name] == cs } }
56
+ key.name = keys.key(char)
57
+ key.ctrl = true
58
+ # f1 - f12
59
+ when keys[:f1], keys[:f1_xterm] then key.name = :f1
60
+ when keys[:f2], keys[:f2_xterm] then key.name = :f2
61
+ when keys[:f3], keys[:f3_xterm] then key.name = :f3
62
+ when keys[:f4], keys[:f4_xterm] then key.name = :f4
63
+ when keys[:f5] then key.name = :f5
64
+ when keys[:f6] then key.name = :f6
65
+ when keys[:f7] then key.name = :f7
66
+ when keys[:f8] then key.name = :f8
67
+ when keys[:f9] then key.name = :f9
68
+ when keys[:f10] then key.name = :f10
69
+ when keys[:f11] then key.name = :f11
70
+ when keys[:f12] then key.name = :f12
109
71
  end
72
+
110
73
  new(char, key)
111
74
  end
112
75
 
113
- # Check if key event can be emitted
76
+ # Check if key event can be triggered
114
77
  #
115
78
  # @return [Boolean]
116
79
  #
117
80
  # @api public
118
- def emit?
81
+ def trigger?
119
82
  !key.nil? && !key.name.nil?
120
83
  end
121
84
  end # KeyEvent
@@ -19,11 +19,11 @@ module TTY
19
19
  #
20
20
  # @api public
21
21
  def echo(is_on = true, &block)
22
- previous = @input.echo?
23
- @input.echo = is_on
24
- yield
25
- ensure
26
- @input.echo = previous
22
+ if is_on
23
+ yield
24
+ else
25
+ @input.noecho(&block)
26
+ end
27
27
  end
28
28
 
29
29
  # Use raw mode in the given block
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require 'fiddle'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ module WinAPI
9
+ include Fiddle
10
+
11
+ Handle = RUBY_VERSION >= "2.0.0" ? Fiddle::Handle : DL::Handle
12
+
13
+ CRT_HANDLE = Handle.new("msvcrt") rescue Handle.new("crtdll")
14
+
15
+ def getch
16
+ @@getch ||= Fiddle::Function.new(CRT_HANDLE["_getch"], [], TYPE_INT)
17
+ @@getch.call
18
+ end
19
+ module_function :getch
20
+
21
+ def getche
22
+ @@getche ||= Fiddle::Function.new(CRT_HANDLE["_getche"], [], TYPE_INT)
23
+ @@getche.call
24
+ end
25
+ module_function :getche
26
+ end # WinAPI
27
+ end # Reader
28
+ end # Prompt
29
+ end # TTY
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'codes'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ class WinConsole
9
+ ESC = "\e".freeze
10
+ NUL_HEX = "\x00".freeze
11
+ EXT_HEX = "\xE0".freeze
12
+
13
+ # Key codes
14
+ #
15
+ # @return [Hash[Symbol]]
16
+ #
17
+ # @api public
18
+ attr_reader :keys
19
+
20
+ # Escape codes
21
+ #
22
+ # @return [Array[Integer]]
23
+ #
24
+ # @api public
25
+ attr_reader :escape_codes
26
+
27
+ def initialize(input)
28
+ require_relative 'win_api'
29
+ @input = input
30
+ @keys = Codes.win_keys
31
+ @escape_codes = [[NUL_HEX.ord], [ESC.ord], EXT_HEX.bytes.to_a]
32
+ end
33
+
34
+ # Get a character from console with echo
35
+ #
36
+ # @param [Hash[Symbol]] options
37
+ # @option options [Symbol] :echo
38
+ # the echo toggle
39
+ #
40
+ # @return [String]
41
+ #
42
+ # @api private
43
+ def get_char(options)
44
+ options[:echo] ? @input.getc : WinAPI.getch.chr
45
+ end
46
+ end # Console
47
+ end # Reader
48
+ end # Prompt
49
+ end # TTY
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require_relative 'symbols'
4
+
3
5
  module TTY
4
6
  # A class responsible for shell prompt interactions.
5
7
  class Prompt
@@ -7,6 +9,8 @@ module TTY
7
9
  #
8
10
  # @api public
9
11
  class Slider
12
+ include Symbols
13
+
10
14
  HELP = '(Use arrow keys, press Enter to select)'.freeze
11
15
 
12
16
  # Initailize a Slider
@@ -88,7 +92,7 @@ module TTY
88
92
  alias_method :keydown, :keyleft
89
93
 
90
94
  def keyright(*)
91
- @active += 1 if (@active + @step) <= range.size
95
+ @active += 1 if (@active + @step) < range.size
92
96
  end
93
97
  alias_method :keyup, :keyright
94
98
 
@@ -159,11 +163,11 @@ module TTY
159
163
  # @api private
160
164
  def render_slider
161
165
  output = ''
162
- output << Symbols::SLIDER_END
163
- output << '-' * @active
164
- output << @prompt.decorate(Symbols::SLIDER_HANDLE, @active_color)
165
- output << '-' * (range.size - @active - 1)
166
- output << Symbols::SLIDER_END
166
+ output << symbols[:pipe]
167
+ output << symbols[:line] * @active
168
+ output << @prompt.decorate(symbols[:handle], @active_color)
169
+ output << symbols[:line] * (range.size - @active - 1)
170
+ output << symbols[:pipe]
167
171
  output << " #{range[@active]}"
168
172
  output
169
173
  end
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/prompt/distance'
3
+ require_relative 'distance'
4
4
 
5
5
  module TTY
6
6
  # A class responsible for terminal prompt interactions.
@@ -2,18 +2,60 @@
2
2
 
3
3
  module TTY
4
4
  class Prompt
5
+ # Cross platform common Unicode symbols.
6
+ #
7
+ # @api public
5
8
  module Symbols
6
- SPACE = " "
7
- SUCCESS = ""
8
- FAILURE = ""
9
+ KEYS = {
10
+ tick: '',
11
+ cross: '',
12
+ star: '★',
13
+ dot: '•',
14
+ pointer: '‣',
15
+ line: '─',
16
+ pipe: '|',
17
+ handle: 'O',
18
+ ellipsis: '…',
19
+ radio_on: '⬢',
20
+ radio_off: '⬡',
21
+ checkbox_on: '☒',
22
+ checkbox_off: '☐',
23
+ circle_on: 'ⓧ',
24
+ circle_off: 'Ⓘ'
25
+ }.freeze
9
26
 
10
- ITEM_SECURE = "•"
11
- ITEM_SELECTED = "‣"
12
- RADIO_CHECKED = "⬢"
13
- RADIO_UNCHECKED = "⬡"
14
- SLIDER_HANDLE = 'O'
15
- SLIDER_RANGE = '-'
16
- SLIDER_END = '|'
27
+ WIN_KEYS = {
28
+ tick: '√',
29
+ cross: '×',
30
+ star: '*',
31
+ dot: '.',
32
+ pointer: '>',
33
+ line: '-',
34
+ pipe: '|',
35
+ handle: 'O',
36
+ ellipsis: '...',
37
+ radio_on: '(*)',
38
+ radio_off: '( )',
39
+ checkbox_on: '[×]',
40
+ checkbox_off: '[ ]',
41
+ circle_on: '(x)',
42
+ circle_off: '( )'
43
+ }.freeze
44
+
45
+ def symbols
46
+ @symbols ||= windows? ? WIN_KEYS : KEYS
47
+ end
48
+ module_function :symbols
49
+
50
+ # Check if Windowz
51
+ #
52
+ # @return [Boolean]
53
+ #
54
+ # @api public
55
+ def windows?
56
+ ::File::ALT_SEPARATOR == "\\"
57
+ end
58
+ module_function :windows?
17
59
  end # Symbols
18
60
  end # Prompt
19
61
  end # TTY