tty2-prompt 0.23.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +23 -0
- data/README.md +52 -0
- data/lib/tty2/prompt/answers_collector.rb +78 -0
- data/lib/tty2/prompt/block_paginator.rb +59 -0
- data/lib/tty2/prompt/choice.rb +147 -0
- data/lib/tty2/prompt/choices.rb +129 -0
- data/lib/tty2/prompt/confirm_question.rb +158 -0
- data/lib/tty2/prompt/const.rb +17 -0
- data/lib/tty2/prompt/converter_dsl.rb +21 -0
- data/lib/tty2/prompt/converter_registry.rb +69 -0
- data/lib/tty2/prompt/converters.rb +182 -0
- data/lib/tty2/prompt/distance.rb +49 -0
- data/lib/tty2/prompt/enum_list.rb +433 -0
- data/lib/tty2/prompt/errors.rb +31 -0
- data/lib/tty2/prompt/evaluator.rb +29 -0
- data/lib/tty2/prompt/expander.rb +321 -0
- data/lib/tty2/prompt/keypress.rb +98 -0
- data/lib/tty2/prompt/list.rb +589 -0
- data/lib/tty2/prompt/mask_question.rb +96 -0
- data/lib/tty2/prompt/multi_list.rb +224 -0
- data/lib/tty2/prompt/multiline.rb +72 -0
- data/lib/tty2/prompt/paginator.rb +111 -0
- data/lib/tty2/prompt/question/checks.rb +105 -0
- data/lib/tty2/prompt/question/modifier.rb +96 -0
- data/lib/tty2/prompt/question/validation.rb +72 -0
- data/lib/tty2/prompt/question.rb +391 -0
- data/lib/tty2/prompt/result.rb +42 -0
- data/lib/tty2/prompt/selected_choices.rb +77 -0
- data/lib/tty2/prompt/slider.rb +286 -0
- data/lib/tty2/prompt/statement.rb +55 -0
- data/lib/tty2/prompt/suggestion.rb +113 -0
- data/lib/tty2/prompt/symbols.rb +89 -0
- data/lib/tty2/prompt/test.rb +36 -0
- data/lib/tty2/prompt/timer.rb +75 -0
- data/lib/tty2/prompt/utils.rb +42 -0
- data/lib/tty2/prompt/version.rb +7 -0
- data/lib/tty2/prompt.rb +589 -0
- data/lib/tty2-prompt.rb +1 -0
- metadata +148 -0
@@ -0,0 +1,286 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
# A class responsible for shell prompt interactions.
|
5
|
+
class Prompt
|
6
|
+
# A class responsible for gathering numeric input from range
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class Slider
|
10
|
+
HELP = "(Use %s arrow keys, press Enter to select)"
|
11
|
+
|
12
|
+
FORMAT = ":slider %s"
|
13
|
+
|
14
|
+
# Initailize a Slider
|
15
|
+
#
|
16
|
+
# @param [Prompt] prompt
|
17
|
+
# the prompt
|
18
|
+
# @param [Hash] options
|
19
|
+
# the options to configure this slider
|
20
|
+
# @option options [Integer] :min The minimum value
|
21
|
+
# @option options [Integer] :max The maximum value
|
22
|
+
# @option options [Integer] :step The step value
|
23
|
+
# @option options [String] :format The display format
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def initialize(prompt, **options)
|
27
|
+
@prompt = prompt
|
28
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
29
|
+
@choices = Choices.new
|
30
|
+
@min = options.fetch(:min, 0)
|
31
|
+
@max = options.fetch(:max, 10)
|
32
|
+
@step = options.fetch(:step, 1)
|
33
|
+
@default = options[:default]
|
34
|
+
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
35
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
36
|
+
@format = options.fetch(:format) { FORMAT }
|
37
|
+
@quiet = options.fetch(:quiet) { @prompt.quiet }
|
38
|
+
@help = options[:help]
|
39
|
+
@show_help = options.fetch(:show_help) { :start }
|
40
|
+
@symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
|
41
|
+
@first_render = true
|
42
|
+
@done = false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Change symbols used by this prompt
|
46
|
+
#
|
47
|
+
# @param [Hash] new_symbols
|
48
|
+
# the new symbols to use
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def symbols(new_symbols = (not_set = true))
|
52
|
+
return @symbols if not_set
|
53
|
+
|
54
|
+
@symbols.merge!(new_symbols)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Setup initial active position
|
58
|
+
#
|
59
|
+
# @return [Integer]
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
def initial
|
63
|
+
if @default.nil?
|
64
|
+
# no default - choose the middle option
|
65
|
+
choices.size / 2
|
66
|
+
elsif default_choice = choices.find_by(:name, @default)
|
67
|
+
# found a Choice by name - use it
|
68
|
+
choices.index(default_choice)
|
69
|
+
else
|
70
|
+
# default is the index number
|
71
|
+
@default - 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Default help text
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def default_help
|
79
|
+
arrows = @symbols[:arrow_left] + "/" + @symbols[:arrow_right]
|
80
|
+
sprintf(HELP, arrows)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set help text
|
84
|
+
#
|
85
|
+
# @param [String] text
|
86
|
+
#
|
87
|
+
# @api private
|
88
|
+
def help(text = (not_set = true))
|
89
|
+
return @help if !@help.nil? && not_set
|
90
|
+
|
91
|
+
@help = (@help.nil? && not_set) ? default_help : text
|
92
|
+
end
|
93
|
+
|
94
|
+
# Change when help is displayed
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
def show_help(value = (not_set = true))
|
98
|
+
return @show_ehlp if not_set
|
99
|
+
|
100
|
+
@show_help = value
|
101
|
+
end
|
102
|
+
|
103
|
+
# @api public
|
104
|
+
def default(value)
|
105
|
+
@default = value
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api public
|
109
|
+
def min(value)
|
110
|
+
@min = value
|
111
|
+
end
|
112
|
+
|
113
|
+
# @api public
|
114
|
+
def max(value)
|
115
|
+
@max = value
|
116
|
+
end
|
117
|
+
|
118
|
+
# @api public
|
119
|
+
def step(value)
|
120
|
+
@step = value
|
121
|
+
end
|
122
|
+
|
123
|
+
# Add a single choice
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def choice(*value, &block)
|
127
|
+
if block
|
128
|
+
@choices << (value << block)
|
129
|
+
else
|
130
|
+
@choices << value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Add multiple choices
|
135
|
+
#
|
136
|
+
# @param [Array[Object]] values
|
137
|
+
# the values to add as choices
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
def choices(values = (not_set = true))
|
141
|
+
if not_set
|
142
|
+
@choices
|
143
|
+
else
|
144
|
+
values.each { |val| @choices << val }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# @api public
|
149
|
+
def format(value)
|
150
|
+
@format = value
|
151
|
+
end
|
152
|
+
|
153
|
+
# Set quiet mode.
|
154
|
+
#
|
155
|
+
# @api public
|
156
|
+
def quiet(value)
|
157
|
+
@quiet = value
|
158
|
+
end
|
159
|
+
|
160
|
+
# Call the slider by passing question
|
161
|
+
#
|
162
|
+
# @param [String] question
|
163
|
+
# the question to ask
|
164
|
+
#
|
165
|
+
# @apu public
|
166
|
+
def call(question, possibilities = nil, &block)
|
167
|
+
@question = question
|
168
|
+
choices(possibilities) if possibilities
|
169
|
+
block.call(self) if block
|
170
|
+
# set up a Choices collection for min, max, step
|
171
|
+
# if no possibilities were supplied
|
172
|
+
choices((@min..@max).step(@step).to_a) if @choices.empty?
|
173
|
+
|
174
|
+
@active = initial
|
175
|
+
@prompt.subscribe(self) do
|
176
|
+
render
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def keyleft(*)
|
181
|
+
@active -= 1 if @active > 0
|
182
|
+
end
|
183
|
+
alias keydown keyleft
|
184
|
+
|
185
|
+
def keyright(*)
|
186
|
+
@active += 1 if (@active + 1) < choices.size
|
187
|
+
end
|
188
|
+
alias keyup keyright
|
189
|
+
|
190
|
+
def keyreturn(*)
|
191
|
+
@done = true
|
192
|
+
end
|
193
|
+
alias keyspace keyreturn
|
194
|
+
alias keyenter keyreturn
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
# Check if help is shown only on start
|
199
|
+
#
|
200
|
+
# @api private
|
201
|
+
def help_start?
|
202
|
+
@show_help =~ /start/i
|
203
|
+
end
|
204
|
+
|
205
|
+
# Check if help is always displayed
|
206
|
+
#
|
207
|
+
# @api private
|
208
|
+
def help_always?
|
209
|
+
@show_help =~ /always/i
|
210
|
+
end
|
211
|
+
|
212
|
+
# Render an interactive range slider.
|
213
|
+
#
|
214
|
+
# @api private
|
215
|
+
def render
|
216
|
+
@prompt.print(@prompt.hide)
|
217
|
+
until @done
|
218
|
+
question = render_question
|
219
|
+
@prompt.print(question)
|
220
|
+
@prompt.read_keypress
|
221
|
+
refresh(question.lines.count)
|
222
|
+
end
|
223
|
+
@prompt.print(render_question) unless @quiet
|
224
|
+
answer
|
225
|
+
ensure
|
226
|
+
@prompt.print(@prompt.show)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Clear screen
|
230
|
+
#
|
231
|
+
# @param [Integer] lines
|
232
|
+
# the lines to clear
|
233
|
+
#
|
234
|
+
# @api private
|
235
|
+
def refresh(lines)
|
236
|
+
@prompt.print(@prompt.clear_lines(lines))
|
237
|
+
end
|
238
|
+
|
239
|
+
# @return [Integer, String]
|
240
|
+
#
|
241
|
+
# @api private
|
242
|
+
def answer
|
243
|
+
choices[@active].value
|
244
|
+
end
|
245
|
+
|
246
|
+
# Render question with the slider
|
247
|
+
#
|
248
|
+
# @return [String]
|
249
|
+
#
|
250
|
+
# @api private
|
251
|
+
def render_question
|
252
|
+
header = ["#{@prefix}#{@question} "]
|
253
|
+
if @done
|
254
|
+
header << @prompt.decorate(choices[@active].to_s, @active_color)
|
255
|
+
header << "\n"
|
256
|
+
else
|
257
|
+
header << render_slider
|
258
|
+
end
|
259
|
+
if @first_render && (help_start? || help_always?) ||
|
260
|
+
(help_always? && !@done)
|
261
|
+
header << "\n" + @prompt.decorate(help, @help_color)
|
262
|
+
@first_render = false
|
263
|
+
end
|
264
|
+
header.join
|
265
|
+
end
|
266
|
+
|
267
|
+
# Render slider representation
|
268
|
+
#
|
269
|
+
# @return [String]
|
270
|
+
#
|
271
|
+
# @api private
|
272
|
+
def render_slider
|
273
|
+
slider = (@symbols[:line] * @active) +
|
274
|
+
@prompt.decorate(@symbols[:bullet], @active_color) +
|
275
|
+
(@symbols[:line] * (choices.size - @active - 1))
|
276
|
+
value = choices[@active].name
|
277
|
+
case @format
|
278
|
+
when Proc
|
279
|
+
@format.call(slider, value)
|
280
|
+
else
|
281
|
+
@format.gsub(":slider", slider) % [value]
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end # Slider
|
285
|
+
end # Prompt
|
286
|
+
end # TTY2
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
# A class responsible for shell prompt interactions.
|
5
|
+
class Prompt
|
6
|
+
# A class representing a statement output to prompt.
|
7
|
+
class Statement
|
8
|
+
# Flag to display newline
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
attr_reader :newline
|
12
|
+
|
13
|
+
# Color used to display statement
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
attr_reader :color
|
17
|
+
|
18
|
+
# Initialize a Statement
|
19
|
+
#
|
20
|
+
# @param [TTY2::Prompt] prompt
|
21
|
+
#
|
22
|
+
# @param [Hash] options
|
23
|
+
#
|
24
|
+
# @option options [Symbol] :newline
|
25
|
+
# force a newline break after the message
|
26
|
+
#
|
27
|
+
# @option options [Symbol] :color
|
28
|
+
# change the message display to color
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def initialize(prompt, newline: true, color: false)
|
32
|
+
@prompt = prompt
|
33
|
+
@newline = newline
|
34
|
+
@color = color
|
35
|
+
end
|
36
|
+
|
37
|
+
# Output the message to the prompt
|
38
|
+
#
|
39
|
+
# @param [String] message
|
40
|
+
# the message to be printed to stdout
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def call(message)
|
44
|
+
message = @prompt.decorate(message, *color) if color
|
45
|
+
|
46
|
+
if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message
|
47
|
+
@prompt.puts message
|
48
|
+
else
|
49
|
+
@prompt.print message
|
50
|
+
@prompt.flush
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end # Statement
|
54
|
+
end # Prompt
|
55
|
+
end # TTY2
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "distance"
|
4
|
+
|
5
|
+
module TTY2
|
6
|
+
# A class responsible for terminal prompt interactions.
|
7
|
+
class Prompt
|
8
|
+
# A class representing a suggestion out of possible choices
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Suggestion
|
12
|
+
DEFAULT_INDENT = 8
|
13
|
+
|
14
|
+
SINGLE_TEXT = "Did you mean this?"
|
15
|
+
|
16
|
+
PLURAL_TEXT = "Did you mean one of these?"
|
17
|
+
|
18
|
+
# Number of spaces
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
attr_reader :indent
|
22
|
+
|
23
|
+
# Text for a single suggestion
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
attr_reader :single_text
|
27
|
+
|
28
|
+
# Text for multiple suggestions
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
attr_reader :plural_text
|
32
|
+
|
33
|
+
# Initialize a Suggestion
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def initialize(**options)
|
37
|
+
@indent = options.fetch(:indent) { DEFAULT_INDENT }
|
38
|
+
@single_text = options.fetch(:single_text) { SINGLE_TEXT }
|
39
|
+
@plural_text = options.fetch(:plural_text) { PLURAL_TEXT }
|
40
|
+
@suggestions = []
|
41
|
+
@comparator = Distance.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# Suggest matches out of possibile strings
|
45
|
+
#
|
46
|
+
# @param [String] message
|
47
|
+
#
|
48
|
+
# @param [Array[String]] possibilities
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def suggest(message, possibilities)
|
52
|
+
distances = measure_distances(message, possibilities)
|
53
|
+
minimum_distance = distances.keys.min
|
54
|
+
max_distance = distances.keys.max
|
55
|
+
|
56
|
+
if minimum_distance < max_distance
|
57
|
+
@suggestions = distances[minimum_distance].sort
|
58
|
+
end
|
59
|
+
evaluate
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Measure distances between messag and possibilities
|
65
|
+
#
|
66
|
+
# @param [String] message
|
67
|
+
#
|
68
|
+
# @param [Array[String]] possibilities
|
69
|
+
#
|
70
|
+
# @return [Hash]
|
71
|
+
#
|
72
|
+
# @api private
|
73
|
+
def measure_distances(message, possibilities)
|
74
|
+
distances = Hash.new { |hash, key| hash[key] = [] }
|
75
|
+
|
76
|
+
possibilities.each do |possibility|
|
77
|
+
distances[@comparator.distance(message, possibility)] << possibility
|
78
|
+
end
|
79
|
+
distances
|
80
|
+
end
|
81
|
+
|
82
|
+
# Build up a suggestion string
|
83
|
+
#
|
84
|
+
# @param [Array[String]] suggestions
|
85
|
+
#
|
86
|
+
# @return [String]
|
87
|
+
#
|
88
|
+
# @api private
|
89
|
+
def evaluate
|
90
|
+
return @suggestions if @suggestions.empty?
|
91
|
+
|
92
|
+
if @suggestions.one?
|
93
|
+
build_single_suggestion
|
94
|
+
else
|
95
|
+
build_multiple_suggestions
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @api private
|
100
|
+
def build_single_suggestion
|
101
|
+
single_text + "\n" + (" " * indent) + @suggestions.first
|
102
|
+
end
|
103
|
+
|
104
|
+
# @api private
|
105
|
+
def build_multiple_suggestions
|
106
|
+
plural_text + "\n" +
|
107
|
+
@suggestions.map do |sugest|
|
108
|
+
" " * indent + sugest
|
109
|
+
end.join("\n")
|
110
|
+
end
|
111
|
+
end # Suggestion
|
112
|
+
end # Prompt
|
113
|
+
end # TTY2
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
class Prompt
|
5
|
+
# Cross platform common Unicode symbols.
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
module Symbols
|
9
|
+
KEYS = {
|
10
|
+
tick: "✓",
|
11
|
+
cross: "✘",
|
12
|
+
star: "★",
|
13
|
+
square: "◼",
|
14
|
+
square_empty: "◻",
|
15
|
+
dot: "•",
|
16
|
+
bullet: "●",
|
17
|
+
bullet_empty: "○",
|
18
|
+
marker: "‣",
|
19
|
+
line: "─",
|
20
|
+
pipe: "|",
|
21
|
+
ellipsis: "…",
|
22
|
+
radio_on: "⬢",
|
23
|
+
radio_off: "⬡",
|
24
|
+
checkbox_on: "☒",
|
25
|
+
checkbox_off: "☐",
|
26
|
+
circle: "◯",
|
27
|
+
circle_on: "ⓧ",
|
28
|
+
circle_off: "Ⓘ",
|
29
|
+
arrow_up: "↑",
|
30
|
+
arrow_down: "↓",
|
31
|
+
arrow_up_down: "↕",
|
32
|
+
arrow_left: "←",
|
33
|
+
arrow_right: "→",
|
34
|
+
arrow_left_right: "↔",
|
35
|
+
heart: "♥",
|
36
|
+
diamond: "♦",
|
37
|
+
club: "♣",
|
38
|
+
spade: "♠"
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
WIN_KEYS = {
|
42
|
+
tick: "√",
|
43
|
+
cross: "x",
|
44
|
+
star: "*",
|
45
|
+
square: "[█]",
|
46
|
+
square_empty: "[ ]",
|
47
|
+
dot: ".",
|
48
|
+
bullet: "O",
|
49
|
+
bullet_empty: "○",
|
50
|
+
marker: ">",
|
51
|
+
line: "-",
|
52
|
+
pipe: "|",
|
53
|
+
ellipsis: "...",
|
54
|
+
radio_on: "(*)",
|
55
|
+
radio_off: "( )",
|
56
|
+
checkbox_on: "[×]",
|
57
|
+
checkbox_off: "[ ]",
|
58
|
+
circle: "( )",
|
59
|
+
circle_on: "(x)",
|
60
|
+
circle_off: "( )",
|
61
|
+
arrow_up: "↑",
|
62
|
+
arrow_down: "↓",
|
63
|
+
arrow_up_down: "↕",
|
64
|
+
arrow_left: "←",
|
65
|
+
arrow_right: "→",
|
66
|
+
arrow_left_right: "↔",
|
67
|
+
heart: "♥",
|
68
|
+
diamond: "♦",
|
69
|
+
club: "♣",
|
70
|
+
spade: "♠"
|
71
|
+
}.freeze
|
72
|
+
|
73
|
+
def symbols
|
74
|
+
@symbols ||= windows? ? WIN_KEYS : KEYS
|
75
|
+
end
|
76
|
+
module_function :symbols
|
77
|
+
|
78
|
+
# Check if Windowz
|
79
|
+
#
|
80
|
+
# @return [Boolean]
|
81
|
+
#
|
82
|
+
# @api public
|
83
|
+
def windows?
|
84
|
+
::File::ALT_SEPARATOR == "\\"
|
85
|
+
end
|
86
|
+
module_function :windows?
|
87
|
+
end # Symbols
|
88
|
+
end # Prompt
|
89
|
+
end # TTY2
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
require_relative "../prompt"
|
6
|
+
|
7
|
+
module TTY2
|
8
|
+
# Used for initializing test cases
|
9
|
+
class Prompt
|
10
|
+
module StringIOExtensions
|
11
|
+
def wait_readable(*)
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def ioctl(*)
|
16
|
+
80
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Test < TTY2::Prompt
|
21
|
+
def initialize(**options)
|
22
|
+
@input = StringIO.new
|
23
|
+
@input.extend(StringIOExtensions)
|
24
|
+
@output = StringIO.new
|
25
|
+
|
26
|
+
options.merge!({
|
27
|
+
input: @input,
|
28
|
+
output: @output,
|
29
|
+
env: { "TTY2_TEST" => true },
|
30
|
+
enable_color: options.fetch(:enable_color, true)
|
31
|
+
})
|
32
|
+
super(**options)
|
33
|
+
end
|
34
|
+
end # Test
|
35
|
+
end # Prompt
|
36
|
+
end # TTY2
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
class Prompt
|
5
|
+
class Timer
|
6
|
+
attr_reader :duration
|
7
|
+
|
8
|
+
attr_reader :total
|
9
|
+
|
10
|
+
attr_reader :interval
|
11
|
+
|
12
|
+
def initialize(duration, interval)
|
13
|
+
@duration = duration
|
14
|
+
@interval = interval
|
15
|
+
@total = 0.0
|
16
|
+
@current = nil
|
17
|
+
@events = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
return if @current
|
22
|
+
|
23
|
+
@current = time_now
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
return unless @current
|
28
|
+
|
29
|
+
@current = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def runtime
|
33
|
+
time_now - @current
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_tick(&block)
|
37
|
+
@events << block
|
38
|
+
end
|
39
|
+
|
40
|
+
def while_remaining
|
41
|
+
start
|
42
|
+
remaining = duration
|
43
|
+
|
44
|
+
if @duration
|
45
|
+
while remaining >= 0.0
|
46
|
+
if runtime >= total
|
47
|
+
tick = duration - @total
|
48
|
+
@events.each { |block| block.(tick) }
|
49
|
+
@total += @interval
|
50
|
+
end
|
51
|
+
|
52
|
+
yield(remaining)
|
53
|
+
remaining = duration - runtime
|
54
|
+
end
|
55
|
+
else
|
56
|
+
loop { yield }
|
57
|
+
end
|
58
|
+
ensure
|
59
|
+
stop
|
60
|
+
end
|
61
|
+
|
62
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
63
|
+
# Object representing current time
|
64
|
+
def time_now
|
65
|
+
::Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
# Object represeting current time
|
69
|
+
def time_now
|
70
|
+
::Time.now
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end # Timer
|
74
|
+
end # Prompt
|
75
|
+
end # TTY2
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
module Utils
|
5
|
+
module_function
|
6
|
+
|
7
|
+
BLANK_REGEX = /\A[[:space:]]*\z/o.freeze
|
8
|
+
|
9
|
+
# Extract options hash from array argument
|
10
|
+
#
|
11
|
+
# @param [Array[Object]] args
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def extract_options(args)
|
15
|
+
options = args.last
|
16
|
+
options.respond_to?(:to_hash) ? options.to_hash.dup : {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract_options!(args)
|
20
|
+
args.last.respond_to?(:to_hash) ? args.pop : {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if value is nil or an empty string
|
24
|
+
#
|
25
|
+
# @param [Object] value
|
26
|
+
# the value to check
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def blank?(value)
|
32
|
+
value.nil? || BLANK_REGEX === value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Deep copy object
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def deep_copy(object)
|
39
|
+
Marshal.load(Marshal.dump(object))
|
40
|
+
end
|
41
|
+
end # Utils
|
42
|
+
end # TTY2
|