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