tty-prompt 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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