tty-reader 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/Rakefile +10 -0
- data/appveyor.yml +25 -0
- data/bin/console +6 -0
- data/bin/setup +8 -0
- data/lib/tty-reader.rb +3 -0
- data/lib/tty/reader.rb +348 -0
- data/lib/tty/reader/codes.rb +120 -0
- data/lib/tty/reader/console.rb +56 -0
- data/lib/tty/reader/history.rb +144 -0
- data/lib/tty/reader/key_event.rb +90 -0
- data/lib/tty/reader/line.rb +161 -0
- data/lib/tty/reader/mode.rb +43 -0
- data/lib/tty/reader/version.rb +7 -0
- data/lib/tty/reader/win_api.rb +54 -0
- data/lib/tty/reader/win_console.rb +90 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-reader.gemspec +28 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4450cd173fbc5dece09c32d0ad799bb5fcccff3a
|
4
|
+
data.tar.gz: e744eaefca8e5b03f79b41d502fe71a2ba8b3166
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b113279156eb75f4f2c7079abe5c225993c4f0b24bd33fb07dd45366e316bf9a9356ea23b0f96cf122e36502fa85161d8ccc33f7a4e8ef2dae57faf184f4f77a
|
7
|
+
data.tar.gz: 21fd68ce3809c21a88dd24d18ec25348e4111fc41a7828c6c2bb3ccb6279f7fed3477c1261d9d65e3911f8d6c5656f92f908fee5ac9d1ab44609100feb9c9a5c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
---
|
2
|
+
language: ruby
|
3
|
+
sudo: false
|
4
|
+
cache: bundler
|
5
|
+
bundler_args: --without tools
|
6
|
+
script: "bundle exec rake ci"
|
7
|
+
rvm:
|
8
|
+
- 1.9.3
|
9
|
+
- 2.0.0
|
10
|
+
- 2.1.10
|
11
|
+
- 2.2.6
|
12
|
+
- 2.3.3
|
13
|
+
- 2.4.1
|
14
|
+
- ruby-head
|
15
|
+
- jruby-9000
|
16
|
+
- jruby-head
|
17
|
+
matrix:
|
18
|
+
allow_failures:
|
19
|
+
- rvm: ruby-head
|
20
|
+
- rvm: jruby-head
|
21
|
+
fast_finish: true
|
22
|
+
branches:
|
23
|
+
only: master
|
24
|
+
notifications:
|
25
|
+
email: false
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at [email]. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
gemspec
|
6
|
+
|
7
|
+
group :test do
|
8
|
+
gem 'benchmark-ips', '~> 2.0.0'
|
9
|
+
gem 'simplecov', '~> 0.10.0'
|
10
|
+
gem 'coveralls', '~> 0.8.2'
|
11
|
+
gem 'term-ansicolor', '=1.3.2'
|
12
|
+
end
|
13
|
+
|
14
|
+
group :tools do
|
15
|
+
gem 'byebug', platform: :mri
|
16
|
+
end
|
17
|
+
|
18
|
+
group :metrics do
|
19
|
+
gem 'yard', '~> 0.8.7'
|
20
|
+
gem 'yardstick', '~> 0.9.9'
|
21
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Piotr Murach
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# TTY::Reader [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/tty-reader.svg)][gem]
|
4
|
+
[![Build Status](https://secure.travis-ci.org/piotrmurach/tty-reader.svg?branch=master)][travis]
|
5
|
+
[![Build status](https://ci.appveyor.com/api/projects/status/cj4owy2vlty2q1ko?svg=true)][appveyor]
|
6
|
+
[![Code Climate](https://codeclimate.com/github/piotrmurach/tty-reader/badges/gpa.svg)][codeclimate]
|
7
|
+
[![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-reader/badge.svg)][coverage]
|
8
|
+
[![Inline docs](http://inch-ci.org/github/piotrmurach/tty-reader.svg?branch=master)][inchpages]
|
9
|
+
|
10
|
+
[gitter]: https://gitter.im/piotrmurach/tty
|
11
|
+
[gem]: http://badge.fury.io/rb/tty-reader
|
12
|
+
[travis]: http://travis-ci.org/piotrmurach/tty-reader
|
13
|
+
[appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-reader
|
14
|
+
[codeclimate]: https://codeclimate.com/github/piotrmurach/tty-reader
|
15
|
+
[coverage]: https://coveralls.io/github/piotrmurach/tty-reader
|
16
|
+
[inchpages]: http://inch-ci.org/github/piotrmurach/tty-reader
|
17
|
+
|
18
|
+
> A pure Ruby library that provides a set of methods for processing keyboard input in character, line and multiline modes. In addition it maintains history of entered input with an ability to recall and re-edit those inputs and register to listen for keystroke events.
|
19
|
+
|
20
|
+
**TTY::Reader** provides independent reader component for [TTY](https://github.com/piotrmurach/tty) toolkit.
|
21
|
+
|
22
|
+
## Features
|
23
|
+
|
24
|
+
* Reading single keypress
|
25
|
+
* Line editing
|
26
|
+
* Multiline input
|
27
|
+
* History management
|
28
|
+
* Ability to register for key events
|
29
|
+
|
30
|
+
## Installation
|
31
|
+
|
32
|
+
Add this line to your application's Gemfile:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
gem 'tty-reader'
|
36
|
+
```
|
37
|
+
|
38
|
+
And then execute:
|
39
|
+
|
40
|
+
$ bundle
|
41
|
+
|
42
|
+
Or install it yourself as:
|
43
|
+
|
44
|
+
$ gem install tty-reader
|
45
|
+
|
46
|
+
* [1. Usage](#1-usage)
|
47
|
+
* [2. API](#2-api)
|
48
|
+
* [2.1 read_keypress](#21-read_keypress)
|
49
|
+
* [2.2 read_line](#22-read_line)
|
50
|
+
* [2.3 read_multiline](#23-read_multiline)
|
51
|
+
* [2.4 events](#24-events)
|
52
|
+
* [3. Configuration](#3-configuration)
|
53
|
+
* [3.1 :interrupt](#31-interrupt)
|
54
|
+
* [3.2 :track_history](#31-track_history)
|
55
|
+
|
56
|
+
## Usage
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
reader = TTY::Reader.new
|
60
|
+
```
|
61
|
+
|
62
|
+
## API
|
63
|
+
|
64
|
+
### 2.1 read_keypress
|
65
|
+
|
66
|
+
To read a single key stroke from the user use `read_char` or `read_keypress`:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
reader.read_char
|
70
|
+
reader.read_keypress
|
71
|
+
```
|
72
|
+
|
73
|
+
## 2.2 read_line
|
74
|
+
|
75
|
+
To read a single line terminated by new line character use `read_line` like so:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
reader.read_line
|
79
|
+
```
|
80
|
+
|
81
|
+
## 2.3 read_multiline
|
82
|
+
|
83
|
+
To read more than one line terminated by `Ctrl+d` or `Ctrl+z` use `read_multiline`:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
reader.read_multiline
|
87
|
+
# => [ "line1", "line2", ... ]
|
88
|
+
```
|
89
|
+
|
90
|
+
## 2.4 events
|
91
|
+
|
92
|
+
You can register to listen on a key pressed events. This can be done by calling `on` with a event name:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
reader.on(:keypress) { |event| .... }
|
96
|
+
```
|
97
|
+
|
98
|
+
The event object is yielded to a block whenever particular key event fires. The event has `key` and `value` methods. Further, the `key` responds to following messages:
|
99
|
+
|
100
|
+
* `name` - the name of the event such as :up, :down, letter or digit
|
101
|
+
* `meta` - true if event is non-standard key associated
|
102
|
+
* `shift` - true if shift has been pressed with the key
|
103
|
+
* `ctrl` - true if ctrl has been pressed with the key
|
104
|
+
|
105
|
+
For example, to add listen to vim like navigation keys, one would do the following:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
reader.on(:keypress) do |event|
|
109
|
+
if event.value == 'j'
|
110
|
+
...
|
111
|
+
end
|
112
|
+
|
113
|
+
if event.value == 'k'
|
114
|
+
...
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
You can subscribe to more than one event:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
prompt.on(:keypress) { |key| ... }
|
123
|
+
.on(:keydown) { |key| ... }
|
124
|
+
```
|
125
|
+
|
126
|
+
The available events are:
|
127
|
+
|
128
|
+
* `:keypress`
|
129
|
+
* `:keydown`
|
130
|
+
* `:keyup`
|
131
|
+
* `:keyleft`
|
132
|
+
* `:keyright`
|
133
|
+
* `:keynum`
|
134
|
+
* `:keytab`
|
135
|
+
* `:keyenter`
|
136
|
+
* `:keyreturn`
|
137
|
+
* `:keyspace`
|
138
|
+
* `:keyescape`
|
139
|
+
* `:keydelete`
|
140
|
+
* `:keybackspace`
|
141
|
+
|
142
|
+
## 3. Configuration
|
143
|
+
|
144
|
+
### 3.1. :interrupt
|
145
|
+
|
146
|
+
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:
|
147
|
+
|
148
|
+
* `:signal` - sends interrupt signal
|
149
|
+
* `:exit` - exists with status code
|
150
|
+
* `:noop` - skips handler
|
151
|
+
* custom proc
|
152
|
+
|
153
|
+
For example, to send interrupt signal do:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
reader = TTY::Reader.new(interrupt: :signal)
|
157
|
+
```
|
158
|
+
|
159
|
+
### 3.2. :track_history
|
160
|
+
|
161
|
+
The `read_line` and `read_multiline` provide history buffer that tracks all the lines entered during `TTY::Reader.new` interactions. The history buffer provides previoius or next lines when user presses up/down arrows respectively. However, if you wish to disable this behaviour use `:track_history` option like so:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
reader = TTY::Reader.new(track_history: false)
|
165
|
+
```
|
166
|
+
|
167
|
+
## Development
|
168
|
+
|
169
|
+
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.
|
170
|
+
|
171
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
172
|
+
|
173
|
+
## Contributing
|
174
|
+
|
175
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tty-reader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
180
|
+
|
181
|
+
## Code of Conduct
|
182
|
+
|
183
|
+
Everyone interacting in the Tty::Reader project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/tty-reader/blob/master/CODE_OF_CONDUCT.md).
|
184
|
+
|
185
|
+
## Copyright
|
186
|
+
|
187
|
+
Copyright (c) 2017 Piotr Murach. See LICENSE for further details.
|
data/Rakefile
ADDED
data/appveyor.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
---
|
2
|
+
install:
|
3
|
+
- SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
|
4
|
+
- ruby --version
|
5
|
+
- gem --version
|
6
|
+
- bundle install
|
7
|
+
build: off
|
8
|
+
test_script:
|
9
|
+
- bundle exec rake ci
|
10
|
+
environment:
|
11
|
+
matrix:
|
12
|
+
- ruby_version: "193"
|
13
|
+
- ruby_version: "200"
|
14
|
+
- ruby_version: "200-x64"
|
15
|
+
- ruby_version: "21"
|
16
|
+
- ruby_version: "21-x64"
|
17
|
+
- ruby_version: "22"
|
18
|
+
- ruby_version: "22-x64"
|
19
|
+
- ruby_version: "23"
|
20
|
+
- ruby_version: "23-x64"
|
21
|
+
- ruby_version: "24"
|
22
|
+
- ruby_version: "24-x64"
|
23
|
+
matrix:
|
24
|
+
allow_failures:
|
25
|
+
- ruby_version: "193"
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/tty-reader.rb
ADDED
data/lib/tty/reader.rb
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'wisper'
|
5
|
+
|
6
|
+
require_relative 'reader/history'
|
7
|
+
require_relative 'reader/line'
|
8
|
+
require_relative 'reader/key_event'
|
9
|
+
require_relative 'reader/console'
|
10
|
+
require_relative 'reader/win_console'
|
11
|
+
require_relative 'reader/version'
|
12
|
+
|
13
|
+
module TTY
|
14
|
+
# A class responsible for reading character input from STDIN
|
15
|
+
#
|
16
|
+
# Used internally to provide key and line reading functionality
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
class Reader
|
20
|
+
include Wisper::Publisher
|
21
|
+
|
22
|
+
# Raised when the user hits the interrupt key(Control-C)
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
InputInterrupt = Class.new(StandardError)
|
26
|
+
|
27
|
+
attr_reader :input
|
28
|
+
|
29
|
+
attr_reader :output
|
30
|
+
|
31
|
+
attr_reader :env
|
32
|
+
|
33
|
+
attr_reader :track_history
|
34
|
+
alias track_history? track_history
|
35
|
+
|
36
|
+
attr_reader :console
|
37
|
+
|
38
|
+
# Key codes
|
39
|
+
CARRIAGE_RETURN = 13
|
40
|
+
NEWLINE = 10
|
41
|
+
BACKSPACE = 127
|
42
|
+
DELETE = 8
|
43
|
+
|
44
|
+
# Initialize a Reader
|
45
|
+
#
|
46
|
+
# @param [IO] input
|
47
|
+
# the input stream
|
48
|
+
# @param [IO] output
|
49
|
+
# the output stream
|
50
|
+
# @param [Hash] options
|
51
|
+
# @option options [Symbol] :interrupt
|
52
|
+
# handling of Ctrl+C key out of :signal, :exit, :noop
|
53
|
+
# @option options [Boolean] :track_history
|
54
|
+
# disable line history tracking, true by default
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def initialize(input = $stdin, output = $stdout, options = {})
|
58
|
+
@input = input
|
59
|
+
@output = output
|
60
|
+
@interrupt = options.fetch(:interrupt) { :error }
|
61
|
+
@env = options.fetch(:env) { ENV }
|
62
|
+
@track_history = options.fetch(:track_history) { true }
|
63
|
+
@console = select_console(input)
|
64
|
+
@history = History.new do |h|
|
65
|
+
h.duplicates = false
|
66
|
+
h.exclude = proc { |line| line.strip == '' }
|
67
|
+
end
|
68
|
+
@stop = false # gathering input
|
69
|
+
|
70
|
+
subscribe(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Select appropriate console
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
def select_console(input)
|
77
|
+
if windows? && !env['TTY_TEST']
|
78
|
+
WinConsole.new(input)
|
79
|
+
else
|
80
|
+
Console.new(input)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get input in unbuffered mode.
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# unbufferred do
|
88
|
+
# ...
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
def unbufferred(&block)
|
93
|
+
bufferring = output.sync
|
94
|
+
# Immediately flush output
|
95
|
+
output.sync = true
|
96
|
+
block[] if block_given?
|
97
|
+
ensure
|
98
|
+
output.sync = bufferring
|
99
|
+
end
|
100
|
+
|
101
|
+
# Read a keypress including invisible multibyte codes
|
102
|
+
# and return a character as a string.
|
103
|
+
# Nothing is echoed to the console. This call will block for a
|
104
|
+
# single keypress, but will not wait for Enter to be pressed.
|
105
|
+
#
|
106
|
+
# @param [Hash[Symbol]] options
|
107
|
+
# @option options [Boolean] echo
|
108
|
+
# whether to echo chars back or not, defaults to false
|
109
|
+
# @option options [Boolean] raw
|
110
|
+
# whenther raw mode enabled, defaults to true
|
111
|
+
#
|
112
|
+
# @return [String]
|
113
|
+
#
|
114
|
+
# @api public
|
115
|
+
def read_keypress(options = {})
|
116
|
+
opts = { echo: false, raw: true }.merge(options)
|
117
|
+
codes = unbufferred { get_codes(opts) }
|
118
|
+
char = codes ? codes.pack('U*') : nil
|
119
|
+
|
120
|
+
trigger_key_event(char) if char
|
121
|
+
char
|
122
|
+
end
|
123
|
+
alias read_char read_keypress
|
124
|
+
|
125
|
+
# Get input code points
|
126
|
+
#
|
127
|
+
# @param [Hash[Symbol]] options
|
128
|
+
# @param [Array[Integer]] codes
|
129
|
+
#
|
130
|
+
# @return [Array[Integer]]
|
131
|
+
#
|
132
|
+
# @api private
|
133
|
+
def get_codes(options = {}, codes = [])
|
134
|
+
opts = { echo: true, raw: false }.merge(options)
|
135
|
+
char = console.get_char(opts)
|
136
|
+
handle_interrupt if char == console.keys[:ctrl_c]
|
137
|
+
return if char.nil?
|
138
|
+
codes << char.ord
|
139
|
+
|
140
|
+
condition = proc { |escape|
|
141
|
+
(codes - escape).empty? ||
|
142
|
+
(escape - codes).empty? &&
|
143
|
+
!(64..126).include?(codes.last)
|
144
|
+
}
|
145
|
+
|
146
|
+
while console.escape_codes.any?(&condition)
|
147
|
+
get_codes(options, codes)
|
148
|
+
end
|
149
|
+
codes
|
150
|
+
end
|
151
|
+
|
152
|
+
# Get a single line from STDIN. Each key pressed is echoed
|
153
|
+
# back to the shell. The input terminates when enter or
|
154
|
+
# return key is pressed.
|
155
|
+
#
|
156
|
+
# @param [String] prompt
|
157
|
+
# the prompt to display before input
|
158
|
+
#
|
159
|
+
# @param [Boolean] echo
|
160
|
+
# if true echo back characters, output nothing otherwise
|
161
|
+
#
|
162
|
+
# @return [String]
|
163
|
+
#
|
164
|
+
# @api public
|
165
|
+
def read_line(*args)
|
166
|
+
options = args.last.respond_to?(:to_hash) ? args.pop : {}
|
167
|
+
prompt = args.empty? ? '' : args.pop
|
168
|
+
opts = { echo: true, raw: true }.merge(options)
|
169
|
+
line = Line.new('')
|
170
|
+
ctrls = console.keys.keys.grep(/ctrl/)
|
171
|
+
clear_line = "\e[2K\e[1G"
|
172
|
+
|
173
|
+
while (codes = unbufferred { get_codes(opts) }) && (code = codes[0])
|
174
|
+
char = codes.pack('U*')
|
175
|
+
trigger_key_event(char)
|
176
|
+
|
177
|
+
if console.keys[:backspace] == char || BACKSPACE == code
|
178
|
+
next if line.start?
|
179
|
+
line.left
|
180
|
+
line.delete
|
181
|
+
elsif console.keys[:delete] == char || DELETE == code
|
182
|
+
line.delete
|
183
|
+
elsif [console.keys[:ctrl_d],
|
184
|
+
console.keys[:ctrl_z]].include?(char)
|
185
|
+
break
|
186
|
+
elsif ctrls.include?(console.keys.key(char))
|
187
|
+
# skip
|
188
|
+
elsif console.keys[:up] == char
|
189
|
+
next unless history_previous?
|
190
|
+
line.replace(history_previous)
|
191
|
+
elsif console.keys[:down] == char
|
192
|
+
line.replace(history_next? ? history_next : '')
|
193
|
+
elsif console.keys[:left] == char
|
194
|
+
line.left
|
195
|
+
elsif console.keys[:right] == char
|
196
|
+
line.right
|
197
|
+
else
|
198
|
+
if opts[:raw] && code == CARRIAGE_RETURN
|
199
|
+
char = "\n"
|
200
|
+
line.move_to_end
|
201
|
+
end
|
202
|
+
line.insert(char)
|
203
|
+
end
|
204
|
+
|
205
|
+
if opts[:raw] && opts[:echo]
|
206
|
+
output.print(clear_line)
|
207
|
+
output.print(prompt + line.to_s)
|
208
|
+
if char == "\n"
|
209
|
+
line.move_to_start
|
210
|
+
elsif !line.end?
|
211
|
+
output.print("\e[#{line.size - line.cursor}D")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
break if (code == CARRIAGE_RETURN || code == NEWLINE)
|
216
|
+
|
217
|
+
if (console.keys[:backspace] == char || BACKSPACE == code) && opts[:echo]
|
218
|
+
if opts[:raw]
|
219
|
+
output.print("\e[1X") unless line.start?
|
220
|
+
else
|
221
|
+
output.print(?\s + (line.start? ? '' : ?\b))
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
add_to_history(line.to_s.rstrip) if track_history?
|
226
|
+
line.to_s
|
227
|
+
end
|
228
|
+
|
229
|
+
# Read multiple lines and return them in an array.
|
230
|
+
# Skip empty lines in the returned lines array.
|
231
|
+
# The input gathering is terminated by Ctrl+d or Ctrl+z.
|
232
|
+
#
|
233
|
+
# @param [String] prompt
|
234
|
+
# the prompt displayed before the input
|
235
|
+
#
|
236
|
+
# @yield [String] line
|
237
|
+
#
|
238
|
+
# @return [Array[String]]
|
239
|
+
#
|
240
|
+
# @api public
|
241
|
+
def read_multiline(prompt = '')
|
242
|
+
@stop = false
|
243
|
+
lines = []
|
244
|
+
loop do
|
245
|
+
line = read_line(prompt)
|
246
|
+
break if !line || line == ''
|
247
|
+
next if line !~ /\S/ && !@stop
|
248
|
+
if block_given?
|
249
|
+
yield(line) unless line.to_s.empty?
|
250
|
+
else
|
251
|
+
lines << line unless line.to_s.empty?
|
252
|
+
end
|
253
|
+
break if @stop
|
254
|
+
end
|
255
|
+
lines
|
256
|
+
end
|
257
|
+
alias read_lines read_multiline
|
258
|
+
|
259
|
+
# Expose event broadcasting
|
260
|
+
#
|
261
|
+
# @api public
|
262
|
+
def trigger(event, *args)
|
263
|
+
publish(event, *args)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Capture Ctrl+d and Ctrl+z key events
|
267
|
+
#
|
268
|
+
# @api private
|
269
|
+
def keyctrl_d(*)
|
270
|
+
@stop = true
|
271
|
+
end
|
272
|
+
alias keyctrl_z keyctrl_d
|
273
|
+
|
274
|
+
def add_to_history(line)
|
275
|
+
@history.push(line)
|
276
|
+
end
|
277
|
+
|
278
|
+
def history_next?
|
279
|
+
@history.next?
|
280
|
+
end
|
281
|
+
|
282
|
+
def history_next
|
283
|
+
@history.next
|
284
|
+
@history.get
|
285
|
+
end
|
286
|
+
|
287
|
+
def history_previous?
|
288
|
+
@history.previous?
|
289
|
+
end
|
290
|
+
|
291
|
+
def history_previous
|
292
|
+
line = @history.get
|
293
|
+
@history.previous
|
294
|
+
line
|
295
|
+
end
|
296
|
+
|
297
|
+
# Inspect class name and public attributes
|
298
|
+
# @return [String]
|
299
|
+
#
|
300
|
+
# @api public
|
301
|
+
def inspect
|
302
|
+
"#<#{self.class}: @input=#{input}, @output=#{output}>"
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
# Publish event
|
308
|
+
#
|
309
|
+
# @param [String] char
|
310
|
+
# the key pressed
|
311
|
+
#
|
312
|
+
# @return [nil]
|
313
|
+
#
|
314
|
+
# @api private
|
315
|
+
def trigger_key_event(char)
|
316
|
+
event = KeyEvent.from(console.keys, char)
|
317
|
+
trigger(:"key#{event.key.name}", event) if event.trigger?
|
318
|
+
trigger(:keypress, event)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Handle input interrupt based on provided value
|
322
|
+
#
|
323
|
+
# @api private
|
324
|
+
def handle_interrupt
|
325
|
+
case @interrupt
|
326
|
+
when :signal
|
327
|
+
Process.kill('SIGINT', Process.pid)
|
328
|
+
when :exit
|
329
|
+
exit(130)
|
330
|
+
when Proc
|
331
|
+
@interrupt.call
|
332
|
+
when :noop
|
333
|
+
return
|
334
|
+
else
|
335
|
+
raise InputInterrupt
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Check if Windowz mode
|
340
|
+
#
|
341
|
+
# @return [Boolean]
|
342
|
+
#
|
343
|
+
# @api public
|
344
|
+
def windows?
|
345
|
+
::File::ALT_SEPARATOR == '\\'
|
346
|
+
end
|
347
|
+
end # Reader
|
348
|
+
end # TTY
|