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.
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!
- Pure Ruby
- Reading single keypress
- Line editing
- Reading multiline input
- Ability to register for keystroke events
- Track input history
- No global state
- Works on Linux, OS X, FreeBSD and Windows
- Supports Ruby versions
>= 2.0.0
& JRuby
Add this line to your application's Gemfile:
gem 'tty-reader'
And then execute:
$ bundle
Or install it yourself as:
$ gem install tty-reader
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
To read a single key stroke from the user use read_char
or read_keypress
:
reader.read_char
reader.read_keypress
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
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(">> ")
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:
key
- key pressedvalue
- value of the key pressedline
- the content of the currently edited line, empty otherwise
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:
name
- the name of the event such as :up, :down, letter or digitmeta
- true if event is non-standard key associatedshift
- true if shift has been pressed with the keyctrl
- true if ctrl has been pressed with the key
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:
prompt.on(:keypress) { |key| ... }
.on(:keydown) { |key| ... }
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
You can unsubscribe any object from listening to the key events using the unsubscribe
message:
reader.unsubscribe(my_listener)
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
The available key events for character input are:
:keypress
:keyenter
:keyreturn
:keytab
:keybackspace
:keyspace
:keyescape
:keydelete
:keyalpha
:keynum
The navigation related key events are:
:keydown
:keyup
:keyleft
:keyright
:keyhome
:keyend
:keyclear
The specific ctrl
key events:
:keyctrl_a
:keyctrl_b
- ...
:keyctrl_z
The key events for functional keys f*
are:
:keyf1
:keyf2
- ...
:keyf24
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:
:signal
- sends interrupt signal:exit
- exists with status code:noop
- skips handler- custom proc
For example, to send interrupt signal do:
reader = TTY::Reader.new(interrupt: :signal)
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)
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)
This option controls whether duplicate lines are stored in history. By default set to true
. You can change this:
reader = TTY::Reader.new(history_duplicates: false)
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) { ... })
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.
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.
- Clone the project on GitHub
- Create a feature branch
- Submit a Pull Request
Important notes:
- All new features must include test coverage. At a bare minimum, unit tests are required. It is preferred if you include acceptance tests as well.
- The tests must be be idempotent. Any test run should produce the same result when run over and over.
- All new features must include source code & readme documentation Any new method you add should include yarddoc style documentation with clearly specified parameter and return types.
The gem is available as open source under the terms of the MIT License.
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 (c) 2017 Piotr Murach. See LICENSE for further details.