Class: TTY2::Reader

Inherits:
Object
  • Object
show all
Includes:
Wisper::Publisher
Defined in:
lib/tty2/reader.rb,
lib/tty2/reader/keys.rb,
lib/tty2/reader/line.rb,
lib/tty2/reader/mode.rb,
lib/tty2/reader/console.rb,
lib/tty2/reader/history.rb,
lib/tty2/reader/version.rb,
lib/tty2/reader/win_api.rb,
lib/tty2/reader/completer.rb,
lib/tty2/reader/key_event.rb,
lib/tty2/reader/completions.rb,
lib/tty2/reader/win_console.rb,
lib/tty2/reader/completion_event.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: Completer, CompletionEvent, Completions, Console, History, Key, KeyEvent, Line, Mode, WinConsole

Constant Summary collapse

BACKSPACE =

Key codes

8
TAB =
9
NEWLINE =
10
CARRIAGE_RETURN =
13
DELETE =
127
EXIT_KEYS =

Keys that terminate input

%i[ctrl_d ctrl_z].freeze
END_WITH_LINE_BREAK =

Pattern to check if line ends with a line break character

/(\r|\n)$/.freeze
InputInterrupt =

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

Class.new(Interrupt)
VERSION =
"0.9.0.3"

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_size: History::DEFAULT_SIZE, history_duplicates: false, completion_handler: nil, completion_suffix: "", completion_cycling: true) ⇒ 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

  • completion_handler (Proc) (defaults to: nil)

    the hanlder for finding word completion suggestions

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

    the suffix to add to suggested word completion

  • completion_cycling (Boolean) (defaults to: true)

    enable cycling through completions, true by default



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/tty2/reader.rb', line 103

def initialize(input: $stdin, output: $stdout, interrupt: :error,
               env: ENV, track_history: true, history_cycle: false,
               history_exclude: History::DEFAULT_EXCLUDE,
               history_size: History::DEFAULT_SIZE,
               history_duplicates: false,
               completion_handler: nil, completion_suffix: "",
               completion_cycling: true)
  @input = input
  @output = output
  @interrupt = interrupt
  @env = env
  @track_history = track_history
  @history_cycle = history_cycle
  @history_exclude = history_exclude
  @history_duplicates = history_duplicates
  @history_size = history_size
  @completion_handler = completion_handler
  @completion_suffix = completion_suffix
  @completion_cycling = completion_cycling
  @completer = Completer.new(handler: completion_handler,
                             suffix: completion_suffix,
                             cycling: completion_cycling)
  @console = select_console(input)
  @history = History.new(history_size) do |h|
    h.cycle = history_cycle
    h.duplicates = history_duplicates
    h.exclude = history_exclude
  end
  @cursor = TTY::Cursor
end

Instance Attribute Details

#completion_cyclingObject



71
72
73
# File 'lib/tty2/reader.rb', line 71

def completion_cycling
  @completion_cycling
end

#completion_handlerObject

The handler for finding word completion suggestions



64
65
66
# File 'lib/tty2/reader.rb', line 64

def completion_handler
  @completion_handler
end

#completion_suffixObject

The suffix to add to suggested word completion



69
70
71
# File 'lib/tty2/reader.rb', line 69

def completion_suffix
  @completion_suffix
end

#consoleObject (readonly)



73
74
75
# File 'lib/tty2/reader.rb', line 73

def console
  @console
end

#cursorObject (readonly)



75
76
77
# File 'lib/tty2/reader.rb', line 75

def cursor
  @cursor
end

#envObject (readonly)



56
57
58
# File 'lib/tty2/reader.rb', line 56

def env
  @env
end

#inputObject (readonly)



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

def input
  @input
end

#outputObject (readonly)



54
55
56
# File 'lib/tty2/reader.rb', line 54

def output
  @output
end

#track_historyObject (readonly) Also known as: track_history?



58
59
60
# File 'lib/tty2/reader.rb', line 58

def track_history
  @track_history
end

Class Method Details

.windows?Boolean

Check if Windowz mode

Returns:

  • (Boolean)


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

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

Instance Method Details

#add_to_history(line) ⇒ 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.

Add a line to history

Parameters:

  • line (String)


529
530
531
# File 'lib/tty2/reader.rb', line 529

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



435
436
437
438
439
440
441
442
# File 'lib/tty2/reader.rb', line 435

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)


454
455
456
457
458
459
460
461
462
463
# File 'lib/tty2/reader.rb', line 454

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


272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/tty2/reader.rb', line 272

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_nextString

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.

Move history to the next line

Returns:

  • (String)

    the next line



548
549
550
551
# File 'lib/tty2/reader.rb', line 548

def history_next
  @history.next
  @history.get
end

#history_next?Boolean

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.

Check if history has next line

Parameters:

  • (Boolean)

Returns:

  • (Boolean)


538
539
540
# File 'lib/tty2/reader.rb', line 538

def history_next?
  @history.next?
end

#history_previous(skip: false) ⇒ String

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.

Move history to the previous line

Parameters:

  • skip (Boolean) (defaults to: false)

    whether or not to move history index

Returns:

  • (String)

    the previous line



571
572
573
574
# File 'lib/tty2/reader.rb', line 571

def history_previous(skip: false)
  @history.previous unless skip
  @history.get
end

#history_previous?Boolean

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.

Check if history has previous line

Returns:

  • (Boolean)


558
559
560
# File 'lib/tty2/reader.rb', line 558

def history_previous?
  @history.previous?
end

#inspectString

Inspect class name and public attributes

Returns:

  • (String)


581
582
583
# File 'lib/tty2/reader.rb', line 581

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

#old_subcribeObject



167
# File 'lib/tty2/reader.rb', line 167

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)


247
248
249
250
251
252
253
254
255
# File 'lib/tty2/reader.rb', line 247

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, exit_keys: nil) ⇒ 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

  • exit_keys (Array<Symbol>) (defaults to: nil)

    the custom keys to exit line editing

  • [Boolean] (Hash)

    a customizable set of options

Returns:

  • (String)


313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/tty2/reader.rb', line 313

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

  output.print(line)

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

    if exit_keys && exit_keys.include?(key_name)
      trigger_key_event(char, line: line)
      break
    end

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

    if (key_name == :tab || code == TAB || key_name == :shift_tab) &&
       completion_handler
      initial = previous_key_name != :tab && previous_key_name != :shift_tab
      direction = key_name == :shift_tab ? :previous : :next
      if completion = @completer.complete(line, initial: initial,
                                                direction: direction)
        trigger_completion_event(completion, line.to_s)
      end
    elsif key_name == :escape && completion_handler &&
          (previous_key_name == :tab || previous_key_name == :shift_tab)
      @completer.cancel(line)
    elsif key_name == :backspace || code == BACKSPACE
      if !line.start?
        line.left
        line.delete
      end
    elsif key_name == :delete || code == DELETE
      line.delete
    elsif key_name.to_s =~ /ctrl_/
      # skip
    elsif key_name == :up
      @history.replace(line.text) if history_in_use
      if history_previous?
        line.replace(history_previous(skip: !history_in_use))
        history_in_use = true
      end
    elsif key_name == :down
      @history.replace(line.text) if history_in_use
      if history_next?
        line.replace(history_next)
      elsif history_in_use
        line.replace(buffer)
        history_in_use = false
      end
    elsif key_name == :left
      line.left
    elsif key_name == :right
      line.right
    elsif key_name == :home
      line.move_to_start
    elsif key_name == :end
      line.move_to_end
    else
      if raw && [CARRIAGE_RETURN, NEWLINE].include?(code)
        char = "\n"
        line.move_to_end
      end
      line.insert(char)
      buffer = line.text unless history_in_use
    end

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

    previous_key_name = key_name

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

    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, exit_keys: EXIT_KEYS) {|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

  • exit_keys (Array<Symbol>) (defaults to: EXIT_KEYS)

    the custom keys to exit line editing

  • [Boolean] (Hash)

    a customizable set of options

Yields:

  • (String)

    line

Returns:

  • (Array[String])


487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/tty2/reader.rb', line 487

def read_multiline(prompt = "", value: "", echo: true, raw: true,
                   nonblock: false, exit_keys: EXIT_KEYS)
  lines = []
  stop = false
  clear_value = !value.to_s.empty?

  loop do
    line = read_line(prompt, value: value, echo: echo, raw: raw,
                             nonblock: nonblock, exit_keys: exit_keys).to_s
    if clear_value
      clear_value = false
      value = ""
    end
    break if line.empty?

    stop = line.match(END_WITH_LINE_BREAK).nil?
    next if line !~ /\S/ && !stop

    if block_given?
      yield(line)
    else
      lines << line
    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



207
208
209
210
211
212
213
# File 'lib/tty2/reader.rb', line 207

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)


177
178
179
180
181
182
183
184
185
# File 'lib/tty2/reader.rb', line 177

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



520
521
522
# File 'lib/tty2/reader.rb', line 520

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

#unbufferred(&block) ⇒ Object

Get input in unbuffered mode.

Examples:

unbufferred do
  ...
end


223
224
225
226
227
228
229
230
# File 'lib/tty2/reader.rb', line 223

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)


195
196
197
198
199
200
201
202
# File 'lib/tty2/reader.rb', line 195

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