tty-reader 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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. In addition it maintains history of entered input with an ability to recall and re-edit those inputs and register to listen for keystrokes.}
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["README.md", "LICENSE.txt", "Rakefile", "lib/**/*.rb", "bin/**", "examples/**/*.rb", "tasks/**", "tty-reader.gemspec"]
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"]