infopark-user_io 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 09b42d53dccdd52424b6ae8eb12a6a2241199db2
4
+ data.tar.gz: 19de857e3bf959eef4c2c0ccd908c7c6060f0c15
5
+ SHA512:
6
+ metadata.gz: 445017a230548c5f55439e5082ce7519a8385e8fae5c21786431844a6b5b887ba037422ac8c8ed10449c1e5e787ae10898829e8cf24f8a06333a822b93792af5
7
+ data.tar.gz: f4b865b51d53d6f212ab0982636976f0a3d4a7687f8155a0147c3b489d40ab8bb9160aeedf44d24dd60b4c1310c54f27781c65c38cff9e2a611e3626c7feca20
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /pkg
2
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,5 @@
1
+ module Infopark
2
+ class UserIO
3
+ VERSION = "0.0.6"
4
+ end
5
+ end
@@ -0,0 +1,283 @@
1
+ module Infopark
2
+
3
+ # TODO extract into infopark base gem
4
+ class ImplementationError < StandardError; end
5
+
6
+ # TODO
7
+ # - beep (\a) on #acknowledge, #ask or #confirm (and maybe on #listen, too)
8
+ class UserIO
9
+ class Aborted < RuntimeError
10
+ end
11
+
12
+ class MissingEnv < RuntimeError
13
+ end
14
+
15
+ class Progress
16
+ def initialize(label, user_io)
17
+ @label = label
18
+ @user_io = user_io
19
+ end
20
+
21
+ def start
22
+ unless @started
23
+ user_io.tell("#{label} ", newline: false)
24
+ @started = true
25
+ end
26
+ end
27
+
28
+ def increment
29
+ raise ImplementationError, "progress not started yet" unless @started
30
+ user_io.tell(".", newline: false)
31
+ end
32
+
33
+ def finish
34
+ if @started
35
+ user_io.tell("… ", newline: false)
36
+ user_io.tell("OK", color: :green, bright: true)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :label, :user_io
43
+ end
44
+
45
+ def initialize(output_prefix: nil)
46
+ case output_prefix
47
+ when String
48
+ @output_prefix = "[#{output_prefix}] "
49
+ when Proc, Method
50
+ @output_prefix_proc = ->() { "[#{output_prefix.call}] " }
51
+ when :timestamp
52
+ @output_prefix_proc = ->() { "[#{Time.now.strftime("%T.%L")}] " }
53
+ end
54
+ @line_pending = {}
55
+ end
56
+
57
+ def tell(*texts, newline: true, **line_options)
58
+ lines = texts.flatten.map {|text| text.to_s.split("\n", -1) }.flatten
59
+
60
+ lines[0...-1].each {|line| tell_line(line, **line_options) }
61
+ tell_line(lines.last, newline: newline, **line_options)
62
+ end
63
+
64
+ def warn(*text)
65
+ tell(*text, color: :yellow, bright: true)
66
+ end
67
+
68
+ def tell_error(e, **options)
69
+ tell(e, **options, color: :red, bright: true)
70
+ end
71
+
72
+ def acknowledge(*text)
73
+ tell("-" * 80)
74
+ tell(*text, color: :cyan, bright: true)
75
+ tell("-" * 80)
76
+ tell("Please press ENTER to continue.")
77
+ read_line
78
+ end
79
+
80
+ def ask(*text, default: nil)
81
+ # TODO implementation error if default not boolean or nil
82
+ tell("-" * 80)
83
+ tell(*text, color: :cyan, bright: true)
84
+ tell("-" * 80)
85
+ default_answer = default ? "yes" : "no" unless default.nil?
86
+ tell("(yes/no) #{default_answer && "[#{default_answer}] "}> ", newline: false)
87
+ until %w(yes no).include?(answer = read_line.strip.downcase)
88
+ if answer.empty?
89
+ answer = default_answer
90
+ break
91
+ end
92
+ tell("I couldn't understand “#{answer}”.", newline: false, color: :red, bright: true)
93
+ tell(" > ", newline: false)
94
+ end
95
+ answer == "yes"
96
+ end
97
+
98
+ def listen(prompt = nil, **options)
99
+ prompt << " " if prompt
100
+ tell("#{prompt}> ", **options, newline: false)
101
+ read_line.strip
102
+ end
103
+
104
+ def confirm(*text)
105
+ ask(*text) or raise Aborted
106
+ end
107
+
108
+ def new_progress(label)
109
+ Progress.new(label, self)
110
+ end
111
+
112
+ def start_progress(label)
113
+ new_progress(label).tap(&:start)
114
+ end
115
+
116
+ def background_other_threads
117
+ unless @foreground_thread
118
+ @background_lines = []
119
+ @foreground_thread = Thread.current
120
+ end
121
+ end
122
+
123
+ def foreground
124
+ if @foreground_thread
125
+ @background_lines.each(&STDOUT.method(:write))
126
+ @foreground_thread = nil
127
+ # take over line_pending from background
128
+ @line_pending[false] = @line_pending[true]
129
+ @line_pending[true] = false
130
+ end
131
+ end
132
+
133
+ def <<(msg)
134
+ tell(msg.chomp, newline: msg.end_with?("\n"))
135
+ end
136
+
137
+ def tty?
138
+ STDOUT.tty?
139
+ end
140
+
141
+ def edit_file(kind_of_data, filename = nil)
142
+ wait_for_foreground if background?
143
+
144
+ editor = ENV['EDITOR'] or raise MissingEnv, "No EDITOR specified."
145
+
146
+ filename ||= Tempfile.new("").path
147
+ tell("Start editing #{kind_of_data} using #{editor}…")
148
+ sleep(1.7)
149
+ system(editor, filename)
150
+
151
+ File.read(filename)
152
+ end
153
+
154
+ def select(description, items, item_describer: :to_s, default: nil)
155
+ return if items.empty?
156
+
157
+ describer =
158
+ case item_describer
159
+ when Method, Proc
160
+ item_describer
161
+ else
162
+ ->(item) { item.send(item_describer) }
163
+ end
164
+
165
+ choice = nil
166
+ if items.size == 1
167
+ choice = items.first
168
+ tell("Selected #{describer.call(choice)}.", color: :yellow)
169
+ return choice
170
+ end
171
+
172
+ items = items.sort_by(&describer)
173
+
174
+ tell("-" * 80)
175
+ tell("Please select #{description}:", color: :cyan, bright: true)
176
+ items.each_with_index do |item, i|
177
+ tell("#{i + 1}: #{describer.call(item)}", color: :cyan, bright: true)
178
+ end
179
+ tell("-" * 80)
180
+ default_index = items.index(default)
181
+ default_selection = "[#{default_index + 1}] " if default_index
182
+ until choice
183
+ tell("Your choice #{default_selection}> ", newline: false)
184
+ answer = read_line.strip
185
+ if answer.empty?
186
+ choice = default
187
+ else
188
+ int_answer = answer.to_i
189
+ if int_answer.to_s != answer
190
+ tell("Please enter a valid integer.")
191
+ elsif int_answer < 1 || int_answer > items.size
192
+ tell("Please enter a number from 1 through #{items.size}.")
193
+ else
194
+ choice = items[int_answer - 1]
195
+ end
196
+ end
197
+ end
198
+ choice
199
+ end
200
+
201
+ private
202
+
203
+ def background?
204
+ !!@foreground_thread && @foreground_thread != Thread.current
205
+ end
206
+
207
+ def wait_for_foreground
208
+ sleep 0.1 while background?
209
+ end
210
+
211
+ def output_prefix
212
+ @output_prefix || @output_prefix_proc && @output_prefix_proc.call
213
+ end
214
+
215
+ def read_line
216
+ wait_for_foreground if background?
217
+ @line_pending[false] = false
218
+ STDIN.gets.chomp
219
+ end
220
+
221
+ def tell_line(line, newline: true, prefix: true, **color_options)
222
+ if tty?
223
+ if line_prefix = text_color(**color_options)
224
+ line_postfix = text_color(color: :none, bright: false)
225
+ end
226
+ end
227
+
228
+ prefix = false if @line_pending[background?]
229
+
230
+ out_line = "#{output_prefix if prefix}#{line_prefix}#{line}#{line_postfix}#{"\n" if newline}"
231
+ if background?
232
+ @background_lines << out_line
233
+ else
234
+ STDOUT.write(out_line)
235
+ end
236
+
237
+ @line_pending[background?] = !newline
238
+ end
239
+
240
+ def control_sequence(*parameters, function)
241
+ "\033[#{parameters.join(";")}#{function}"
242
+ end
243
+
244
+ # SGR: Select Graphic Rendition … far too long for a function name ;)
245
+ def sgr_sequence(*parameters)
246
+ control_sequence(*parameters, :m)
247
+ end
248
+
249
+ def text_color(color: nil, bright: nil, faint: nil, italic: nil, underline: nil)
250
+ return if color.nil? && bright.nil?
251
+ sequence = []
252
+ unless bright.nil? && faint.nil?
253
+ sequence << (bright ? 1 : (faint ? 2 : 22))
254
+ end
255
+ unless italic.nil?
256
+ sequence << (italic ? 3 : 23)
257
+ end
258
+ unless underline.nil?
259
+ sequence << (underline ? 4 : 24)
260
+ end
261
+ case color
262
+ when :red
263
+ sequence << 31
264
+ when :green
265
+ sequence << 32
266
+ when :yellow
267
+ sequence << 33
268
+ when :blue
269
+ sequence << 34
270
+ when :purple
271
+ sequence << 35
272
+ when :cyan
273
+ sequence << 36
274
+ when :white
275
+ sequence << 37
276
+ when :none
277
+ sequence << 39
278
+ end
279
+ sgr_sequence(*sequence)
280
+ end
281
+ end
282
+
283
+ end
@@ -0,0 +1 @@
1
+ require 'infopark/user_io'
@@ -0,0 +1,64 @@
1
+ require_relative '../lib/infopark-user_io'
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |expectations|
5
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6
+ end
7
+
8
+ config.mock_with :rspec do |mocks|
9
+ mocks.verify_partial_doubles = true
10
+ end
11
+
12
+ config.shared_context_metadata_behavior = :apply_to_host_groups
13
+
14
+ config.example_status_persistence_file_path = "spec/examples.txt"
15
+
16
+ # The settings below are suggested to provide a good initial experience
17
+ # with RSpec, but feel free to customize to your heart's content.
18
+ =begin
19
+ # This allows you to limit a spec run to individual examples or groups
20
+ # you care about by tagging them with `:focus` metadata. When nothing
21
+ # is tagged with `:focus`, all examples get run. RSpec also provides
22
+ # aliases for `it`, `describe`, and `context` that include `:focus`
23
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
24
+ config.filter_run_when_matching :focus
25
+
26
+ # Limits the available syntax to the non-monkey patched syntax that is
27
+ # recommended. For more details, see:
28
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
29
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
30
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
31
+ config.disable_monkey_patching!
32
+
33
+ # This setting enables warnings. It's recommended, but in some cases may
34
+ # be too noisy due to issues in dependencies.
35
+ config.warnings = true
36
+
37
+ # Many RSpec users commonly either run the entire suite or an individual
38
+ # file, and it's useful to allow more verbose output when running an
39
+ # individual spec file.
40
+ if config.files_to_run.one?
41
+ # Use the documentation formatter for detailed output,
42
+ # unless a formatter has already been configured
43
+ # (e.g. via a command-line flag).
44
+ config.default_formatter = "doc"
45
+ end
46
+
47
+ # Print the 10 slowest examples and example groups at the
48
+ # end of the spec run, to help surface which specs are running
49
+ # particularly slow.
50
+ config.profile_examples = 10
51
+
52
+ # Run specs in random order to surface order dependencies. If you find an
53
+ # order dependency and want to debug it, you can fix the order by providing
54
+ # the seed, which is printed after each run.
55
+ # --seed 1234
56
+ config.order = :random
57
+
58
+ # Seed global randomization in this process using the `--seed` CLI option.
59
+ # Setting this allows you to use `--seed` to deterministically reproduce
60
+ # test failures related to randomization by passing the same `--seed` value
61
+ # as the one that triggered the failure.
62
+ Kernel.srand config.seed
63
+ =end
64
+ end
@@ -0,0 +1,115 @@
1
+ RSpec.describe ::Infopark::UserIO do
2
+ let(:options) { {} }
3
+
4
+ subject(:user_io) { ::Infopark::UserIO.new(**options) }
5
+
6
+ before do
7
+ allow($stdout).to receive(:puts)
8
+ # for debugging: .and_call_original
9
+ allow($stdout).to receive(:write)
10
+ # for debugging: .and_call_original
11
+ end
12
+
13
+ describe "#acknowledge" do
14
+ before { allow($stdin).to receive(:gets).and_return("\n") }
15
+
16
+ let(:message) { "Some important statement." }
17
+
18
+ subject(:acknowledge) { user_io.acknowledge(message) }
19
+
20
+ it "presents the message (colorized)" do
21
+ expect($stdout).to receive(:write).with("\e[1;36m""Some important statement.""\e[22;39m\n")
22
+ acknowledge
23
+ end
24
+
25
+ it "asks for pressing “Enter”" do
26
+ expect($stdout).to receive(:write).with("Please press ENTER to continue.\n")
27
+ acknowledge
28
+ end
29
+
30
+ it "requests input" do
31
+ expect($stdin).to receive(:gets).and_return("\n")
32
+ acknowledge
33
+ end
34
+ end
35
+
36
+ describe "#ask" do
37
+ before { allow($stdin).to receive(:gets).and_return("yes\n") }
38
+
39
+ let(:ask_options) { {} }
40
+ let(:question) { "do you want to?" }
41
+
42
+ subject(:ask) { user_io.ask(*Array(question), **ask_options) }
43
+
44
+ shared_examples_for "any question" do
45
+ # TODO
46
+ #it_behaves_like "handling valid answer"
47
+ #it_behaves_like "handling invalid input"
48
+ #it_behaves_like "printing prefix on every line"
49
+ end
50
+
51
+ context "with default" do
52
+ let(:ask_options) { {default: default_value} }
53
+
54
+ context "“true”" do
55
+ let(:default_value) { true }
56
+
57
+ it "presents default answer “yes”" do
58
+ expect($stdout).to receive(:write).with("(yes/no) [yes] > ")
59
+ ask
60
+ end
61
+
62
+ it "returns “true” on empty input" do
63
+ expect($stdin).to receive(:gets).and_return("\n")
64
+ expect(ask).to be true
65
+ end
66
+
67
+ it_behaves_like "any question"
68
+ end
69
+
70
+ context "“false”" do
71
+ let(:default_value) { false }
72
+
73
+ it "presents default answer “no”" do
74
+ expect($stdout).to receive(:write).with("(yes/no) [no] > ")
75
+ ask
76
+ end
77
+
78
+ it "returns “false” on empty input" do
79
+ expect($stdin).to receive(:gets).and_return("\n")
80
+ expect(ask).to be false
81
+ end
82
+
83
+ it_behaves_like "any question"
84
+ end
85
+
86
+ context "non boolean" do
87
+ # TODO
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#select" do
93
+ before { allow($stdin).to receive(:gets).and_return("1\n") }
94
+
95
+ let(:description) { "a thing" }
96
+ let(:items) { [:a, :b, :c] }
97
+ let(:select_options) { {} }
98
+
99
+ subject(:select) { user_io.select(description, items, **select_options) }
100
+
101
+ context "with default" do
102
+ let(:select_options) { {default: :b} }
103
+
104
+ it "presents the default's index as default answer" do
105
+ expect($stdout).to receive(:write).with("Your choice [2] > ")
106
+ select
107
+ end
108
+
109
+ it "returns the default on empty input" do
110
+ expect($stdin).to receive(:gets).and_return("\n")
111
+ expect(select).to eq(:b)
112
+ end
113
+ end
114
+ end
115
+ end
data/user_io.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ require_relative 'lib/infopark/user_io/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'infopark-user_io'
5
+ s.version = Infopark::UserIO::VERSION
6
+ s.summary = 'A utility lib to interact with the user on the command line.'
7
+ s.description = s.summary
8
+ s.authors = ['Tilo Prütz']
9
+ s.email = 'tilo@infopark.de'
10
+ s.files = `git ls-files -z`.split("\0")
11
+ s.license = 'UNLICENSED'
12
+
13
+ s.add_development_dependency "bundler"
14
+ s.add_development_dependency "rake"
15
+ s.add_development_dependency "rspec"
16
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: infopark-user_io
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Tilo Prütz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A utility lib to interact with the user on the command line.
56
+ email: tilo@infopark.de
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gitignore"
62
+ - ".rspec"
63
+ - Gemfile
64
+ - Rakefile
65
+ - lib/infopark-user_io.rb
66
+ - lib/infopark/user_io.rb
67
+ - lib/infopark/user_io/version.rb
68
+ - spec/spec_helper.rb
69
+ - spec/user_io_spec.rb
70
+ - user_io.gemspec
71
+ homepage:
72
+ licenses:
73
+ - UNLICENSED
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.4.5.1
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A utility lib to interact with the user on the command line.
95
+ test_files: []