tty-prompt 0.11.0 → 0.12.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +66 -7
  4. data/examples/key_events.rb +11 -0
  5. data/examples/keypress.rb +3 -5
  6. data/examples/multiline.rb +9 -0
  7. data/examples/pause.rb +7 -0
  8. data/lib/tty/prompt.rb +82 -44
  9. data/lib/tty/prompt/confirm_question.rb +20 -36
  10. data/lib/tty/prompt/enum_list.rb +32 -23
  11. data/lib/tty/prompt/expander.rb +35 -31
  12. data/lib/tty/prompt/keypress.rb +91 -0
  13. data/lib/tty/prompt/list.rb +38 -23
  14. data/lib/tty/prompt/mask_question.rb +4 -7
  15. data/lib/tty/prompt/multi_list.rb +3 -1
  16. data/lib/tty/prompt/multiline.rb +71 -0
  17. data/lib/tty/prompt/question.rb +33 -35
  18. data/lib/tty/prompt/reader.rb +154 -38
  19. data/lib/tty/prompt/reader/codes.rb +4 -4
  20. data/lib/tty/prompt/reader/console.rb +1 -1
  21. data/lib/tty/prompt/reader/history.rb +145 -0
  22. data/lib/tty/prompt/reader/key_event.rb +4 -0
  23. data/lib/tty/prompt/reader/line.rb +162 -0
  24. data/lib/tty/prompt/reader/mode.rb +2 -2
  25. data/lib/tty/prompt/reader/win_console.rb +5 -1
  26. data/lib/tty/prompt/slider.rb +18 -12
  27. data/lib/tty/prompt/timeout.rb +48 -0
  28. data/lib/tty/prompt/version.rb +1 -1
  29. data/spec/unit/ask_spec.rb +15 -0
  30. data/spec/unit/converters/convert_bool_spec.rb +1 -0
  31. data/spec/unit/keypress_spec.rb +35 -6
  32. data/spec/unit/multi_select_spec.rb +18 -0
  33. data/spec/unit/multiline_spec.rb +67 -9
  34. data/spec/unit/question/default_spec.rb +1 -0
  35. data/spec/unit/question/echo_spec.rb +8 -0
  36. data/spec/unit/question/in_spec.rb +13 -0
  37. data/spec/unit/question/required_spec.rb +31 -2
  38. data/spec/unit/question/validate_spec.rb +39 -9
  39. data/spec/unit/reader/history_spec.rb +172 -0
  40. data/spec/unit/reader/key_event_spec.rb +12 -8
  41. data/spec/unit/reader/line_spec.rb +110 -0
  42. data/spec/unit/reader/publish_keypress_event_spec.rb +11 -0
  43. data/spec/unit/reader/read_line_spec.rb +32 -2
  44. data/spec/unit/reader/read_multiline_spec.rb +21 -7
  45. data/spec/unit/select_spec.rb +40 -1
  46. data/spec/unit/yes_no_spec.rb +48 -4
  47. metadata +14 -3
  48. data/lib/tty/prompt/history.rb +0 -16
@@ -44,8 +44,10 @@ module TTY
44
44
  escape: "\e",
45
45
  space: " ",
46
46
  backspace: ?\C-?,
47
+ home: "\e[1~",
47
48
  insert: "\e[2~",
48
49
  delete: "\e[3~",
50
+ end: "\e[4~",
49
51
  page_up: "\e[5~",
50
52
  page_down: "\e[6~",
51
53
 
@@ -54,8 +56,6 @@ module TTY
54
56
  right: "\e[C",
55
57
  left: "\e[D",
56
58
  clear: "\e[E",
57
- end: "\e[F",
58
- home: "\e[H",
59
59
 
60
60
  f1_xterm: "\eOP",
61
61
  f2_xterm: "\eOQ",
@@ -86,6 +86,8 @@ module TTY
86
86
  escape: "\e",
87
87
  space: " ",
88
88
  backspace: "\b",
89
+ home: [224, 71].pack('U*'),
90
+ end: [224, 79].pack('U*'),
89
91
  insert: [224, 82].pack('U*'),
90
92
  delete: [224, 83].pack('U*'),
91
93
  page_up: [224, 73].pack('U*'),
@@ -96,8 +98,6 @@ module TTY
96
98
  right: [224, 77].pack('U*'),
97
99
  left: [224, 75].pack('U*'),
98
100
  clear: [224, 83].pack('U*'),
99
- end: [224, 79].pack('U*'),
100
- home: [224, 71].pack('U*'),
101
101
 
102
102
  f1: "\x00;",
103
103
  f2: "\x00<",
@@ -26,7 +26,7 @@ module TTY
26
26
 
27
27
  def initialize(input)
28
28
  @input = input
29
- @mode = Mode.new
29
+ @mode = Mode.new(input)
30
30
  @keys = Codes.keys
31
31
  @escape_codes = [[ESC.ord], CSI.bytes.to_a]
32
32
  end
@@ -0,0 +1,145 @@
1
+ # encoding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ # A class responsible for storing a history of all lines entered by
9
+ # user when interacting with shell prompt.
10
+ #
11
+ # @api private
12
+ class History
13
+ include Enumerable
14
+ extend Forwardable
15
+
16
+ # Default maximum size
17
+ DEFAULT_SIZE = 32 << 4
18
+
19
+ def_delegators :@history, :size, :length, :to_s, :inspect
20
+
21
+ # Set and retrieve the maximum size of the buffer
22
+ attr_accessor :max_size
23
+
24
+ attr_reader :index
25
+
26
+ attr_accessor :cycle
27
+
28
+ attr_accessor :duplicates
29
+
30
+ attr_accessor :exclude
31
+
32
+ # Create a History buffer
33
+ #
34
+ # param [Integer] max_size
35
+ # the maximum size for history buffer
36
+ #
37
+ # param [Hash[Symbol]] options
38
+ # @option options [Boolean] :duplicates
39
+ # whether or not to store duplicates, true by default
40
+ #
41
+ # @api public
42
+ def initialize(max_size = DEFAULT_SIZE, options = {})
43
+ @max_size = max_size
44
+ @index = 0
45
+ @history = []
46
+ @duplicates = options.fetch(:duplicates) { true }
47
+ @exclude = options.fetch(:exclude) { proc {} }
48
+ @cycle = options.fetch(:cycle) { false }
49
+ yield self if block_given?
50
+ end
51
+
52
+ # Iterates over history lines
53
+ #
54
+ # @api public
55
+ def each
56
+ if block_given?
57
+ @history.each { |line| yield line }
58
+ else
59
+ @history.to_enum
60
+ end
61
+ end
62
+
63
+ # Add the last typed line to history buffer
64
+ #
65
+ # @param [String] line
66
+ #
67
+ # @api public
68
+ def push(line)
69
+ @history.delete(line) unless @duplicates
70
+ return if line.to_s.empty? || @exclude[line]
71
+
72
+ @history.shift if size >= max_size
73
+ @history << line
74
+ @index = @history.size - 1
75
+
76
+ self
77
+ end
78
+ alias << push
79
+
80
+ # Move the pointer to the next line in the history
81
+ #
82
+ # @api public
83
+ def next
84
+ return if size.zero?
85
+ if @index == size - 1
86
+ @index = 0 if @cycle
87
+ else
88
+ @index += 1
89
+ end
90
+ end
91
+
92
+ def next?
93
+ size > 0 && !(@index == size - 1 && !@cycle)
94
+ end
95
+
96
+ # Move the pointer to the previous line in the history
97
+ def previous
98
+ return if size.zero?
99
+ if @index.zero?
100
+ @index = size - 1 if @cycle
101
+ else
102
+ @index -= 1
103
+ end
104
+ end
105
+
106
+ def previous?
107
+ size > 0 && !(@index < 0 && !@cycle)
108
+ end
109
+
110
+ # Return line at the specified index
111
+ #
112
+ # @raise [IndexError] index out of range
113
+ #
114
+ # @api public
115
+ def [](index)
116
+ if index < 0
117
+ index += @history.size if index < 0
118
+ end
119
+ line = @history[index]
120
+ if line.nil?
121
+ raise IndexError, 'invalid index'
122
+ end
123
+ line.dup
124
+ end
125
+
126
+ # Get current line
127
+ #
128
+ # @api public
129
+ def get
130
+ return if size.zero?
131
+
132
+ self[@index]
133
+ end
134
+
135
+ # Empty all history lines
136
+ #
137
+ # @api public
138
+ def clear
139
+ @history.clear
140
+ @index = 0
141
+ end
142
+ end # History
143
+ end # Reader
144
+ end # Prompt
145
+ end # TTY
@@ -49,9 +49,13 @@ module TTY
49
49
  when keys[:down] then key.name = :down
50
50
  when keys[:left] then key.name = :left
51
51
  when keys[:right] then key.name = :right
52
+ # editing
52
53
  when keys[:clear] then key.name = :clear
53
54
  when keys[:end] then key.name = :end
54
55
  when keys[:home] then key.name = :home
56
+ when keys[:insert] then key.name = :insert
57
+ when keys[:page_up] then key.name = :page_up
58
+ when keys[:page_down] then key.name = :page_down
55
59
  when proc { |cs| ctrls.any? { |name| keys[name] == cs } }
56
60
  key.name = keys.key(char)
57
61
  key.ctrl = true
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+
3
+ require 'forwardable'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ class Line
9
+ extend Forwardable
10
+
11
+ def_delegators :@text, :size, :length, :to_s, :inspect,
12
+ :slice!, :empty?
13
+
14
+ attr_accessor :text
15
+
16
+ attr_accessor :cursor
17
+
18
+ def initialize(text = "")
19
+ @text = text
20
+ @cursor = [0, @text.length].max
21
+ yield self if block_given?
22
+ end
23
+
24
+ # Check if cursor reached beginning of the line
25
+ #
26
+ # @return [Boolean]
27
+ #
28
+ # @api public
29
+ def start?
30
+ @cursor == 0
31
+ end
32
+
33
+ # Check if cursor reached end of the line
34
+ #
35
+ # @return [Boolean]
36
+ #
37
+ # @api public
38
+ def end?
39
+ @cursor == @text.length
40
+ end
41
+
42
+ # Move line position to the left by n chars
43
+ #
44
+ # @api public
45
+ def left(n = 1)
46
+ @cursor = [0, @cursor - n].max
47
+ end
48
+
49
+ # Move line position to the right by n chars
50
+ #
51
+ # @api public
52
+ def right(n = 1)
53
+ @cursor = [@text.length, @cursor + n].min
54
+ end
55
+
56
+ # Move cursor to beginning position
57
+ #
58
+ # @api public
59
+ def move_to_start
60
+ @cursor = 0
61
+ end
62
+
63
+ # Move cursor to end position
64
+ #
65
+ # @api public
66
+ def move_to_end
67
+ @cursor = @text.length # put cursor outside of text
68
+ end
69
+
70
+ # Insert characters inside a line. When the lines exceeds
71
+ # maximum length, an extra space is added to accomodate index.
72
+ #
73
+ # @param [Integer] i
74
+ # the index to insert at
75
+ #
76
+ # @example
77
+ # text = 'aaa'
78
+ # line[5]= 'b'
79
+ # => 'aaa b'
80
+ #
81
+ # @api public
82
+ def []=(i, chars)
83
+ if i.is_a?(Range)
84
+ @text[i] = chars
85
+ @cursor += chars.length
86
+ return
87
+ end
88
+
89
+ if i <= 0
90
+ before_text = ''
91
+ after_text = @text.dup
92
+ elsif i == @text.length - 1
93
+ before_text = @text.dup
94
+ after_text = ''
95
+ elsif i > @text.length - 1
96
+ before_text = @text.dup
97
+ after_text = ?\s * (i - @text.length)
98
+ @cursor += after_text.length
99
+ else
100
+ before_text = @text[0..i-1].dup
101
+ after_text = @text[i..-1].dup
102
+ end
103
+
104
+ if i > @text.length - 1
105
+ @text = before_text << after_text << chars
106
+ else
107
+ @text = before_text << chars << after_text
108
+ end
109
+
110
+ @cursor = i + chars.length
111
+ end
112
+
113
+ # Read character
114
+ #
115
+ # @api public
116
+ def [](i)
117
+ @text[i]
118
+ end
119
+
120
+ # Replace current line with new text
121
+ #
122
+ # @param [String] text
123
+ #
124
+ # @api public
125
+ def replace(text)
126
+ @text = text
127
+ @cursor = @text.length # put cursor outside of text
128
+ end
129
+
130
+ # Insert char(s) at cursor position
131
+ #
132
+ # @api public
133
+ def insert(chars)
134
+ self[@cursor] = chars
135
+ end
136
+
137
+ # Add char and move cursor
138
+ #
139
+ # @api public
140
+ def <<(char)
141
+ @text << char
142
+ @cursor += 1
143
+ end
144
+
145
+ # Remove char from the line at current position
146
+ #
147
+ # @api public
148
+ def delete
149
+ @text.slice!(@cursor, 1)
150
+ end
151
+
152
+ # Remove char from the line in front of the cursor
153
+ #
154
+ # @api public
155
+ def remove
156
+ left
157
+ @text.slice!(@cursor, 1)
158
+ end
159
+ end # Line
160
+ end # Reader
161
+ end # Prompt
162
+ end # TTY
@@ -19,7 +19,7 @@ module TTY
19
19
  #
20
20
  # @api public
21
21
  def echo(is_on = true, &block)
22
- if is_on
22
+ if is_on || !@input.tty?
23
23
  yield
24
24
  else
25
25
  @input.noecho(&block)
@@ -32,7 +32,7 @@ module TTY
32
32
  #
33
33
  # @api public
34
34
  def raw(is_on = true, &block)
35
- if is_on
35
+ if is_on && @input.tty?
36
36
  @input.raw(&block)
37
37
  else
38
38
  yield
@@ -41,7 +41,11 @@ module TTY
41
41
  #
42
42
  # @api private
43
43
  def get_char(options)
44
- options[:echo] ? @input.getc : WinAPI.getch.chr
44
+ if options[:raw]
45
+ WinAPI.getch.chr
46
+ else
47
+ options[:echo] ? @input.getc : WinAPI.getch.chr
48
+ end
45
49
  end
46
50
  end # Console
47
51
  end # Reader
@@ -109,48 +109,54 @@ module TTY
109
109
  def render
110
110
  @prompt.print(@prompt.hide)
111
111
  until @done
112
- render_question
112
+ question = render_question
113
+ @prompt.print(question)
113
114
  @prompt.read_keypress
114
- refresh
115
+ refresh(question.lines.count)
115
116
  end
116
- render_question
117
- answer = render_answer
117
+ @prompt.print(render_question)
118
+ answer
118
119
  ensure
119
120
  @prompt.print(@prompt.show)
120
- answer
121
121
  end
122
122
 
123
123
  # Clear screen
124
124
  #
125
+ # @param [Integer] lines
126
+ # the lines to clear
127
+ #
125
128
  # @api private
126
- def refresh
127
- lines = @question.scan("\n").length + 2
129
+ def refresh(lines)
128
130
  @prompt.print(@prompt.clear_lines(lines))
129
131
  end
130
132
 
131
133
  # @return [Integer]
132
134
  #
133
135
  # @api private
134
- def render_answer
136
+ def answer
135
137
  range[@active]
136
138
  end
137
139
 
138
140
  # Render question with the slider
139
141
  #
142
+ # @return [String]
143
+ #
140
144
  # @api private
141
145
  def render_question
142
- header = "#{@prefix}#{@question} #{render_header}"
143
- @prompt.puts(header)
146
+ header = "#{@prefix}#{@question} #{render_header}\n"
144
147
  @first_render = false
145
- @prompt.print(render_slider) unless @done
148
+ header << render_slider unless @done
149
+ header
146
150
  end
147
151
 
148
152
  # Render actual answer or help
149
153
  #
154
+ # @return [String]
155
+ #
150
156
  # @api private
151
157
  def render_header
152
158
  if @done
153
- @prompt.decorate(render_answer.to_s, @active_color)
159
+ @prompt.decorate(answer.to_s, @active_color)
154
160
  elsif @first_render
155
161
  @prompt.decorate(HELP, @help_color)
156
162
  end