Home

Awesome

<div align="center"> <a href="https://ttytoolkit.org"><img width="130" src="https://github.com/piotrmurach/tty/raw/master/images/tty.png" alt="TTY Toolkit logo" /></a> </div>

TTY::Reader Gitter

Gem Version Actions CI Build status Maintainability Coverage Status Inline docs

A pure Ruby library that provides a set of methods for processing keyboard input in character, line and multiline modes. It maintains history of entered input with an ability to recall and re-edit those inputs. It lets you register to listen for keystroke events and trigger custom key events yourself.

TTY::Reader provides independent reader component for TTY toolkit.

Compatibility

The tty-reader is not compatible with the GNU Readline and doesn't aim to be. It originated from tty-prompt project to provide flexibility, independence from underlying operating system and Ruby like API interface for creating different prompts.

TTY::Reader forges its own path to provide features necessary for building line editing in terminal applications!

Features

Installation

Add this line to your application's Gemfile:

gem "tty-reader"

And then execute:

$ bundle

Or install it yourself as:

$ gem install tty-reader

Usage

In just a few lines you can recreate IRB prompt.

Initialize the reader:

reader = TTY::Reader.new

Then register to listen for key events, in this case listen for Ctrl-X or Esc keys to exit:

reader.on(:keyctrl_x, :keyescape) do
  puts "Exiting..."
  exit
end

Finally, keep asking user for line input with a => as a prompt:

loop do
  reader.read_line("=> ")
end

API

2.1 read_keypress

To read a single key stroke from the user use read_char or read_keypress:

reader.read_char
reader.read_keypress
reader.read_keypress(nonblock: true)

2.2 read_line

By default read_line works in raw mode which means it behaves like a line editor that allows you to edit each character, respond to control characters such as Control-A to Control-B or navigate through history.

For example, to read a single line terminated by a new line character use read_line like so:

reader.read_line

If you wish for the keystrokes to be interpreted by the terminal instead, use so called cooked mode by providing the :raw option set to false:

reader.read_line(raw: false)

Any non-interpreted characters received are written back to terminal, however you can stop this by using :echo option set to false:

reader.read_line(echo: false)

You can also provide a line prefix displayed before input by passing it as a first argument:

reader.read_line(">> ")
# >>

To pre-populate the line content for editing use :value option:

reader.read_line("> ", value: "edit me")
# > edit me

2.3 read_multiline

By default read_multiline works in raw mode which means it behaves like a multiline editor that allows you to edit each character, respond to control characters such as Control-A to Control-B or navigate through history.

For example, to read more than one line terminated by Ctrl+d or Ctrl+z use read_multiline:

reader.read_multiline
# => [ "line1", "line2", ... ]

If you wish for the keystrokes to be interpreted by the terminal instead, use so called cooked mode by providing the :raw option set to false:

reader.read_line(raw: false)

You can also provide a line prefix displayed before input by passing a string as a first argument:

reader.read_multiline(">> ")

2.4 on

You can register to listen on a key pressed events. This can be done by calling on with a event name(s):

reader.on(:keypress) { |event| .... }

or listen for multiple events:

reader.on(:keyctrl_x, :keyescape) { |event| ... }

The KeyEvent object is yielded to a block whenever a particular key event fires. The event responds to:

The value returns the actual key pressed and the line the content for the currently edited line or is empty.

The key is an object that responds to following messages:

For example, to add listen to vim like navigation keys, one would do the following:

reader.on(:keypress) do |event|
  if event.value == "j"
    ...
  end

  if event.value == "k"
    ...
  end
end

You can subscribe to more than one event:

reader.on(:keypress) { |event| ... }
      .on(:keydown)  { |event| ... }

2.5 subscribe

You can subscribe any object to listen for the emitted key events using the subscribe message. The listener would need to implement a method for every event it wishes to receive.

For example, if a MyListener class wishes to only listen for keypress event:

class MyListener
  def keypress(event)
    ...
  end
end

Then subscribing is done:

reader.subscribe(MyListener.new)

Alternatively, subscribe allows you to listen to events only for the duration of block execution like so:

reader.subscribe(MyListener) do
  ...
end

2.6 unsubscribe

You can unsubscribe any object from listening to the key events using the unsubscribe message:

reader.unsubscribe(my_listener)

2.7 trigger

The signature for triggering key events is trigger(event, args...). The first argument is a key event name followed by any number of actual values related to the event being triggered.

For example, to trigger :keydown event do:

reader.trigger(:keydown)

To add vim bindings for line editing you could discern between alphanumeric inputs like so:

reader.on(:keypress) do |event|
  if event.value == "j"
    reader.trigger(:keydown)
  end
  if evevnt.value == "k"
    reader.trigger(:keyup)
  end
end

2.8 supported events

The available key events for character input are:

The navigation related key events are:

The specific ctrl key events:

The key events for functional keys f* are:

3. Configuration

3.1. :interrupt

By default InputInterrupt error will be raised when the user hits the interrupt key(Control-C). However, you can customise this behaviour by passing the :interrupt option. The available options are:

For example, to send interrupt signal do:

reader = TTY::Reader.new(interrupt: :signal)

3.2. :track_history

The read_line and read_multiline provide history buffer that tracks all the lines entered during TTY::Reader.new interactions. The history buffer provides previous or next lines when user presses up/down arrows respectively. However, if you wish to disable this behaviour use :track_history option like so:

reader = TTY::Reader.new(track_history: false)

3.3. :history_cycle

This option determines whether the history buffer allows for infinite navigation. By default it is set to false. You can change this:

reader = TTY::Reader.new(history_cycle: true)

3.4. :history_duplicates

This option controls whether duplicate lines are stored in history. By default set to false. You can change this:

reader = TTY::Reader.new(history_duplicates: true)

3.5. :history_exclude

This option allows you to exclude lines from being stored in history. It accepts a Proc with a line as a first argument. By default it is set to exclude empty lines. To change this:

reader = TTY::Reader.new(history_exclude: ->(line) { ... })

3.6. :history_size

By default, the history buffer can store up to 512 lines. This can be changed with the :history_size configuration:

reader = TTY::Reader.new(history_size: 2048)

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-reader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

  1. Clone the project on GitHub
  2. Create a feature branch
  3. Submit a Pull Request

Important notes:

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the TTY::Reader project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Copyright

Copyright (c) 2017 Piotr Murach. See LICENSE for further details.