infopark-user_io 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"