tty-reader 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +52 -11
- data/benchmarks/speed_read_char.rb +34 -0
- data/benchmarks/speed_read_line.rb +34 -0
- data/examples/shell.rb +2 -2
- data/lib/tty-reader.rb +0 -2
- data/lib/tty/reader.rb +12 -7
- data/lib/tty/reader/console.rb +9 -2
- data/lib/tty/reader/history.rb +0 -1
- data/lib/tty/reader/key_event.rb +3 -4
- data/lib/tty/reader/keys.rb +0 -1
- data/lib/tty/reader/line.rb +12 -5
- data/lib/tty/reader/mode.rb +0 -1
- data/lib/tty/reader/version.rb +2 -2
- data/lib/tty/reader/win_api.rb +1 -4
- data/lib/tty/reader/win_console.rb +0 -1
- data/spec/spec_helper.rb +43 -0
- data/spec/unit/history_spec.rb +177 -0
- data/spec/unit/key_event_spec.rb +102 -0
- data/spec/unit/line_spec.rb +159 -0
- data/spec/unit/publish_keypress_event_spec.rb +109 -0
- data/spec/unit/read_keypress_spec.rb +96 -0
- data/spec/unit/read_line_spec.rb +69 -0
- data/spec/unit/read_multiline_spec.rb +76 -0
- data/spec/unit/subscribe_spec.rb +74 -0
- data/tty-reader.gemspec +4 -3
- metadata +17 -4
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader, '#publish_keypress_event' do
|
4
|
+
let(:input) { StringIO.new }
|
5
|
+
let(:out) { StringIO.new }
|
6
|
+
let(:env) { { "TTY_TEST" => true } }
|
7
|
+
|
8
|
+
let(:reader) { described_class.new(input: input, output: out, env: env) }
|
9
|
+
|
10
|
+
it "publishes :keypress events" do
|
11
|
+
input << "abc\n"
|
12
|
+
input.rewind
|
13
|
+
chars = []
|
14
|
+
lines = []
|
15
|
+
reader.on(:keypress) { |event| chars << event.value; lines << event.line }
|
16
|
+
answer = reader.read_line
|
17
|
+
|
18
|
+
expect(chars).to eq(%W(a b c \n))
|
19
|
+
expect(lines).to eq(%W(a ab abc abc\n))
|
20
|
+
expect(answer).to eq("abc\n")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "publishes :keyescape events" do
|
24
|
+
input << "a\e"
|
25
|
+
input.rewind
|
26
|
+
keys = []
|
27
|
+
reader.on(:keypress) { |event| keys << "keypress_#{event.value}"}
|
28
|
+
reader.on(:keyescape) { |event| keys << "keyescape_#{event.value}" }
|
29
|
+
|
30
|
+
answer = reader.read_line
|
31
|
+
expect(keys).to eq(["keypress_a", "keyescape_\e", "keypress_\e"])
|
32
|
+
expect(answer).to eq("a\e")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "publishes :keyup for read_keypress" do
|
36
|
+
input << "\e[Aaa"
|
37
|
+
input.rewind
|
38
|
+
keys = []
|
39
|
+
reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
|
40
|
+
reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
|
41
|
+
reader.on(:keydown) { |event| keys << "keydown_#{event.value}" }
|
42
|
+
|
43
|
+
answer = reader.read_keypress
|
44
|
+
expect(keys).to eq(["keyup_\e[A", "keypress_\e[A"])
|
45
|
+
expect(answer).to eq("\e[A")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "publishes :keydown event for read_keypress" do
|
49
|
+
input << "\e[Baa"
|
50
|
+
input.rewind
|
51
|
+
keys = []
|
52
|
+
reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
|
53
|
+
reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
|
54
|
+
reader.on(:keydown) { |event| keys << "keydown_#{event.value}" }
|
55
|
+
|
56
|
+
answer = reader.read_keypress
|
57
|
+
expect(keys).to eq(["keydown_\e[B", "keypress_\e[B"])
|
58
|
+
expect(answer).to eq("\e[B")
|
59
|
+
end
|
60
|
+
|
61
|
+
it "publishes :keynum event" do
|
62
|
+
input << "5aa"
|
63
|
+
input.rewind
|
64
|
+
keys = []
|
65
|
+
reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
|
66
|
+
reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
|
67
|
+
reader.on(:keynum) { |event| keys << "keynum_#{event.value}" }
|
68
|
+
|
69
|
+
answer = reader.read_keypress
|
70
|
+
expect(keys).to eq(["keynum_5", "keypress_5"])
|
71
|
+
expect(answer).to eq("5")
|
72
|
+
end
|
73
|
+
|
74
|
+
it "publishes :keyreturn event" do
|
75
|
+
input << "\r"
|
76
|
+
input.rewind
|
77
|
+
keys = []
|
78
|
+
reader.on(:keypress) { |event| keys << "keypress" }
|
79
|
+
reader.on(:keyup) { |event| keys << "keyup" }
|
80
|
+
reader.on(:keyreturn) { |event| keys << "keyreturn" }
|
81
|
+
|
82
|
+
answer = reader.read_keypress
|
83
|
+
expect(keys).to eq(["keyreturn", "keypress"])
|
84
|
+
expect(answer).to eq("\r")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "subscribes to multiple events" do
|
88
|
+
input << "\n"
|
89
|
+
input.rewind
|
90
|
+
keys = []
|
91
|
+
reader.on(:keyenter) { |event| keys << "keyenter" }
|
92
|
+
.on(:keypress) { |event| keys << "keypress" }
|
93
|
+
|
94
|
+
answer = reader.read_keypress
|
95
|
+
expect(keys).to eq(["keyenter", "keypress"])
|
96
|
+
expect(answer).to eq("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
it "subscribes to ctrl+X type of event event" do
|
100
|
+
input << ?\C-z
|
101
|
+
input.rewind
|
102
|
+
keys = []
|
103
|
+
reader.on(:keyctrl_z) { |event| keys << "ctrl_z" }
|
104
|
+
|
105
|
+
answer = reader.read_keypress
|
106
|
+
expect(keys).to eq(['ctrl_z'])
|
107
|
+
expect(answer).to eq(?\C-z)
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader, '#read_keypress' do
|
4
|
+
let(:input) { StringIO.new }
|
5
|
+
let(:out) { StringIO.new }
|
6
|
+
let(:env) { { "TTY_TEST" => true } }
|
7
|
+
|
8
|
+
it "reads single key press" do
|
9
|
+
reader = described_class.new(input: input, output: out, env: env)
|
10
|
+
input << "\e[Aaaaaaa\n"
|
11
|
+
input.rewind
|
12
|
+
|
13
|
+
answer = reader.read_keypress
|
14
|
+
|
15
|
+
expect(answer).to eq("\e[A")
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'reads multibyte key press' do
|
19
|
+
reader = described_class.new(input: input, output: out, env: env)
|
20
|
+
input << "ㄱ"
|
21
|
+
input.rewind
|
22
|
+
|
23
|
+
answer = reader.read_keypress
|
24
|
+
|
25
|
+
expect(answer).to eq("ㄱ")
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when Ctrl+C pressed' do
|
29
|
+
it "defaults to raising InputInterrupt" do
|
30
|
+
reader = described_class.new(input: input, output: out, env: env)
|
31
|
+
input << "\x03"
|
32
|
+
input.rewind
|
33
|
+
|
34
|
+
expect {
|
35
|
+
reader.read_keypress
|
36
|
+
}.to raise_error(TTY::Reader::InputInterrupt)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "sends interrupt signal when :signal option is chosen" do
|
40
|
+
reader = described_class.new(
|
41
|
+
input: input,
|
42
|
+
output: out,
|
43
|
+
interrupt: :signal,
|
44
|
+
env: env)
|
45
|
+
input << "\x03"
|
46
|
+
input.rewind
|
47
|
+
|
48
|
+
allow(Process).to receive(:pid).and_return(666)
|
49
|
+
allow(Process).to receive(:kill)
|
50
|
+
expect(Process).to receive(:kill).with('SIGINT', 666)
|
51
|
+
|
52
|
+
reader.read_keypress
|
53
|
+
end
|
54
|
+
|
55
|
+
it "exits with 130 code when :exit option is chosen" do
|
56
|
+
reader = described_class.new(
|
57
|
+
input: input,
|
58
|
+
output: out,
|
59
|
+
interrupt: :exit,
|
60
|
+
env: env)
|
61
|
+
input << "\x03"
|
62
|
+
input.rewind
|
63
|
+
|
64
|
+
expect {
|
65
|
+
reader.read_keypress
|
66
|
+
}.to raise_error(SystemExit)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "evaluates custom handler when proc object is provided" do
|
70
|
+
handler = proc { raise ArgumentError }
|
71
|
+
reader = described_class.new(
|
72
|
+
input: input,
|
73
|
+
output: out,
|
74
|
+
interrupt: handler,
|
75
|
+
env: env)
|
76
|
+
input << "\x03"
|
77
|
+
input.rewind
|
78
|
+
|
79
|
+
expect {
|
80
|
+
reader.read_keypress
|
81
|
+
}.to raise_error(ArgumentError)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "skips handler when handler is nil" do
|
85
|
+
reader = described_class.new(
|
86
|
+
input: input,
|
87
|
+
output: out,
|
88
|
+
interrupt: :noop,
|
89
|
+
env: env)
|
90
|
+
input << "\x03"
|
91
|
+
input.rewind
|
92
|
+
|
93
|
+
expect(reader.read_keypress).to eq("\x03")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader, '#read_line' do
|
4
|
+
let(:input) { StringIO.new }
|
5
|
+
let(:output) { StringIO.new }
|
6
|
+
let(:env) { { "TTY_TEST" => true } }
|
7
|
+
|
8
|
+
subject(:reader) { described_class.new(input: input, output: output, env: env) }
|
9
|
+
|
10
|
+
it 'masks characters' do
|
11
|
+
input << "password\n"
|
12
|
+
input.rewind
|
13
|
+
answer = reader.read_line(echo: false)
|
14
|
+
expect(answer).to eq("password\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "echoes characters back" do
|
18
|
+
input << "password\n"
|
19
|
+
input.rewind
|
20
|
+
answer = reader.read_line
|
21
|
+
expect(answer).to eq("password\n")
|
22
|
+
expect(output.string).to eq([
|
23
|
+
"\e[2K\e[1Gp",
|
24
|
+
"\e[2K\e[1Gpa",
|
25
|
+
"\e[2K\e[1Gpas",
|
26
|
+
"\e[2K\e[1Gpass",
|
27
|
+
"\e[2K\e[1Gpassw",
|
28
|
+
"\e[2K\e[1Gpasswo",
|
29
|
+
"\e[2K\e[1Gpasswor",
|
30
|
+
"\e[2K\e[1Gpassword",
|
31
|
+
"\e[2K\e[1Gpassword\n"
|
32
|
+
].join)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "doesn't echo characters back" do
|
36
|
+
input << "password\n"
|
37
|
+
input.rewind
|
38
|
+
answer = reader.read_line(echo: false)
|
39
|
+
expect(answer).to eq("password\n")
|
40
|
+
expect(output.string).to eq("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "displays a prompt before input" do
|
44
|
+
input << "aa\n"
|
45
|
+
input.rewind
|
46
|
+
answer = reader.read_line('>> ')
|
47
|
+
expect(answer).to eq("aa\n")
|
48
|
+
expect(output.string).to eq([
|
49
|
+
">> ",
|
50
|
+
"\e[2K\e[1G>> a",
|
51
|
+
"\e[2K\e[1G>> aa",
|
52
|
+
"\e[2K\e[1G>> aa\n"
|
53
|
+
].join)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'deletes characters when backspace pressed' do
|
57
|
+
input << "aa\ba\bcc\n"
|
58
|
+
input.rewind
|
59
|
+
answer = reader.read_line
|
60
|
+
expect(answer).to eq("acc\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'reads multibyte line' do
|
64
|
+
input << "한글"
|
65
|
+
input.rewind
|
66
|
+
answer = reader.read_line
|
67
|
+
expect(answer).to eq("한글")
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader, '#read_multiline' do
|
4
|
+
let(:input) { StringIO.new }
|
5
|
+
let(:output) { StringIO.new }
|
6
|
+
let(:env) { { "TTY_TEST" => true } }
|
7
|
+
|
8
|
+
subject(:reader) { described_class.new(input: input, output: output, env: env) }
|
9
|
+
|
10
|
+
it 'reads no lines' do
|
11
|
+
input << "\C-d"
|
12
|
+
input.rewind
|
13
|
+
answer = reader.read_multiline
|
14
|
+
expect(answer).to eq([])
|
15
|
+
end
|
16
|
+
|
17
|
+
it "reads a line and terminates on Ctrl+d" do
|
18
|
+
input << "Single line\C-d"
|
19
|
+
input.rewind
|
20
|
+
answer = reader.read_multiline
|
21
|
+
expect(answer).to eq(["Single line"])
|
22
|
+
end
|
23
|
+
|
24
|
+
it "reads a line and terminates on Ctrl+z" do
|
25
|
+
input << "Single line\C-z"
|
26
|
+
input.rewind
|
27
|
+
answer = reader.read_multiline
|
28
|
+
expect(answer).to eq(["Single line"])
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'reads few lines' do
|
32
|
+
input << "First line\nSecond line\nThird line\n\C-d"
|
33
|
+
input.rewind
|
34
|
+
answer = reader.read_multiline
|
35
|
+
expect(answer).to eq(["First line\n", "Second line\n", "Third line\n"])
|
36
|
+
end
|
37
|
+
|
38
|
+
it "skips empty lines" do
|
39
|
+
input << "\n\nFirst line\n\n\n\n\nSecond line\C-d"
|
40
|
+
input.rewind
|
41
|
+
answer = reader.read_multiline
|
42
|
+
expect(answer).to eq(["First line\n", "Second line"])
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'reads and yiels every line' do
|
46
|
+
input << "First line\nSecond line\nThird line\C-z"
|
47
|
+
input.rewind
|
48
|
+
lines = []
|
49
|
+
reader.read_multiline { |line| lines << line }
|
50
|
+
expect(lines).to eq(["First line\n", "Second line\n", "Third line"])
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'reads multibyte lines' do
|
54
|
+
input << "국경의 긴 터널을 빠져나오자\n설국이었다.\C-d"
|
55
|
+
input.rewind
|
56
|
+
lines = []
|
57
|
+
reader.read_multiline { |line| lines << line }
|
58
|
+
expect(lines).to eq(["국경의 긴 터널을 빠져나오자\n", '설국이었다.'])
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'reads lines with a prompt' do
|
62
|
+
input << "1\n2\n3\C-d"
|
63
|
+
input.rewind
|
64
|
+
reader.read_multiline(">> ")
|
65
|
+
expect(output.string).to eq([
|
66
|
+
">> ",
|
67
|
+
"\e[2K\e[1G>> 1",
|
68
|
+
"\e[2K\e[1G>> 1\n",
|
69
|
+
">> ",
|
70
|
+
"\e[2K\e[1G>> 2",
|
71
|
+
"\e[2K\e[1G>> 2\n",
|
72
|
+
">> ",
|
73
|
+
"\e[2K\e[1G>> 3",
|
74
|
+
].join)
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader, '#subscribe' do
|
4
|
+
let(:input) { StringIO.new }
|
5
|
+
let(:output) { StringIO.new }
|
6
|
+
let(:env) { { "TTY_TEST" => true } }
|
7
|
+
|
8
|
+
it "subscribes to receive events" do
|
9
|
+
stub_const("Context", Class.new do
|
10
|
+
def initialize(events)
|
11
|
+
@events = events
|
12
|
+
end
|
13
|
+
|
14
|
+
def keypress(event)
|
15
|
+
@events << [:keypress, event.value]
|
16
|
+
end
|
17
|
+
end)
|
18
|
+
|
19
|
+
reader = TTY::Reader.new(input: input, output: output, env: env)
|
20
|
+
events = []
|
21
|
+
context = Context.new(events)
|
22
|
+
reader.subscribe(context)
|
23
|
+
|
24
|
+
input << "aa\n"
|
25
|
+
input.rewind
|
26
|
+
answer = reader.read_line
|
27
|
+
|
28
|
+
expect(answer).to eq("aa\n")
|
29
|
+
expect(events).to eq([
|
30
|
+
[:keypress, "a"],
|
31
|
+
[:keypress, "a"],
|
32
|
+
[:keypress, "\n"]
|
33
|
+
])
|
34
|
+
|
35
|
+
events.clear
|
36
|
+
|
37
|
+
reader.unsubscribe(context)
|
38
|
+
|
39
|
+
input.rewind
|
40
|
+
answer = reader.read_line
|
41
|
+
expect(events).to eq([])
|
42
|
+
end
|
43
|
+
|
44
|
+
it "subscribes to listen to events only in a block" do
|
45
|
+
stub_const("Context", Class.new do
|
46
|
+
def initialize(events)
|
47
|
+
@events = events
|
48
|
+
end
|
49
|
+
|
50
|
+
def keypress(event)
|
51
|
+
@events << [:keypress, event.value]
|
52
|
+
end
|
53
|
+
end)
|
54
|
+
|
55
|
+
reader = TTY::Reader.new(input: input, output: output, env: env)
|
56
|
+
events = []
|
57
|
+
context = Context.new(events)
|
58
|
+
|
59
|
+
input << "aa\nbb\n"
|
60
|
+
input.rewind
|
61
|
+
|
62
|
+
reader.subscribe(context) do
|
63
|
+
reader.read_line
|
64
|
+
end
|
65
|
+
answer = reader.read_line
|
66
|
+
|
67
|
+
expect(answer).to eq("bb\n")
|
68
|
+
expect(events).to eq([
|
69
|
+
[:keypress, "a"],
|
70
|
+
[:keypress, "a"],
|
71
|
+
[:keypress, "\n"]
|
72
|
+
])
|
73
|
+
end
|
74
|
+
end
|
data/tty-reader.gemspec
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
lib = File.expand_path("../lib", __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
3
|
require "tty/reader/version"
|
@@ -9,11 +8,13 @@ Gem::Specification.new do |spec|
|
|
9
8
|
spec.authors = ["Piotr Murach"]
|
10
9
|
spec.email = [""]
|
11
10
|
spec.summary = %q{A set of methods for processing keyboard input in character, line and multiline modes.}
|
12
|
-
spec.description = %q{A set of methods for processing keyboard input in character, line and multiline modes.
|
11
|
+
spec.description = %q{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.}
|
13
12
|
spec.homepage = "https://piotrmurach.github.io/tty"
|
14
13
|
spec.license = "MIT"
|
15
14
|
|
16
|
-
spec.files = Dir[
|
15
|
+
spec.files = Dir['{lib,spec,examples,benchmarks}/**/*.rb']
|
16
|
+
spec.files += Dir['{bin,tasks}/*', 'tty-reader.gemspec']
|
17
|
+
spec.files += Dir['README.md', 'CHANGELOG.md', 'LICENSE.txt', 'Rakefile']
|
17
18
|
spec.bindir = "exe"
|
18
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
20
|
spec.require_paths = ["lib"]
|