tty-reader 0.1.0 → 0.2.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.
@@ -0,0 +1,16 @@
1
+ require_relative '../lib/tty-reader'
2
+
3
+ reader = TTY::Reader.new
4
+
5
+ puts "Press a key (or Ctrl-X to exit)"
6
+
7
+ loop do
8
+ print "=> "
9
+ char = reader.read_keypress
10
+ if ?\C-x == char
11
+ puts "Exiting..."
12
+ exit
13
+ else
14
+ puts "#{char.inspect} [#{char.ord}] (hex: #{char.ord.to_s(16)})"
15
+ end
16
+ end
data/examples/line.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative '../lib/tty-reader'
2
+
3
+ reader = TTY::Reader.new
4
+
5
+ answer = reader.read_line(">> ")
6
+
7
+ puts "answer: #{answer}"
@@ -0,0 +1,7 @@
1
+ require_relative '../lib/tty-reader'
2
+
3
+ reader = TTY::Reader.new
4
+
5
+ answer = reader.read_multiline(">> ")
6
+
7
+ puts "\nanswer: #{answer}"
@@ -0,0 +1,6 @@
1
+ require_relative '../lib/tty-reader'
2
+
3
+ reader = TTY::Reader.new
4
+
5
+ answer = reader.read_line('=> ', echo: false)
6
+ puts "Answer: #{answer}"
data/examples/shell.rb ADDED
@@ -0,0 +1,12 @@
1
+ require_relative '../lib/tty-reader'
2
+
3
+ puts "*** TTY::Reader Shell ***"
4
+ puts "Press Ctrl-X to exit"
5
+
6
+ reader = TTY::Reader.new
7
+
8
+ reader.on(:keyctrl_x) { puts "Exiting..."; exit }
9
+
10
+ loop do
11
+ reader.read_line('=> ')
12
+ end
data/lib/tty/reader.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'tty-cursor'
5
+ require 'tty-screen'
4
6
  require 'wisper'
5
7
 
6
8
  require_relative 'reader/history'
@@ -15,7 +17,7 @@ module TTY
15
17
  #
16
18
  # Used internally to provide key and line reading functionality
17
19
  #
18
- # @api private
20
+ # @api public
19
21
  class Reader
20
22
  include Wisper::Publisher
21
23
 
@@ -24,6 +26,15 @@ module TTY
24
26
  # @api public
25
27
  InputInterrupt = Class.new(StandardError)
26
28
 
29
+ # Check if Windowz mode
30
+ #
31
+ # @return [Boolean]
32
+ #
33
+ # @api public
34
+ def self.windows?
35
+ ::File::ALT_SEPARATOR == '\\'
36
+ end
37
+
27
38
  attr_reader :input
28
39
 
29
40
  attr_reader :output
@@ -35,11 +46,13 @@ module TTY
35
46
 
36
47
  attr_reader :console
37
48
 
49
+ attr_reader :cursor
50
+
38
51
  # Key codes
39
52
  CARRIAGE_RETURN = 13
40
53
  NEWLINE = 10
41
- BACKSPACE = 127
42
- DELETE = 8
54
+ BACKSPACE = 8
55
+ DELETE = 127
43
56
 
44
57
  # Initialize a Reader
45
58
  #
@@ -54,18 +67,26 @@ module TTY
54
67
  # disable line history tracking, true by default
55
68
  #
56
69
  # @api public
57
- def initialize(input = $stdin, output = $stdout, options = {})
58
- @input = input
59
- @output = output
70
+ def initialize(**options)
71
+ @input = options.fetch(:input) { $stdin }
72
+ @output = options.fetch(:output) { $stdout }
60
73
  @interrupt = options.fetch(:interrupt) { :error }
61
74
  @env = options.fetch(:env) { ENV }
75
+
62
76
  @track_history = options.fetch(:track_history) { true }
77
+ @history_cycle = options.fetch(:history_cycle) { false }
78
+ exclude_proc = ->(line) { line.strip == '' }
79
+ @history_exclude = options.fetch(:history_exclude) { exclude_proc }
80
+ @history_duplicates = options.fetch(:history_duplicates) { false }
81
+
63
82
  @console = select_console(input)
64
83
  @history = History.new do |h|
65
- h.duplicates = false
66
- h.exclude = proc { |line| line.strip == '' }
84
+ h.cycle = @history_cycle
85
+ h.duplicates = @history_duplicates
86
+ h.exclude = @history_exclude
67
87
  end
68
88
  @stop = false # gathering input
89
+ @cursor = TTY::Cursor
69
90
 
70
91
  subscribe(self)
71
92
  end
@@ -74,7 +95,7 @@ module TTY
74
95
  #
75
96
  # @api private
76
97
  def select_console(input)
77
- if windows? && !env['TTY_TEST']
98
+ if self.class.windows? && !env['TTY_TEST']
78
99
  WinConsole.new(input)
79
100
  else
80
101
  Console.new(input)
@@ -133,14 +154,14 @@ module TTY
133
154
  def get_codes(options = {}, codes = [])
134
155
  opts = { echo: true, raw: false }.merge(options)
135
156
  char = console.get_char(opts)
136
- handle_interrupt if char == console.keys[:ctrl_c]
157
+ handle_interrupt if console.keys[char] == :ctrl_c
137
158
  return if char.nil?
138
159
  codes << char.ord
139
160
 
140
161
  condition = proc { |escape|
141
162
  (codes - escape).empty? ||
142
163
  (escape - codes).empty? &&
143
- !(64..126).include?(codes.last)
164
+ !(64..126).cover?(codes.last)
144
165
  }
145
166
 
146
167
  while console.escape_codes.any?(&condition)
@@ -162,38 +183,44 @@ module TTY
162
183
  # @return [String]
163
184
  #
164
185
  # @api public
165
- def read_line(*args)
166
- options = args.last.respond_to?(:to_hash) ? args.pop : {}
167
- prompt = args.empty? ? '' : args.pop
186
+ def read_line(prompt = '', **options)
168
187
  opts = { echo: true, raw: true }.merge(options)
169
- line = Line.new('')
170
- ctrls = console.keys.keys.grep(/ctrl/)
171
- clear_line = "\e[2K\e[1G"
188
+ line = Line.new(prompt, '')
189
+ screen_width = TTY::Screen.width
172
190
 
173
- while (codes = unbufferred { get_codes(opts) }) && (code = codes[0])
191
+ output.print(line.prompt)
192
+
193
+ while (codes = get_codes(opts)) && (code = codes[0])
174
194
  char = codes.pack('U*')
175
195
  trigger_key_event(char)
176
196
 
177
- if console.keys[:backspace] == char || BACKSPACE == code
178
- next if line.start?
179
- line.left
180
- line.delete
181
- elsif console.keys[:delete] == char || DELETE == code
197
+ break if [:ctrl_d, :ctrl_z].include?(console.keys[char])
198
+
199
+ if opts[:raw] && opts[:echo]
200
+ clear_display(line, screen_width)
201
+ end
202
+
203
+ if console.keys[char] == :backspace || BACKSPACE == code
204
+ if !line.start?
205
+ line.left
206
+ line.delete
207
+ end
208
+ elsif console.keys[char] == :delete || DELETE == code
182
209
  line.delete
183
- elsif [console.keys[:ctrl_d],
184
- console.keys[:ctrl_z]].include?(char)
185
- break
186
- elsif ctrls.include?(console.keys.key(char))
210
+ elsif console.keys[char].to_s =~ /ctrl_/
187
211
  # skip
188
- elsif console.keys[:up] == char
189
- next unless history_previous?
190
- line.replace(history_previous)
191
- elsif console.keys[:down] == char
212
+ elsif console.keys[char] == :up
213
+ line.replace(history_previous) if history_previous?
214
+ elsif console.keys[char] == :down
192
215
  line.replace(history_next? ? history_next : '')
193
- elsif console.keys[:left] == char
216
+ elsif console.keys[char] == :left
194
217
  line.left
195
- elsif console.keys[:right] == char
218
+ elsif console.keys[char] == :right
196
219
  line.right
220
+ elsif console.keys[char] == :home
221
+ line.move_to_start
222
+ elsif console.keys[char] == :end
223
+ line.move_to_end
197
224
  else
198
225
  if opts[:raw] && code == CARRIAGE_RETURN
199
226
  char = "\n"
@@ -202,28 +229,73 @@ module TTY
202
229
  line.insert(char)
203
230
  end
204
231
 
232
+ if (console.keys[char] == :backspace || BACKSPACE == code) && opts[:echo]
233
+ if opts[:raw]
234
+ output.print("\e[1X") unless line.start?
235
+ else
236
+ output.print(?\s + (line.start? ? '' : ?\b))
237
+ end
238
+ end
239
+
205
240
  if opts[:raw] && opts[:echo]
206
- output.print(clear_line)
207
- output.print(prompt + line.to_s)
241
+ output.print(line.to_s)
208
242
  if char == "\n"
209
243
  line.move_to_start
210
- elsif !line.end?
211
- output.print("\e[#{line.size - line.cursor}D")
244
+ elsif !line.end? # readjust cursor position
245
+ output.print(cursor.backward(line.text_size - line.cursor))
212
246
  end
213
247
  end
214
248
 
215
- break if (code == CARRIAGE_RETURN || code == NEWLINE)
216
-
217
- if (console.keys[:backspace] == char || BACKSPACE == code) && opts[:echo]
218
- if opts[:raw]
219
- output.print("\e[1X") unless line.start?
220
- else
221
- output.print(?\s + (line.start? ? '' : ?\b))
222
- end
249
+ if [CARRIAGE_RETURN, NEWLINE].include?(code)
250
+ output.puts unless opts[:echo]
251
+ break
223
252
  end
224
253
  end
225
- add_to_history(line.to_s.rstrip) if track_history?
226
- line.to_s
254
+ if track_history? && opts[:echo]
255
+ add_to_history(line.text.rstrip)
256
+ end
257
+ line.text
258
+ end
259
+
260
+ # Clear display for the current line input
261
+ #
262
+ # Handles clearing input that is longer than the current
263
+ # terminal width which allows copy & pasting long strings.
264
+ #
265
+ # @param [Line] line
266
+ # the line to display
267
+ # @param [Number] screen_width
268
+ # the terminal screen width
269
+ #
270
+ # @api private
271
+ def clear_display(line, screen_width)
272
+ total_lines = count_screen_lines(line.size, screen_width)
273
+ current_line = count_screen_lines(line.prompt_size + line.cursor, screen_width)
274
+ lines_down = total_lines - current_line
275
+
276
+ output.print(cursor.down(lines_down)) unless lines_down.zero?
277
+ output.print(cursor.clear_lines(total_lines))
278
+ end
279
+
280
+ # Count the number of screen lines given line takes up in terminal
281
+ #
282
+ # @param [Integer] line_or_size
283
+ # the current line or its length
284
+ # @param [Integer] screen_width
285
+ # the width of terminal screen
286
+ #
287
+ # @return [Integer]
288
+ #
289
+ # @api public
290
+ def count_screen_lines(line_or_size, screen_width = TTY::Screen.width)
291
+ line_size = if line_or_size.is_a?(Integer)
292
+ line_or_size
293
+ else
294
+ Line.sanitize(line_or_size).size
295
+ end
296
+ # new character + we don't want to add new line on screen_width
297
+ new_chars = self.class.windows? ? -1 : 1
298
+ 1 + [0, (line_size - new_chars) / screen_width].max
227
299
  end
228
300
 
229
301
  # Read multiple lines and return them in an array.
@@ -238,11 +310,11 @@ module TTY
238
310
  # @return [Array[String]]
239
311
  #
240
312
  # @api public
241
- def read_multiline(prompt = '')
313
+ def read_multiline(*args)
242
314
  @stop = false
243
315
  lines = []
244
316
  loop do
245
- line = read_line(prompt)
317
+ line = read_line(*args)
246
318
  break if !line || line == ''
247
319
  next if line !~ /\S/ && !@stop
248
320
  if block_given?
@@ -335,14 +407,5 @@ module TTY
335
407
  raise InputInterrupt
336
408
  end
337
409
  end
338
-
339
- # Check if Windowz mode
340
- #
341
- # @return [Boolean]
342
- #
343
- # @api public
344
- def windows?
345
- ::File::ALT_SEPARATOR == '\\'
346
- end
347
410
  end # Reader
348
411
  end # TTY
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative 'codes'
4
+ require_relative 'keys'
5
5
  require_relative 'mode'
6
6
 
7
7
  module TTY
@@ -27,7 +27,7 @@ module TTY
27
27
  def initialize(input)
28
28
  @input = input
29
29
  @mode = Mode.new(input)
30
- @keys = Codes.keys
30
+ @keys = Keys.ctrl_keys.merge(Keys.keys)
31
31
  @escape_codes = [[ESC.ord], CSI.bytes.to_a]
32
32
  end
33
33
 
@@ -35,13 +35,17 @@ module TTY
35
35
  # the maximum size for history buffer
36
36
  #
37
37
  # param [Hash[Symbol]] options
38
+ # @option options [Boolean] :cycle
39
+ # whether or not the history should cycle, false by default
38
40
  # @option options [Boolean] :duplicates
39
41
  # whether or not to store duplicates, true by default
42
+ # @option options [Boolean] :exclude
43
+ # a Proc to exclude items from storing in history
40
44
  #
41
45
  # @api public
42
- def initialize(max_size = DEFAULT_SIZE, options = {})
46
+ def initialize(max_size = DEFAULT_SIZE, **options)
43
47
  @max_size = max_size
44
- @index = 0
48
+ @index = nil
45
49
  @history = []
46
50
  @duplicates = options.fetch(:duplicates) { true }
47
51
  @exclude = options.fetch(:exclude) { proc {} }
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative 'keys'
5
+
4
6
  module TTY
5
7
  class Reader
6
8
  # Responsible for meta-data information about key pressed
@@ -27,16 +29,9 @@ module TTY
27
29
  # @api public
28
30
  def self.from(keys, char)
29
31
  key = Key.new
30
- ctrls = keys.keys.grep(/ctrl/)
32
+ key.name = (name = keys[char]) ? name : :ignore
31
33
 
32
34
  case char
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
35
  when proc { |c| c =~ /^[a-z]{1}$/ }
41
36
  key.name = :alpha
42
37
  when proc { |c| c =~ /^[A-Z]{1}$/ }
@@ -44,34 +39,8 @@ module TTY
44
39
  key.shift = true
45
40
  when proc { |c| c =~ /^\d+$/ }
46
41
  key.name = :num
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
- # editing
53
- when keys[:clear] then key.name = :clear
54
- when keys[:end] then key.name = :end
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
59
- when proc { |cs| ctrls.any? { |name| keys[name] == cs } }
60
- key.name = keys.key(char)
42
+ when proc { |cs| !Keys.ctrl_keys[cs].nil? }
61
43
  key.ctrl = true
62
- # f1 - f12
63
- when keys[:f1], keys[:f1_xterm] then key.name = :f1
64
- when keys[:f2], keys[:f2_xterm] then key.name = :f2
65
- when keys[:f3], keys[:f3_xterm] then key.name = :f3
66
- when keys[:f4], keys[:f4_xterm] then key.name = :f4
67
- when keys[:f5] then key.name = :f5
68
- when keys[:f6] then key.name = :f6
69
- when keys[:f7] then key.name = :f7
70
- when keys[:f8] then key.name = :f8
71
- when keys[:f9] then key.name = :f9
72
- when keys[:f10] then key.name = :f10
73
- when keys[:f11] then key.name = :f11
74
- when keys[:f12] then key.name = :f12
75
44
  end
76
45
 
77
46
  new(char, key)
@@ -0,0 +1,165 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module TTY
5
+ class Reader
6
+ # Mapping of escape codes to keys
7
+ module Keys
8
+ def ctrl_keys
9
+ {
10
+ ?\C-a => :ctrl_a,
11
+ ?\C-b => :ctrl_b,
12
+ ?\C-c => :ctrl_c,
13
+ ?\C-d => :ctrl_d,
14
+ ?\C-e => :ctrl_e,
15
+ ?\C-f => :ctrl_f,
16
+ ?\C-g => :ctrl_g,
17
+ ?\C-h => :ctrl_h, # identical to '\b'
18
+ ?\C-i => :ctrl_i, # identical to '\t'
19
+ ?\C-j => :ctrl_j, # identical to '\n'
20
+ ?\C-k => :ctrl_k,
21
+ ?\C-l => :ctrl_l,
22
+ ?\C-m => :ctrl_m, # identical to '\r'
23
+ ?\C-n => :ctrl_n,
24
+ ?\C-o => :ctrl_o,
25
+ ?\C-p => :ctrl_p,
26
+ ?\C-q => :ctrl_q,
27
+ ?\C-r => :ctrl_r,
28
+ ?\C-s => :ctrl_s,
29
+ ?\C-t => :ctrl_t,
30
+ ?\C-u => :ctrl_u,
31
+ ?\C-v => :ctrl_v,
32
+ ?\C-w => :ctrl_w,
33
+ ?\C-x => :ctrl_x,
34
+ ?\C-y => :ctrl_y,
35
+ ?\C-z => :ctrl_z,
36
+ ?\C-@ => :ctrl_space,
37
+ ?\C-| => :ctrl_backslash, # both Ctrl-| & Ctrl-\
38
+ ?\C-] => :ctrl_square_close,
39
+ "\e[1;5A" => :ctrl_up,
40
+ "\e[1;5B" => :ctrl_down,
41
+ "\e[1;5C" => :ctrl_right,
42
+ "\e[1;5D" => :ctrl_left
43
+ }
44
+ end
45
+ module_function :ctrl_keys
46
+
47
+ def keys
48
+ {
49
+ "\t" => :tab,
50
+ "\n" => :enter,
51
+ "\r" => :return,
52
+ "\e" => :escape,
53
+ " " => :space,
54
+ "\x7F" => :backspace,
55
+ "\e[1~" => :home,
56
+ "\e[2~" => :insert,
57
+ "\e[3~" => :delete,
58
+ "\e[3;2~" => :shift_delete,
59
+ "\e[3;5~" => :ctrl_delete,
60
+ "\e[4~" => :end,
61
+ "\e[5~" => :page_up,
62
+ "\e[6~" => :page_down,
63
+ "\e[7~" => :home, # xrvt
64
+ "\e[8~" => :end, # xrvt
65
+
66
+ "\e[A" => :up,
67
+ "\e[B" => :down,
68
+ "\e[C" => :right,
69
+ "\e[D" => :left,
70
+ "\e[E" => :clear,
71
+ "\e[H" => :home,
72
+ "\eOH" => :home,
73
+ "\e[F" => :end,
74
+ "\eOF" => :end,
75
+ "\e[Z" => :back_tab, # shift + tab
76
+
77
+ "\eOP" => :f1,
78
+ "\eOQ" => :f2,
79
+ "\eOR" => :f3,
80
+ "\eOS" => :f4,
81
+ "\e[[A" => :f1, # linux
82
+ "\e[[B" => :f2, # linux
83
+ "\e[[C" => :f3, # linux
84
+ "\e[[D" => :f4, # linux
85
+ "\e[[E" => :f5, # linux
86
+ "\e[11~" => :f1, # rxvt-unicode
87
+ "\e[12~" => :f2, # rxvt-unicode
88
+ "\e[13~" => :f3, # rxvt-unicode
89
+ "\e[14~" => :f4, # rxvt-unicode
90
+ "\e[15~" => :f5,
91
+ "\e[17~" => :f6,
92
+ "\e[18~" => :f7,
93
+ "\e[19~" => :f8,
94
+ "\e[20~" => :f9,
95
+ "\e[21~" => :f10,
96
+ "\e[23~" => :f11,
97
+ "\e[24~" => :f12,
98
+ "\e[25~" => :f13,
99
+ "\e[26~" => :f14,
100
+ "\e[28~" => :f15,
101
+ "\e[29~" => :f16,
102
+ "\e[31~" => :f17,
103
+ "\e[32~" => :f18,
104
+ "\e[33~" => :f19,
105
+ "\e[34~" => :f20,
106
+ # xterm
107
+ "\e[1;2P" => :f13,
108
+ "\e[2;2Q" => :f14,
109
+ "\e[1;2S" => :f16,
110
+ "\e[15;2~" => :f17,
111
+ "\e[17;2~" => :f18,
112
+ "\e[18;2~" => :f19,
113
+ "\e[19;2~" => :f20,
114
+ "\e[20;2~" => :f21,
115
+ "\e[21;2~" => :f22,
116
+ "\e[23;2~" => :f23,
117
+ "\e[24;2~" => :f24,
118
+
119
+ "\eOA" => :up,
120
+ "\eOB" => :down,
121
+ "\eOC" => :right,
122
+ "\eOD" => :left
123
+ }
124
+ end
125
+ module_function :keys
126
+
127
+ def win_keys
128
+ {
129
+ "\t" => :tab,
130
+ "\n" => :enter,
131
+ "\r" => :return,
132
+ "\e" => :escape,
133
+ " " => :space,
134
+ "\b" => :backspace,
135
+ [224, 71].pack('U*') => :home,
136
+ [224, 79].pack('U*') => :end,
137
+ [224, 82].pack('U*') => :insert,
138
+ [224, 83].pack('U*') => :delete,
139
+ [224, 73].pack('U*') => :page_up,
140
+ [224, 81].pack('U*') => :page_down,
141
+
142
+ [224, 72].pack('U*') => :up,
143
+ [224, 80].pack('U*') => :down,
144
+ [224, 77].pack('U*') => :right,
145
+ [224, 75].pack('U*') => :left,
146
+ [224, 83].pack('U*') => :clear,
147
+
148
+ "\x00;" => :f1,
149
+ "\x00<" => :f2,
150
+ "\x00" => :f3,
151
+ "\x00=" => :f4,
152
+ "\x00?" => :f5,
153
+ "\x00@" => :f6,
154
+ "\x00A" => :f7,
155
+ "\x00B" => :f8,
156
+ "\x00C" => :f9,
157
+ "\x00D" => :f10,
158
+ "\x00\x85" => :f11,
159
+ "\x00\x86" => :f12
160
+ }
161
+ end
162
+ module_function :win_keys
163
+ end # Keys
164
+ end # Reader
165
+ end # TTY