infopark-user_io 1.2.1 → 1.3.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.
@@ -1,203 +1,209 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Infopark
4
+ # TODO: extract into infopark base gem
5
+ class ImplementationError < StandardError; end
2
6
 
3
- # TODO extract into infopark base gem
4
- class ImplementationError < StandardError; end
7
+ # TODO
8
+ # - beep (\a) on #acknowledge, #ask or #confirm (and maybe on #listen, too)
9
+ class UserIO
10
+ class Aborted < RuntimeError
11
+ end
5
12
 
6
- # TODO
7
- # - beep (\a) on #acknowledge, #ask or #confirm (and maybe on #listen, too)
8
- class UserIO
9
- class Aborted < RuntimeError
10
- end
13
+ class MissingEnv < RuntimeError
14
+ end
11
15
 
12
- class MissingEnv < RuntimeError
13
- end
16
+ class Progress
17
+ def initialize(label, user_io)
18
+ @label = label
19
+ @user_io = user_io
20
+ @spinner = "-\\|/"
21
+ end
14
22
 
15
- class Progress
16
- def initialize(label, user_io)
17
- @label = label
18
- @user_io = user_io
19
- @spinner = "-\\|/"
20
- end
23
+ def start
24
+ return if @started
21
25
 
22
- def start
23
- unless @started
24
26
  user_io.tell("#{label} ", newline: false)
25
27
  @started = true
26
28
  reset_spinner
27
29
  end
28
- end
29
30
 
30
- def increment
31
- raise ImplementationError, "progress not started yet" unless @started
32
- user_io.tell(".", newline: false)
33
- reset_spinner
34
- end
31
+ def increment
32
+ raise(ImplementationError, "progress not started yet") unless @started
33
+
34
+ user_io.tell(".", newline: false)
35
+ reset_spinner
36
+ end
37
+
38
+ def finish
39
+ return unless @started
35
40
 
36
- def finish
37
- if @started
38
41
  user_io.tell("… ", newline: false)
39
42
  user_io.tell("OK", color: :green, bright: true)
40
43
  @started = false
41
44
  end
42
- end
43
45
 
44
- def spin
45
- raise ImplementationError, "progress not started yet" unless @started
46
- user_io.tell("#{@spinner[@spin_pos % @spinner.size]}\b", newline: false)
47
- @spin_pos += 1
48
- end
46
+ def spin
47
+ raise(ImplementationError, "progress not started yet") unless @started
49
48
 
50
- private
49
+ user_io.tell("#{@spinner[@spin_pos % @spinner.size]}\b", newline: false)
50
+ @spin_pos += 1
51
+ end
52
+
53
+ private
51
54
 
52
- attr_reader :label, :user_io
55
+ attr_reader :label
56
+ attr_reader :user_io
53
57
 
54
- def reset_spinner
55
- @spin_pos = 0
58
+ def reset_spinner
59
+ @spin_pos = 0
60
+ end
56
61
  end
57
- end
58
62
 
59
- class << self
60
- attr_accessor :global
61
- end
63
+ class << self
64
+ attr_accessor :global
65
+ end
62
66
 
63
- def initialize(output_prefix: nil)
64
- case output_prefix
65
- when String
66
- @output_prefix = "[#{output_prefix}] "
67
- when Proc, Method
68
- @output_prefix_proc = ->() { "[#{output_prefix.call}] " }
69
- when :timestamp
70
- @output_prefix_proc = ->() { "[#{Time.now.strftime("%T.%L")}] " }
67
+ def initialize(output_prefix: nil)
68
+ case output_prefix
69
+ when String
70
+ @output_prefix = "[#{output_prefix}] "
71
+ when Proc, Method
72
+ @output_prefix_proc = -> { "[#{output_prefix.call}] " }
73
+ when :timestamp
74
+ @output_prefix_proc = -> { "[#{Time.now.strftime('%T.%L')}] " }
75
+ end
76
+ @line_pending = {}
71
77
  end
72
- @line_pending = {}
73
- end
74
78
 
75
- def tell(*texts, newline: true, **line_options)
76
- lines = texts.flatten.map {|text| text.to_s.split("\n", -1) }.flatten
79
+ def tell(*texts, newline: true, **line_options)
80
+ lines = texts.flatten.map {|text| text.to_s.split("\n", -1) }.flatten
77
81
 
78
- lines[0...-1].each {|line| tell_line(line, **line_options) }
79
- tell_line(lines.last, newline: newline, **line_options)
80
- end
82
+ lines[0...-1].each {|line| tell_line(line, **line_options) }
83
+ tell_line(lines.last, newline:, **line_options)
84
+ end
81
85
 
82
- def tell_pty_stream(stream, **color_options)
83
- color_prefix, color_postfix = compute_color(**color_options)
84
- write_raw(output_prefix) unless line_pending?
85
- write_raw(color_prefix)
86
- nl_pending = false
87
- uncolored_prefix = "#{color_postfix}#{output_prefix}#{color_prefix}"
88
- until stream.eof?
89
- chunk = stream.read_nonblock(100)
90
- next if chunk.empty?
91
- write_raw("\n#{uncolored_prefix}") if nl_pending
92
- chunk.chop! if nl_pending = chunk.end_with?("\n")
93
- chunk.gsub!(/([\r\n])/, "\\1#{uncolored_prefix}")
94
- write_raw(chunk)
95
- end
96
- write_raw("\n") if nl_pending
97
- write_raw(color_postfix)
98
- line_pending!(false)
99
- end
86
+ def tell_pty_stream(stream, **color_options)
87
+ color_prefix, color_postfix = compute_color(**color_options)
88
+ write_raw(output_prefix) unless line_pending?
89
+ write_raw(color_prefix)
90
+ nl_pending = false
91
+ uncolored_prefix = "#{color_postfix}#{output_prefix}#{color_prefix}"
92
+ until stream.eof?
93
+ chunk = stream.read_nonblock(100)
94
+ next if chunk.empty?
95
+
96
+ write_raw("\n#{uncolored_prefix}") if nl_pending
97
+ chunk.chop! if (nl_pending = chunk.end_with?("\n"))
98
+ chunk.gsub!(/([\r\n])/, "\\1#{uncolored_prefix}")
99
+ write_raw(chunk)
100
+ end
101
+ write_raw("\n") if nl_pending
102
+ write_raw(color_postfix)
103
+ line_pending!(false)
104
+ end
100
105
 
101
- def warn(*text)
102
- tell(*text, color: :yellow, bright: true)
103
- end
106
+ def warn(*text)
107
+ tell(*text, color: :yellow, bright: true)
108
+ end
104
109
 
105
- def tell_error(e, **options)
106
- tell(e, **options, color: :red, bright: true)
107
- tell(e.backtrace, **options, color: :red) if Exception === e
108
- end
110
+ def tell_error(e, **options)
111
+ tell(e, **options, color: :red, bright: true)
112
+ tell(e.backtrace, **options, color: :red) if Exception === e
113
+ end
109
114
 
110
- def acknowledge(*text)
111
- tell("-" * 80)
112
- tell(*text, color: :cyan, bright: true)
113
- tell("-" * 80)
114
- tell("Please press ENTER to continue.")
115
- read_line
116
- end
115
+ def acknowledge(*text)
116
+ tell("-" * 80)
117
+ tell(*text, color: :cyan, bright: true)
118
+ tell("-" * 80)
119
+ tell("Please press ENTER to continue.")
120
+ read_line
121
+ end
117
122
 
118
- def ask(*text, default: nil, expected: "yes")
119
- # TODO implementation error if default not boolean or nil
120
- # TODO implementation error if expected not "yes" or "no"
121
- tell("-" * 80)
122
- tell(*text, color: :cyan, bright: true)
123
- tell("-" * 80)
124
- default_answer = default ? "yes" : "no" unless default.nil?
125
- tell("(yes/no) #{default_answer && "[#{default_answer}] "}> ", newline: false)
126
- until %w(yes no).include?(answer = read_line.strip.downcase)
127
- if answer.empty?
128
- answer = default_answer
129
- break
123
+ def ask(*text, default: nil, expected: "yes")
124
+ # TODO
125
+ # - implementation error if default not boolean or nil
126
+ # - implementation error if expected not "yes" or "no"
127
+ tell("-" * 80)
128
+ tell(*text, color: :cyan, bright: true)
129
+ tell("-" * 80)
130
+ default_answer = default ? "yes" : "no" unless default.nil?
131
+ tell("(yes/no) #{default_answer && "[#{default_answer}] "}> ", newline: false)
132
+ until %w(yes no).include?(answer = read_line.strip.downcase)
133
+ if answer.empty?
134
+ answer = default_answer
135
+ break
136
+ end
137
+ tell("I couldn't understand “#{answer}”.", newline: false, color: :red, bright: true)
138
+ tell(" > ", newline: false)
130
139
  end
131
- tell("I couldn't understand “#{answer}”.", newline: false, color: :red, bright: true)
132
- tell(" > ", newline: false)
140
+ answer == expected
133
141
  end
134
- answer == expected
135
- end
136
142
 
137
- def listen(prompt = nil, **options)
138
- prompt << " " if prompt
139
- tell("#{prompt}> ", **options, newline: false)
140
- read_line.strip
141
- end
143
+ def listen(prompt = nil, **options)
144
+ prompt << " " if prompt
145
+ tell("#{prompt}> ", **options, newline: false)
146
+ read_line.strip
147
+ end
142
148
 
143
- def confirm(*text)
144
- ask(*text) or raise Aborted
145
- end
149
+ def confirm(*text)
150
+ ask(*text) or raise(Aborted)
151
+ end
146
152
 
147
- def new_progress(label)
148
- Progress.new(label, self)
149
- end
153
+ def new_progress(label)
154
+ Progress.new(label, self)
155
+ end
150
156
 
151
- def start_progress(label)
152
- new_progress(label).tap(&:start)
153
- end
157
+ def start_progress(label)
158
+ new_progress(label).tap(&:start)
159
+ end
160
+
161
+ def background_other_threads
162
+ return if @foreground_thread
154
163
 
155
- def background_other_threads
156
- unless @foreground_thread
157
164
  @background_data = []
158
165
  @foreground_thread = Thread.current
159
166
  end
160
- end
161
167
 
162
- def foreground
163
- if @foreground_thread
164
- @background_data.each(&STDOUT.method(:write))
168
+ def foreground
169
+ return unless @foreground_thread
170
+
171
+ @background_data.each(&$stdout.method(:write))
165
172
  @foreground_thread = nil
166
173
  # take over line_pending from background
167
174
  @line_pending[false] = @line_pending[true]
168
175
  @line_pending[true] = false
169
176
  end
170
- end
171
177
 
172
- def <<(msg)
173
- tell(msg.chomp, newline: msg.end_with?("\n"))
174
- end
178
+ def <<(msg)
179
+ tell(msg.chomp, newline: msg.end_with?("\n"))
180
+ end
175
181
 
176
- def tty?
177
- STDOUT.tty?
178
- end
182
+ def tty?
183
+ $stdout.tty?
184
+ end
179
185
 
180
- def edit_file(kind_of_data, filename = nil, template: nil)
181
- wait_for_foreground if background?
186
+ def edit_file(kind_of_data, filename = nil, template: nil)
187
+ wait_for_foreground if background?
182
188
 
183
- editor = ENV['EDITOR'] or raise MissingEnv, "No EDITOR specified."
189
+ editor = ENV.fetch("EDITOR", nil) or raise(MissingEnv, "No EDITOR specified.")
184
190
 
185
- filename ||= Tempfile.new("").path
186
- if template && (!File.exists?(filename) || File.empty?(filename))
187
- File.write(filename, template)
188
- end
191
+ filename ||= Tempfile.new("").path
192
+ if template && (!File.exist?(filename) || File.empty?(filename))
193
+ File.write(filename, template)
194
+ end
189
195
 
190
- tell("Start editing #{kind_of_data} using #{editor}…")
191
- sleep(1.7)
192
- system(editor, filename)
196
+ tell("Start editing #{kind_of_data} using #{editor}…")
197
+ sleep(1.7)
198
+ system(editor, filename)
193
199
 
194
- File.read(filename)
195
- end
200
+ File.read(filename)
201
+ end
196
202
 
197
- def select(description, items, item_describer: :to_s, default: nil)
198
- return if items.empty?
203
+ def select(description, items, item_describer: :to_s, default: nil)
204
+ return if items.empty?
199
205
 
200
- describer =
206
+ describer =
201
207
  case item_describer
202
208
  when Method, Proc
203
209
  item_describer
@@ -205,141 +211,144 @@ class UserIO
205
211
  ->(item) { item.send(item_describer) }
206
212
  end
207
213
 
208
- choice = nil
209
- if items.size == 1
210
- choice = items.first
211
- tell("Selected #{describer.call(choice)}.", color: :yellow)
212
- return choice
213
- end
214
+ choice = nil
215
+ if items.size == 1
216
+ choice = items.first
217
+ tell("Selected #{describer.call(choice)}.", color: :yellow)
218
+ return choice
219
+ end
214
220
 
215
- items = items.sort_by(&describer)
221
+ items = items.sort_by(&describer)
216
222
 
217
- tell("-" * 80)
218
- tell("Please select #{description}:", color: :cyan, bright: true)
219
- items.each_with_index do |item, i|
220
- tell("#{i + 1}: #{describer.call(item)}", color: :cyan, bright: true)
221
- end
222
- tell("-" * 80)
223
- default_index = items.index(default)
224
- default_selection = "[#{default_index + 1}] " if default_index
225
- until choice
226
- tell("Your choice #{default_selection}> ", newline: false)
227
- answer = read_line.strip
228
- if answer.empty?
229
- choice = default
230
- else
231
- int_answer = answer.to_i
232
- if int_answer.to_s != answer
233
- tell("Please enter a valid integer.")
234
- elsif int_answer < 1 || int_answer > items.size
235
- tell("Please enter a number from 1 through #{items.size}.")
223
+ tell("-" * 80)
224
+ tell("Please select #{description}:", color: :cyan, bright: true)
225
+ items.each_with_index do |item, i|
226
+ tell("#{i + 1}: #{describer.call(item)}", color: :cyan, bright: true)
227
+ end
228
+ tell("-" * 80)
229
+ default_index = items.index(default)
230
+ default_selection = "[#{default_index + 1}] " if default_index
231
+ until choice
232
+ tell("Your choice #{default_selection}> ", newline: false)
233
+ answer = read_line.strip
234
+ if answer.empty?
235
+ choice = default
236
236
  else
237
- choice = items[int_answer - 1]
237
+ int_answer = answer.to_i
238
+ if int_answer.to_s != answer
239
+ tell("Please enter a valid integer.")
240
+ elsif int_answer < 1 || int_answer > items.size
241
+ tell("Please enter a number from 1 through #{items.size}.")
242
+ else
243
+ choice = items[int_answer - 1]
244
+ end
238
245
  end
239
246
  end
247
+ choice
240
248
  end
241
- choice
242
- end
243
249
 
244
- private
250
+ private
245
251
 
246
- def background?
247
- !!@foreground_thread && @foreground_thread != Thread.current
248
- end
252
+ def background?
253
+ !!@foreground_thread && @foreground_thread != Thread.current
254
+ end
249
255
 
250
- def wait_for_foreground
251
- sleep 0.1 while background?
252
- end
256
+ def wait_for_foreground
257
+ sleep(0.1) while background?
258
+ end
253
259
 
254
- def output_prefix
255
- @output_prefix || @output_prefix_proc && @output_prefix_proc.call
256
- end
260
+ def output_prefix
261
+ @output_prefix || @output_prefix_proc&.call
262
+ end
257
263
 
258
- def read_line
259
- wait_for_foreground if background?
260
- @line_pending[false] = false
261
- STDIN.gets.chomp
262
- end
264
+ def read_line
265
+ wait_for_foreground if background?
266
+ @line_pending[false] = false
267
+ $stdin.gets.chomp
268
+ end
263
269
 
264
- def tell_line(line, newline: true, prefix: true, **color_options)
265
- line_prefix, line_postfix = compute_color(**color_options)
266
- prefix = false if line_pending?
270
+ def tell_line(line, newline: true, prefix: true, **color_options)
271
+ line_prefix, line_postfix = compute_color(**color_options)
272
+ prefix = false if line_pending?
267
273
 
268
- out_line = "#{output_prefix if prefix}#{line_prefix}#{line}#{line_postfix}#{"\n" if newline}"
269
- write_raw(out_line)
274
+ out_line = "#{output_prefix if prefix}#{line_prefix}#{line}#{line_postfix}#{"\n" if newline}"
275
+ write_raw(out_line)
270
276
 
271
- line_pending!(!newline)
272
- end
277
+ line_pending!(!newline)
278
+ end
273
279
 
274
- def write_raw(bytes)
275
- if background?
276
- @background_data << bytes
277
- else
278
- STDOUT.write(bytes)
280
+ def write_raw(bytes)
281
+ if background?
282
+ @background_data << bytes
283
+ else
284
+ $stdout.write(bytes)
285
+ end
279
286
  end
280
- end
281
287
 
282
- def line_pending?
283
- @line_pending[background?]
284
- end
288
+ def line_pending?
289
+ @line_pending[background?]
290
+ end
285
291
 
286
- def line_pending!(value)
287
- @line_pending[background?] = value
288
- end
292
+ def line_pending!(value)
293
+ @line_pending[background?] = value
294
+ end
289
295
 
290
- def compute_color(**options)
291
- if tty?
292
- if prefix = text_color(**options)
293
- # TODO matching annihilators for options
296
+ def compute_color(**options)
297
+ if tty? && (prefix = text_color(**options))
298
+ # TODO: matching annihilators for options
294
299
  postfix = text_color(color: :none, bright: false)
295
300
  end
301
+ [prefix, postfix]
296
302
  end
297
- [prefix, postfix]
298
- end
299
303
 
300
- def control_sequence(*parameters, function)
301
- "\033[#{parameters.join(";")}#{function}"
302
- end
304
+ def control_sequence(*parameters, function)
305
+ "\033[#{parameters.join(';')}#{function}"
306
+ end
303
307
 
304
- # SGR: Select Graphic Rendition … far too long for a function name ;)
305
- def sgr_sequence(*parameters)
306
- control_sequence(*parameters, :m)
307
- end
308
+ # SGR: Select Graphic Rendition … far too long for a function name ;)
309
+ def sgr_sequence(*parameters)
310
+ control_sequence(*parameters, :m)
311
+ end
308
312
 
309
- def text_color(color: nil, bright: nil, faint: nil, italic: nil, underline: nil)
310
- return if color.nil? && bright.nil?
311
- sequence = []
312
- unless bright.nil? && faint.nil?
313
- sequence << (bright ? 1 : (faint ? 2 : 22))
314
- end
315
- unless italic.nil?
316
- sequence << (italic ? 3 : 23)
317
- end
318
- unless underline.nil?
319
- sequence << (underline ? 4 : 24)
320
- end
321
- case color
322
- when :red
323
- sequence << 31
324
- when :green
325
- sequence << 32
326
- when :yellow
327
- sequence << 33
328
- when :blue
329
- sequence << 34
330
- when :purple
331
- sequence << 35
332
- when :cyan
333
- sequence << 36
334
- when :white
335
- sequence << 37
336
- when :none
337
- sequence << 39
338
- end
339
- sgr_sequence(*sequence)
313
+ def text_color(color: nil, bright: nil, faint: nil, italic: nil, underline: nil)
314
+ return if color.nil? && bright.nil?
315
+
316
+ sequence = []
317
+ unless bright.nil? && faint.nil?
318
+ sequence <<
319
+ if bright
320
+ 1
321
+ else
322
+ faint ? 2 : 22
323
+ end
324
+ end
325
+ unless italic.nil?
326
+ sequence << (italic ? 3 : 23)
327
+ end
328
+ unless underline.nil?
329
+ sequence << (underline ? 4 : 24)
330
+ end
331
+ case color
332
+ when :red
333
+ sequence << 31
334
+ when :green
335
+ sequence << 32
336
+ when :yellow
337
+ sequence << 33
338
+ when :blue
339
+ sequence << 34
340
+ when :purple
341
+ sequence << 35
342
+ when :cyan
343
+ sequence << 36
344
+ when :white
345
+ sequence << 37
346
+ when :none
347
+ sequence << 39
348
+ end
349
+ sgr_sequence(*sequence)
350
+ end
340
351
  end
341
352
  end
342
353
 
343
- end
344
-
345
- require_relative 'user_io/global'
354
+ require_relative "user_io/global"
@@ -1 +1,3 @@
1
- require 'infopark/user_io'
1
+ # frozen_string_literal: true
2
+
3
+ require "infopark/user_io"