Class: TTY::Reader

Inherits:
Object
  • Object
show all
Includes:
Wisper::Publisher
Defined in:
lib/tty/reader.rb,
lib/tty/reader/keys.rb,
lib/tty/reader/line.rb,
lib/tty/reader/mode.rb,
lib/tty/reader/console.rb,
lib/tty/reader/history.rb,
lib/tty/reader/version.rb,
lib/tty/reader/win_api.rb,
lib/tty/reader/key_event.rb,
lib/tty/reader/win_console.rb

Overview

A class responsible for reading character input from STDIN

Used internally to provide key and line reading functionality

Defined Under Namespace

Modules: Keys, WinAPI Classes: Console, History, Key, KeyEvent, Line, Mode, WinConsole

Constant Summary collapse

InputInterrupt =

Raised when the user hits the interrupt key(Control-C)

Class.new(Interrupt)
CARRIAGE_RETURN =

Key codes

13
NEWLINE =
10
BACKSPACE =
8
DELETE =
127
VERSION =
"0.8.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**options) ⇒ Reader

Initialize a Reader

Parameters:

  • input (IO)

    the input stream

  • output (IO)

    the output stream

  • options (Hash)

Options Hash (**options):

  • :interrupt (Symbol)

    handling of Ctrl+C key out of :signal, :exit, :noop

  • :track_history (Boolean)

    disable line history tracking, true by default


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/tty/reader.rb', line 69

def initialize(**options)
  @input     = options.fetch(:input) { $stdin }
  @output    = options.fetch(:output) { $stdout }
  @interrupt = options.fetch(:interrupt) { :error }
  @env       = options.fetch(:env) { ENV }

  @track_history = options.fetch(:track_history) { true }
  @history_cycle = options.fetch(:history_cycle) { false }
  exclude_proc   = ->(line) { line.strip == "" }
  @history_exclude    = options.fetch(:history_exclude) { exclude_proc }
  @history_duplicates = options.fetch(:history_duplicates) { false }

  @console   = select_console(input)
  @history   = History.new do |h|
    h.cycle = @history_cycle
    h.duplicates = @history_duplicates
    h.exclude = @history_exclude
  end
  @stop = false # gathering input
  @cursor = TTY::Cursor

  subscribe(self)
end

Instance Attribute Details

#consoleObject (readonly)


46
47
48
# File 'lib/tty/reader.rb', line 46

def console
  @console
end

#cursorObject (readonly)


48
49
50
# File 'lib/tty/reader.rb', line 48

def cursor
  @cursor
end

#envObject (readonly)


41
42
43
# File 'lib/tty/reader.rb', line 41

def env
  @env
end

#inputObject (readonly)


37
38
39
# File 'lib/tty/reader.rb', line 37

def input
  @input
end

#outputObject (readonly)


39
40
41
# File 'lib/tty/reader.rb', line 39

def output
  @output
end

#track_historyObject (readonly) Also known as: track_history?


43
44
45
# File 'lib/tty/reader.rb', line 43

def track_history
  @track_history
end

Class Method Details

.windows?Boolean

Check if Windowz mode

Returns:

  • (Boolean)

33
34
35
# File 'lib/tty/reader.rb', line 33

def self.windows?
  ::File::ALT_SEPARATOR == "\\"
end

Instance Method Details

#add_to_history(line) ⇒ Object


393
394
395
# File 'lib/tty/reader.rb', line 393

def add_to_history(line)
  @history.push(line)
end

#clear_display(line, screen_width) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Clear display for the current line input

Handles clearing input that is longer than the current terminal width which allows copy & pasting long strings.

Parameters:

  • line (Line)

    the line to display

  • screen_width (Number)

    the terminal screen width


318
319
320
321
322
323
324
325
# File 'lib/tty/reader.rb', line 318

def clear_display(line, screen_width)
  total_lines  = count_screen_lines(line.size, screen_width)
  current_line = count_screen_lines(line.prompt_size + line.cursor, screen_width)
  lines_down = total_lines - current_line

  output.print(cursor.down(lines_down)) unless lines_down.zero?
  output.print(cursor.clear_lines(total_lines))
end

#count_screen_lines(line_or_size, screen_width = TTY::Screen.width) ⇒ Integer

Count the number of screen lines given line takes up in terminal

Parameters:

  • line_or_size (Integer)

    the current line or its length

  • screen_width (Integer) (defaults to: TTY::Screen.width)

    the width of terminal screen

Returns:

  • (Integer)

337
338
339
340
341
342
343
344
345
346
# File 'lib/tty/reader.rb', line 337

def count_screen_lines(line_or_size, screen_width = TTY::Screen.width)
  line_size = if line_or_size.is_a?(Integer)
                line_or_size
              else
                Line.sanitize(line_or_size).size
              end
  # new character + we don't want to add new line on screen_width
  new_chars = self.class.windows? ? -1 : 1
  1 + [0, (line_size - new_chars) / screen_width].max
end

#get_codes(options = {}, codes = []) ⇒ Array[Integer]

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get input code points

Parameters:

  • options (Hash[Symbol]) (defaults to: {})
  • codes (Array[Integer]) (defaults to: [])

Returns:

  • (Array[Integer])

190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/tty/reader.rb', line 190

def get_codes(options = {}, codes = [])
  opts = { echo: true, raw: false }.merge(options)
  char = console.get_char(opts)
  handle_interrupt if console.keys[char] == :ctrl_c
  return if char.nil?
  codes << char.ord

  condition = proc { |escape|
    (codes - escape).empty? ||
    (escape - codes).empty? &&
    !(64..126).cover?(codes.last)
  }

  while console.escape_codes.any?(&condition)
    char_codes = get_codes(options.merge(nonblock: true), codes)
    break if char_codes.nil?
  end

  codes
end

#history_nextObject


401
402
403
404
# File 'lib/tty/reader.rb', line 401

def history_next
  @history.next
  @history.get
end

#history_next?Boolean

Returns:

  • (Boolean)

397
398
399
# File 'lib/tty/reader.rb', line 397

def history_next?
  @history.next?
end

#history_previousObject


410
411
412
413
414
# File 'lib/tty/reader.rb', line 410

def history_previous
  line = @history.get
  @history.previous
  line
end

#history_previous?Boolean

Returns:

  • (Boolean)

406
407
408
# File 'lib/tty/reader.rb', line 406

def history_previous?
  @history.previous?
end

#inspectString

Inspect class name and public attributes

Returns:

  • (String)

420
421
422
# File 'lib/tty/reader.rb', line 420

def inspect
  "#<#{self.class}: @input=#{input}, @output=#{output}>"
end

#keyctrl_dObject Also known as: keyctrl_z

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Capture Ctrl+d and Ctrl+z key events


388
389
390
# File 'lib/tty/reader.rb', line 388

def keyctrl_d(*)
  @stop = true
end

#old_subcribeObject


93
# File 'lib/tty/reader.rb', line 93

alias old_subcribe subscribe

#read_keypress(options = {}) ⇒ String Also known as: read_char

Read a keypress including invisible multibyte codes and return a character as a string. Nothing is echoed to the console. This call will block for a single keypress, but will not wait for Enter to be pressed.

Parameters:

  • options (Hash[Symbol]) (defaults to: {})

Options Hash (options):

  • echo (Boolean)

    whether to echo chars back or not, defaults to false

  • raw (Boolean)

    whenther raw mode enabled, defaults to true

Returns:

  • (String)

172
173
174
175
176
177
178
179
# File 'lib/tty/reader.rb', line 172

def read_keypress(options = {})
  opts  = { echo: false, raw: true }.merge(options)
  codes = unbufferred { get_codes(opts) }
  char  = codes ? codes.pack("U*") : nil

  trigger_key_event(char) if char
  char
end

#read_line(prompt = "", **options) ⇒ String

Get a single line from STDIN. Each key pressed is echoed back to the shell. The input terminates when enter or return key is pressed.

Parameters:

  • prompt (String) (defaults to: "")

    the prompt to display before input

  • value (String)

    the value to pre-populate line with

  • echo (Boolean)

    if true echo back characters, output nothing otherwise

Returns:

  • (String)

227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/tty/reader.rb', line 227

def read_line(prompt = "", **options)
  opts = { echo: true, raw: true }.merge(options)
  value = options.fetch(:value, "")
  line = Line.new(value, prompt: prompt)
  screen_width = TTY::Screen.width

  output.print(line)

  while (codes = get_codes(opts)) && (code = codes[0])
    char = codes.pack("U*")

    if [:ctrl_d, :ctrl_z].include?(console.keys[char])
      trigger_key_event(char, line: line.to_s)
      break
    end

    if opts[:raw] && opts[:echo]
      clear_display(line, screen_width)
    end

    if console.keys[char] == :backspace || BACKSPACE == code
      if !line.start?
        line.left
        line.delete
      end
    elsif console.keys[char] == :delete || DELETE == code
      line.delete
    elsif console.keys[char].to_s =~ /ctrl_/
      # skip
    elsif console.keys[char] == :up
      line.replace(history_previous) if history_previous?
    elsif console.keys[char] == :down
      line.replace(history_next? ? history_next : "")
    elsif console.keys[char] == :left
      line.left
    elsif console.keys[char] == :right
      line.right
    elsif console.keys[char] == :home
      line.move_to_start
    elsif console.keys[char] == :end
      line.move_to_end
    else
      if opts[:raw] && code == CARRIAGE_RETURN
        char = "\n"
        line.move_to_end
      end
      line.insert(char)
    end

    if (console.keys[char] == :backspace || BACKSPACE == code) && opts[:echo]
      if opts[:raw]
        output.print("\e[1X") unless line.start?
      else
        output.print(?\s + (line.start? ? "" : ?\b))
      end
    end

    # trigger before line is printed to allow for line changes
    trigger_key_event(char, line: line.to_s)

    if opts[:raw] && opts[:echo]
      output.print(line.to_s)
      if char == "\n"
        line.move_to_start
      elsif !line.end? # readjust cursor position
        output.print(cursor.backward(line.text_size - line.cursor))
      end
    end

    if [CARRIAGE_RETURN, NEWLINE].include?(code)
      output.puts unless opts[:echo]
      break
    end
  end
  if track_history? && opts[:echo]
    add_to_history(line.text.rstrip)
  end
  line.text
end

#read_multiline(*args) {|String| ... } ⇒ Array[String] Also known as: read_lines

Read multiple lines and return them in an array. Skip empty lines in the returned lines array. The input gathering is terminated by Ctrl+d or Ctrl+z.

Parameters:

  • prompt (String)

    the prompt displayed before the input

Yields:

  • (String)

    line

Returns:

  • (Array[String])

360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/tty/reader.rb', line 360

def read_multiline(*args)
  @stop = false
  lines = []
  loop do
    line = read_line(*args)
    break if !line || line == ""
    next  if line !~ /\S/ && !@stop
    if block_given?
      yield(line) unless line.to_s.empty?
    else
      lines << line unless line.to_s.empty?
    end
    break if @stop
  end
  lines
end

#select_console(input) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Select appropriate console


133
134
135
136
137
138
139
# File 'lib/tty/reader.rb', line 133

def select_console(input)
  if self.class.windows? && !env["TTY_TEST"]
    WinConsole.new(input)
  else
    Console.new(input)
  end
end

#subscribe(listener, options = {}) ⇒ self|yield

Subscribe to receive key events

Examples:

reader.subscribe(MyListener.new)

Returns:

  • (self|yield)

103
104
105
106
107
108
109
110
111
# File 'lib/tty/reader.rb', line 103

def subscribe(listener, options = {})
  old_subcribe(listener, options)
  object = self
  if block_given?
    object = yield
    unsubscribe(listener)
  end
  object
end

#trigger(event, *args) ⇒ Object

Expose event broadcasting


381
382
383
# File 'lib/tty/reader.rb', line 381

def trigger(event, *args)
  publish(event, *args)
end

#unbufferred(&block) ⇒ Object

Get input in unbuffered mode.

Examples:

unbufferred do
  ...
end

149
150
151
152
153
154
155
156
# File 'lib/tty/reader.rb', line 149

def unbufferred(&block)
  bufferring = output.sync
  # Immediately flush output
  output.sync = true
  block[] if block_given?
ensure
  output.sync = bufferring
end

#unsubscribe(listener) ⇒ void

This method returns an undefined value.

Unsubscribe from receiving key events

Examples:

reader.unsubscribe(my_listener)

121
122
123
124
125
126
127
128
# File 'lib/tty/reader.rb', line 121

def unsubscribe(listener)
  registry = send(:local_registrations)
  registry.each do |object|
    if object.listener.equal?(listener)
      registry.delete(object)
    end
  end
end