infopark-user_io 0.0.6

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 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: []