tty-reader 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -5
- data/CHANGELOG.md +22 -1
- data/Gemfile +4 -5
- data/README.md +176 -27
- data/appveyor.yml +0 -4
- data/benchmarks/speed_read_char.rb +34 -0
- data/benchmarks/speed_read_line.rb +34 -0
- data/examples/keypress.rb +16 -0
- data/examples/line.rb +7 -0
- data/examples/multiline.rb +7 -0
- data/examples/noecho.rb +6 -0
- data/examples/shell.rb +12 -0
- data/lib/tty/reader.rb +121 -58
- data/lib/tty/reader/console.rb +2 -2
- data/lib/tty/reader/history.rb +6 -2
- data/lib/tty/reader/key_event.rb +4 -35
- data/lib/tty/reader/keys.rb +165 -0
- data/lib/tty/reader/line.rb +96 -9
- data/lib/tty/reader/version.rb +1 -1
- data/lib/tty/reader/win_console.rb +2 -2
- data/tty-reader.gemspec +7 -3
- metadata +46 -12
- data/lib/tty/reader/codes.rb +0 -120
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1b83301002055869c17bb4ecb0571be89b2d8e0
|
4
|
+
data.tar.gz: 39bd25b1b47c2542c000a407a2839bd4cba12c31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 08c4198829dcc5a4f95c865c4dfcb2c4be9711885a3d071c6f1d9f917e8dc20083d558dde98ffc38af0d7e6ce6de9f4fbfeecb1158109f2905adb047aa6970f1
|
7
|
+
data.tar.gz: 3899ba4f5461099024379df0058deb177dbf1a45d6fb250970f3a639adf792f9563773adeae1b240f2feb41241d6ec45b4a012c15f4b1c234ad6bf582f7c0ac4
|
data/.travis.yml
CHANGED
@@ -2,17 +2,17 @@
|
|
2
2
|
language: ruby
|
3
3
|
sudo: false
|
4
4
|
cache: bundler
|
5
|
+
before_install: "gem update bundler"
|
5
6
|
bundler_args: --without tools
|
6
7
|
script: "bundle exec rake ci"
|
7
8
|
rvm:
|
8
|
-
- 1.9.3
|
9
9
|
- 2.0.0
|
10
10
|
- 2.1.10
|
11
|
-
- 2.2.
|
12
|
-
- 2.3.
|
13
|
-
- 2.4.
|
11
|
+
- 2.2.9
|
12
|
+
- 2.3.6
|
13
|
+
- 2.4.3
|
14
14
|
- ruby-head
|
15
|
-
- jruby-
|
15
|
+
- jruby-9.1.1.0
|
16
16
|
- jruby-head
|
17
17
|
matrix:
|
18
18
|
allow_failures:
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,28 @@
|
|
1
1
|
# Change log
|
2
2
|
|
3
|
+
## [v0.2.0] - 2018-01-01
|
4
|
+
|
5
|
+
### Added
|
6
|
+
* Add home & end keys support in #read_line
|
7
|
+
* Add tty-screen & tty-cursor dependencies
|
8
|
+
|
9
|
+
### Changed
|
10
|
+
* Change Codes to Keys and inverse keys lookup to allow for different system keys matching same name.
|
11
|
+
* Change Reader#initialize to only accept options and make input and output options as well.
|
12
|
+
* Change #read_line to print newline character in noecho mode
|
13
|
+
* Change Reader::Line to include prompt prefix
|
14
|
+
* Change Reader#initialize to only accept options in place of positional arguments
|
15
|
+
* Change Reader to expose history options
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
* Fix issues with recognising :home & :end keys on different terminals
|
19
|
+
* Fix #read_line to work with strings spanning multiple screen widths and allow copy-pasting a long string without repeating prompt
|
20
|
+
* Fix backspace keystroke in cooked mode
|
21
|
+
* Fix history to only save lines in echo mode
|
22
|
+
|
3
23
|
## [v0.1.0] - 2017-08-30
|
4
24
|
|
5
25
|
* Initial implementation and release
|
6
26
|
|
7
|
-
[v0.
|
27
|
+
[v0.2.0]: https://github.com/piotrmurach/tty-reader/compare/v0.1.0...v0.2.0
|
28
|
+
[v0.1.0]: https://github.com/piotrmurach/tty-reader/compare/v0.1.0
|
data/Gemfile
CHANGED
@@ -5,10 +5,9 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
|
5
5
|
gemspec
|
6
6
|
|
7
7
|
group :test do
|
8
|
-
gem 'benchmark-ips', '~> 2.
|
9
|
-
gem 'simplecov', '~> 0.
|
10
|
-
gem 'coveralls', '~> 0.8.
|
11
|
-
gem 'term-ansicolor', '=1.3.2'
|
8
|
+
gem 'benchmark-ips', '~> 2.7.2'
|
9
|
+
gem 'simplecov', '~> 0.14.1'
|
10
|
+
gem 'coveralls', '~> 0.8.21'
|
12
11
|
end
|
13
12
|
|
14
13
|
group :tools do
|
@@ -16,6 +15,6 @@ group :tools do
|
|
16
15
|
end
|
17
16
|
|
18
17
|
group :metrics do
|
19
|
-
gem 'yard', '~> 0.
|
18
|
+
gem 'yard', '~> 0.9.12'
|
20
19
|
gem 'yardstick', '~> 0.9.9'
|
21
20
|
end
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/tty-reader.svg)][gem]
|
4
4
|
[![Build Status](https://secure.travis-ci.org/piotrmurach/tty-reader.svg?branch=master)][travis]
|
5
5
|
[![Build status](https://ci.appveyor.com/api/projects/status/cj4owy2vlty2q1ko?svg=true)][appveyor]
|
6
|
-
[![
|
6
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/2f68d5e8ecc271bda820/maintainability)][codeclimate]
|
7
7
|
[![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-reader/badge.svg)][coverage]
|
8
8
|
[![Inline docs](http://inch-ci.org/github/piotrmurach/tty-reader.svg?branch=master)][inchpages]
|
9
9
|
|
@@ -11,7 +11,7 @@
|
|
11
11
|
[gem]: http://badge.fury.io/rb/tty-reader
|
12
12
|
[travis]: http://travis-ci.org/piotrmurach/tty-reader
|
13
13
|
[appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-reader
|
14
|
-
[codeclimate]: https://codeclimate.com/github/piotrmurach/tty-reader
|
14
|
+
[codeclimate]: https://codeclimate.com/github/piotrmurach/tty-reader/maintainability
|
15
15
|
[coverage]: https://coveralls.io/github/piotrmurach/tty-reader
|
16
16
|
[inchpages]: http://inch-ci.org/github/piotrmurach/tty-reader
|
17
17
|
|
@@ -19,13 +19,23 @@
|
|
19
19
|
|
20
20
|
**TTY::Reader** provides independent reader component for [TTY](https://github.com/piotrmurach/tty) toolkit.
|
21
21
|
|
22
|
+
## Compatibility
|
23
|
+
|
24
|
+
The `tty-reader` is not compatible with the GNU Readline and doesn't aim to be. It originated from [tty-prompt](https://github.com/piotrmurach/tty-prompt) project to provide flexibility, independence from underlying operating system and Ruby like API interface for creating different prompts.
|
25
|
+
|
26
|
+
`TTY::Reader` forges its own path to provide features necessary for building line editing in terminal applications!
|
27
|
+
|
22
28
|
## Features
|
23
29
|
|
24
|
-
*
|
30
|
+
* Pure Ruby
|
25
31
|
* Line editing
|
26
|
-
*
|
32
|
+
* Reading single keypress
|
33
|
+
* Reading multiline input
|
27
34
|
* History management
|
28
|
-
* Ability to register for
|
35
|
+
* Ability to register for keystroke events
|
36
|
+
* No global state
|
37
|
+
* Works on Linux, OS X, FreeBSD and Windows
|
38
|
+
* Supports Ruby versions `>= 2.0.0` & JRuby
|
29
39
|
|
30
40
|
## Installation
|
31
41
|
|
@@ -48,10 +58,16 @@ Or install it yourself as:
|
|
48
58
|
* [2.1 read_keypress](#21-read_keypress)
|
49
59
|
* [2.2 read_line](#22-read_line)
|
50
60
|
* [2.3 read_multiline](#23-read_multiline)
|
51
|
-
* [2.4
|
61
|
+
* [2.4 on](#24-on)
|
62
|
+
* [2.5 subscribe](#25-subscribe)
|
63
|
+
* [2.6 trigger](#26-trigger)
|
64
|
+
* [2.7 supported events](#27-supported-events)
|
52
65
|
* [3. Configuration](#3-configuration)
|
53
66
|
* [3.1 :interrupt](#31-interrupt)
|
54
|
-
* [3.2 :track_history](#
|
67
|
+
* [3.2 :track_history](#32-track_history)
|
68
|
+
* [3.3 :history_cycle](#33-history_cycle)
|
69
|
+
* [3.4 :history_duplicates](#34-history_duplicates)
|
70
|
+
* [3.5 :history_exclude](#35-history_exclude)
|
55
71
|
|
56
72
|
## Usage
|
57
73
|
|
@@ -70,24 +86,59 @@ reader.read_char
|
|
70
86
|
reader.read_keypress
|
71
87
|
```
|
72
88
|
|
73
|
-
|
89
|
+
### 2.2 read_line
|
90
|
+
|
91
|
+
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.
|
74
92
|
|
75
|
-
|
93
|
+
For example, to read a single line terminated by a new line character use `read_line` like so:
|
76
94
|
|
77
95
|
```ruby
|
78
96
|
reader.read_line
|
79
97
|
```
|
80
98
|
|
81
|
-
|
99
|
+
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`:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
reader.read_line(raw: false)
|
103
|
+
```
|
104
|
+
|
105
|
+
Any non-interpreted characters received are written back to terminal, however you can stop this by using `:echo` option set to `false`:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
reader.read_line(echo: false)
|
109
|
+
```
|
110
|
+
|
111
|
+
You can also provide a line prefix displayed before input by passing it as a first aargument:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
reader.read_line(">> ")
|
115
|
+
# >> input goes here ...
|
116
|
+
```
|
117
|
+
|
118
|
+
### 2.3 read_multiline
|
82
119
|
|
83
|
-
|
120
|
+
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.
|
121
|
+
|
122
|
+
For example, to read more than one line terminated by `Ctrl+d` or `Ctrl+z` use `read_multiline`:
|
84
123
|
|
85
124
|
```ruby
|
86
125
|
reader.read_multiline
|
87
126
|
# => [ "line1", "line2", ... ]
|
88
127
|
```
|
89
128
|
|
90
|
-
|
129
|
+
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`:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
reader.read_line(raw: false)
|
133
|
+
```
|
134
|
+
|
135
|
+
You can also provide a linke prefix displayed before input by passing a string as a first argument:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
reader.read_multiline(">> ")
|
139
|
+
```
|
140
|
+
|
141
|
+
### 2.4 on
|
91
142
|
|
92
143
|
You can register to listen on a key pressed events. This can be done by calling `on` with a event name:
|
93
144
|
|
@@ -123,25 +174,92 @@ prompt.on(:keypress) { |key| ... }
|
|
123
174
|
.on(:keydown) { |key| ... }
|
124
175
|
```
|
125
176
|
|
126
|
-
|
177
|
+
### 2.5 subscribe
|
178
|
+
|
179
|
+
You can subscribe any object to listen for the emitted [key events](#27-supported-events) using the `subscribe` message. The listener would need to implement a method for every event it wishes to receive.
|
180
|
+
|
181
|
+
For example, if a `Context` class wishes to only listen for `keypress` event:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
class Context
|
185
|
+
def keypress(event)
|
186
|
+
...
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
Then subcribing is done:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
context = Context.new
|
195
|
+
reader.subscribe(context)
|
196
|
+
```
|
197
|
+
|
198
|
+
### 2.6 trigger
|
199
|
+
|
200
|
+
The signature for triggering key events is `trigger(event, args...)`. The first argument is a [key event name](#27-supported-events) followed by any number of actual values related to the event being triggered.
|
201
|
+
|
202
|
+
For example, to trigger `:keydown` event do:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
reader.trigger(:keydown)
|
206
|
+
```
|
207
|
+
|
208
|
+
To add vim bindings for line editing you could discern between alphanumeric inputs like so:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
reader.on(:keypress) do |event|
|
212
|
+
if event.value == 'j'
|
213
|
+
reader.trigger(:keydown)
|
214
|
+
end
|
215
|
+
if evevnt.value == 'k'
|
216
|
+
reader.trigger(:keup)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
### 2.7 supported events
|
222
|
+
|
223
|
+
The available key events for character input are:
|
127
224
|
|
128
225
|
* `:keypress`
|
129
|
-
* `:keydown`
|
130
|
-
* `:keyup`
|
131
|
-
* `:keyleft`
|
132
|
-
* `:keyright`
|
133
|
-
* `:keynum`
|
134
|
-
* `:keytab`
|
135
226
|
* `:keyenter`
|
136
227
|
* `:keyreturn`
|
228
|
+
* `:keytab`
|
229
|
+
* `:keybackspace`
|
137
230
|
* `:keyspace`
|
138
231
|
* `:keyescape`
|
139
232
|
* `:keydelete`
|
140
|
-
* `:
|
233
|
+
* `:keyalpha`
|
234
|
+
* `:keynum`
|
235
|
+
|
236
|
+
The navigation relted key events are:
|
237
|
+
|
238
|
+
* `:keydown`
|
239
|
+
* `:keyup`
|
240
|
+
* `:keyleft`
|
241
|
+
* `:keyright`
|
242
|
+
* `:keyhome`
|
243
|
+
* `:keyend`
|
244
|
+
* `:keyclear`
|
245
|
+
|
246
|
+
The specific `ctrl` key events:
|
247
|
+
|
248
|
+
* `:keyctrl_a`
|
249
|
+
* `:keyctrl_b`
|
250
|
+
* ...
|
251
|
+
* `:keyctrl_z`
|
252
|
+
|
253
|
+
The key events for functional keys `f*` are:
|
254
|
+
|
255
|
+
* `:keyf1`
|
256
|
+
* `:keyf2`
|
257
|
+
* ...
|
258
|
+
* `:keyf24`
|
141
259
|
|
142
260
|
## 3. Configuration
|
143
261
|
|
144
|
-
### 3.1.
|
262
|
+
### 3.1. `:interrupt`
|
145
263
|
|
146
264
|
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
265
|
|
@@ -156,7 +274,7 @@ For example, to send interrupt signal do:
|
|
156
274
|
reader = TTY::Reader.new(interrupt: :signal)
|
157
275
|
```
|
158
276
|
|
159
|
-
### 3.2.
|
277
|
+
### 3.2. `:track_history`
|
160
278
|
|
161
279
|
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
280
|
|
@@ -164,15 +282,46 @@ The `read_line` and `read_multiline` provide history buffer that tracks all the
|
|
164
282
|
reader = TTY::Reader.new(track_history: false)
|
165
283
|
```
|
166
284
|
|
285
|
+
### 3.3. `:history_cycle`
|
286
|
+
|
287
|
+
This option determines whether the history buffer allows for infinite navigation. By default it is set to `false`. You can change this:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
reader = TTY::Reader.new(history_cycle: true)
|
291
|
+
```
|
292
|
+
|
293
|
+
### 3.4. `:history_duplicates`
|
294
|
+
|
295
|
+
This option controls whether duplicate lines are stored in history. By default set to `true`. You can change this:
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
reader = TTY::Reader.new(history_duplicates: false)
|
299
|
+
```
|
300
|
+
|
301
|
+
### 3.5. `:history_exclude`
|
302
|
+
|
303
|
+
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 exlude empty lines. To change this:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
reader = TTY::Reader.new(history_exclude: ->(line) { ... })
|
307
|
+
```
|
308
|
+
|
167
309
|
## Development
|
168
310
|
|
169
311
|
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
312
|
|
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
313
|
## Contributing
|
174
314
|
|
175
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
315
|
+
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](http://contributor-covenant.org) code of conduct.
|
316
|
+
|
317
|
+
1. Clone the project on GitHub
|
318
|
+
2. Create a feature branch
|
319
|
+
3. Submit a Pull Request
|
320
|
+
|
321
|
+
Important notes:
|
322
|
+
- **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.
|
323
|
+
- **The tests must be be idempotent.** Any test run should produce the same result when run over and over.
|
324
|
+
- **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.
|
176
325
|
|
177
326
|
## License
|
178
327
|
|
@@ -180,8 +329,8 @@ The gem is available as open source under the terms of the [MIT License](http://
|
|
180
329
|
|
181
330
|
## Code of Conduct
|
182
331
|
|
183
|
-
Everyone interacting in the
|
332
|
+
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
333
|
|
185
334
|
## Copyright
|
186
335
|
|
187
|
-
Copyright (c) 2017 Piotr Murach. See LICENSE for further details.
|
336
|
+
Copyright (c) 2017-2018 Piotr Murach. See LICENSE for further details.
|
data/appveyor.yml
CHANGED
@@ -9,7 +9,6 @@ test_script:
|
|
9
9
|
- bundle exec rake ci
|
10
10
|
environment:
|
11
11
|
matrix:
|
12
|
-
- ruby_version: "193"
|
13
12
|
- ruby_version: "200"
|
14
13
|
- ruby_version: "200-x64"
|
15
14
|
- ruby_version: "21"
|
@@ -20,6 +19,3 @@ environment:
|
|
20
19
|
- ruby_version: "23-x64"
|
21
20
|
- ruby_version: "24"
|
22
21
|
- ruby_version: "24-x64"
|
23
|
-
matrix:
|
24
|
-
allow_failures:
|
25
|
-
- ruby_version: "193"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require 'tty-reader'
|
3
|
+
|
4
|
+
input = StringIO.new("a")
|
5
|
+
output = StringIO.new
|
6
|
+
$stdin = input
|
7
|
+
reader = TTY::Reader.new(input, output)
|
8
|
+
|
9
|
+
Benchmark.ips do |x|
|
10
|
+
x.report('getc') do
|
11
|
+
input.rewind
|
12
|
+
$stdin.getc
|
13
|
+
end
|
14
|
+
|
15
|
+
x.report('read_char') do
|
16
|
+
input.rewind
|
17
|
+
reader.read_char
|
18
|
+
end
|
19
|
+
|
20
|
+
x.compare!
|
21
|
+
end
|
22
|
+
|
23
|
+
# v0.1.0
|
24
|
+
#
|
25
|
+
# Calculating -------------------------------------
|
26
|
+
# getc 52462 i/100ms
|
27
|
+
# read_char 751 i/100ms
|
28
|
+
# -------------------------------------------------
|
29
|
+
# getc 2484819.4 (±4.1%) i/s - 12433494 in 5.013438s
|
30
|
+
# read_char 7736.4 (±2.9%) i/s - 39052 in 5.052628s
|
31
|
+
#
|
32
|
+
# Comparison:
|
33
|
+
# getc: 2484819.4 i/s
|
34
|
+
# read_char: 7736.4 i/s - 321.19x slower
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require 'tty-reader'
|
3
|
+
|
4
|
+
input = StringIO.new("abc\n")
|
5
|
+
output = StringIO.new
|
6
|
+
$stdin = input
|
7
|
+
reader = TTY::Reader.new(input, output)
|
8
|
+
|
9
|
+
Benchmark.ips do |x|
|
10
|
+
x.report('gets') do
|
11
|
+
input.rewind
|
12
|
+
$stdin.gets
|
13
|
+
end
|
14
|
+
|
15
|
+
x.report('read_line') do
|
16
|
+
input.rewind
|
17
|
+
reader.read_line
|
18
|
+
end
|
19
|
+
|
20
|
+
x.compare!
|
21
|
+
end
|
22
|
+
|
23
|
+
# v0.1.0
|
24
|
+
#
|
25
|
+
# Calculating -------------------------------------
|
26
|
+
# gets 51729 i/100ms
|
27
|
+
# read_line 164 i/100ms
|
28
|
+
# -------------------------------------------------
|
29
|
+
# gets 1955255.2 (±3.7%) i/s - 9776781 in 5.008004s
|
30
|
+
# read_line 1215.1 (±33.1%) i/s - 5248 in 5.066569s
|
31
|
+
#
|
32
|
+
# Comparison:
|
33
|
+
# gets: 1955255.2 i/s
|
34
|
+
# read_line: 1215.1 i/s - 1609.19x slower
|