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

CARRIAGE_RETURN =

Key codes

13
NEWLINE =
10
BACKSPACE =
8
DELETE =
127
EXIT_KEYS =

Keys that terminate input

[:ctrl_d, :ctrl_z]
InputInterrupt =

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

Class.new(Interrupt)
VERSION =
"0.9.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout, interrupt: :error, env: ENV, track_history: true, history_cycle: false, history_exclude: History::DEFAULT_EXCLUDE, history_duplicates: false) ⇒ Reader

Initialize a Reader

Parameters:

  • input (IO) (defaults to: $stdin)

    the input stream

  • output (IO) (defaults to: $stdout)

    the output stream

  • interrupt (Symbol) (defaults to: :error)

    the way to handle the Ctrl+C key out of :signal, :exit, :noop

  • env (Hash) (defaults to: ENV)

    the environment variables

  • track_history (Boolean) (defaults to: true)

    disable line history tracking, true by default

  • history_cycle (Boolean) (defaults to: false)

    allow cycling through history, false by default

  • history_duplicates (Boolean) (defaults to: false)

    allow duplicate entires, false by default

  • history_exclude (Proc) (defaults to: History::DEFAULT_EXCLUDE)

    exclude lines from history, by default all lines are stored


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/tty/reader.rb', line 79

def initialize(input: $stdin, output: $stdout, interrupt: :error,
               env: ENV, track_history: true, history_cycle: false,
               history_exclude: History::DEFAULT_EXCLUDE,
               history_duplicates: false)
  @input = input
  @output = output
  @interrupt = interrupt
  @env = env
  @track_history = track_history
  @history_cycle = history_cycle
  @history_exclude = history_exclude
  @history_duplicates = history_duplicates

  @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)


55
56
57
# File 'lib/tty/reader.rb', line 55

def console
  @console
end

#cursorObject (readonly)


57
58
59
# File 'lib/tty/reader.rb', line 57

def cursor
  @cursor
end

#envObject (readonly)


50
51
52
# File 'lib/tty/reader.rb', line 50

def env
  @env
end

#inputObject (readonly)


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

def input
  @input
end

#outputObject (readonly)


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

def output
  @output
end

#track_historyObject (readonly) Also known as: track_history?


52
53
54
# File 'lib/tty/reader.rb', line 52

def track_history
  @track_history
end

Class Method Details

.windows?Boolean

Check if Windowz mode

Returns:

  • (Boolean)

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

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

Instance Method Details

#add_to_history(line) ⇒ Object


433
434
435
# File 'lib/tty/reader.rb', line 433

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


343
344
345
346
347
348
349
350
# File 'lib/tty/reader.rb', line 343

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)

362
363
364
365
366
367
368
369
370
371
# File 'lib/tty/reader.rb', line 362

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(echo: true, raw: false, nonblock: false, 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:

  • echo (Boolean) (defaults to: true)

    whether to echo chars back or not, defaults to false

  • codes (Array[Integer]) (defaults to: [])

    the currently read char code points

  • [Boolean] (Hash)

    a customizable set of options

Returns:

  • (Array[Integer])

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/tty/reader.rb', line 209

def get_codes(echo: true, raw: false, nonblock: false, codes: [])
  char = console.get_char(echo: echo, raw: raw, nonblock: nonblock)
  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(echo: echo, raw: raw,
                           nonblock: true, codes: codes)
    break if char_codes.nil?
  end

  codes
end

#history_nextObject


441
442
443
444
# File 'lib/tty/reader.rb', line 441

def history_next
  @history.next
  @history.get
end

#history_next?Boolean

Returns:

  • (Boolean)

437
438
439
# File 'lib/tty/reader.rb', line 437

def history_next?
  @history.next?
end

#history_previousObject


450
451
452
453
454
# File 'lib/tty/reader.rb', line 450

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

#history_previous?Boolean

Returns:

  • (Boolean)

446
447
448
# File 'lib/tty/reader.rb', line 446

def history_previous?
  @history.previous?
end

#inspectString

Inspect class name and public attributes

Returns:

  • (String)

460
461
462
# File 'lib/tty/reader.rb', line 460

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


428
429
430
# File 'lib/tty/reader.rb', line 428

def keyctrl_d(*)
  @stop = true
end

#old_subcribeObject


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

alias old_subcribe subscribe

#read_keypress(echo: false, raw: true, nonblock: false) ⇒ 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:

  • echo (Boolean) (defaults to: false)

    whether to echo chars back or not, defaults to false

  • [Boolean] (Hash)

    a customizable set of options

Returns:

  • (String)

184
185
186
187
188
189
190
191
192
# File 'lib/tty/reader.rb', line 184

def read_keypress(echo: false, raw: true, nonblock: false)
  codes = unbufferred do
    get_codes(echo: echo, raw: raw, nonblock: nonblock)
  end
  char = codes ? codes.pack("U*") : nil

  trigger_key_event(char) if char
  char
end

#read_line(prompt = "", value: "", echo: true, raw: true, nonblock: false) ⇒ 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) (defaults to: "")

    the value to pre-populate line with

  • echo (Boolean) (defaults to: true)

    whether to echo chars back or not, defaults to false

  • [Boolean] (Hash)

    a customizable set of options

Returns:

  • (String)

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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/tty/reader.rb', line 248

def read_line(prompt = "", value: "", echo: true, raw: true, nonblock: false)
  line = Line.new(value, prompt: prompt)
  screen_width = TTY::Screen.width
  buffer = ""

  output.print(line)

  while (codes = get_codes(echo: echo, raw: raw, nonblock: nonblock)) &&
        (code = codes[0])
    char = codes.pack("U*")

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

    if raw && echo
      clear_display(line, screen_width)
    end

    if console.keys[char] == :backspace || code == BACKSPACE
      if !line.start?
        line.left
        line.delete
      end
    elsif console.keys[char] == :delete || code == DELETE
      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 : buffer) if track_history?
    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 raw && code == CARRIAGE_RETURN
        char = "\n"
        line.move_to_end
      end
      line.insert(char)
      buffer = line.text
    end

    if (console.keys[char] == :backspace || code == BACKSPACE) && echo
      if 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 raw && 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)
      buffer = ""
      output.puts unless echo
      break
    end
  end

  if track_history? && echo
    add_to_history(line.text.rstrip)
  end

  line.text
end

#read_multiline(prompt = "", value: "", echo: true, raw: true, nonblock: false) {|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) (defaults to: "")

    the prompt displayed before the input

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

    the value to pre-populate line with

  • echo (Boolean) (defaults to: true)

    whether to echo chars back or not, defaults to false

  • [Boolean] (Hash)

    a customizable set of options

Yields:

  • (String)

    line

Returns:

  • (Array[String])

393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/tty/reader.rb', line 393

def read_multiline(prompt = "", value: "", echo: true, raw: true,
                   nonblock: false)
  @stop = false
  lines = []
  empty_str = ""

  loop do
    line = read_line(prompt, value: value, echo: echo, raw: raw,
                             nonblock: nonblock)
    value = empty_str unless value.empty? # reset
    break if !line || line == empty_str
    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


144
145
146
147
148
149
150
# File 'lib/tty/reader.rb', line 144

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)

114
115
116
117
118
119
120
121
122
# File 'lib/tty/reader.rb', line 114

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


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

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

#unbufferred(&block) ⇒ Object

Get input in unbuffered mode.

Examples:

unbufferred do
  ...
end

160
161
162
163
164
165
166
167
# File 'lib/tty/reader.rb', line 160

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)

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

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