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.
@@ -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"]