tty-reader 0.1.0 → 0.2.0

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