/
annoy.rb
270 lines (238 loc) · 8.05 KB
/
annoy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#---
# TODO: Use Matrix to give a more accurate annoyance factor
# TODO: Add trivia questions
#+++
require 'timeout'
require 'sysinfo'
require 'highline'
# = Annoy
#
# Like your annoying friend that asks you questions all the time.
#
class Annoy
attr_accessor :factor
attr_accessor :flavor
attr_accessor :answer
attr_accessor :writer
attr_accessor :period
attr_accessor :system
@@operators = {
:low => %w(+ -),
:medium => %w(* -),
:high => %w(& * -),
:insane => %w(** << | & *)
}.freeze
@@strlen = {
:low => 2,
:medium => 3,
:high => 4,
:insane => 32
}.freeze
@@randsize = {
:low => 10,
:medium => 14,
:high => 50,
:insane => 1000
}.freeze
@@period = 60.freeze # max seconds to wait
@@flavors = [:numeric, :string].freeze
@@skip = false # skip questions
# Calling this method tells Annoy to not prompt for
# a response. All questions will return true.
def Annoy.enable_skip; @@skip = true; end
# Tells annoy to prompt for a response.
def Annoy.disable_skip; @@skip = false; end
# Returns true of Annoy is in skip mode
def Annoy.skip?; @@skip; end
# * +factor+ annoyance factor, one of :low (default), :medium, :high, :insane
# * +flavor+ annoyance flavor, one of :rand (default), :numeric, string
# * +writer+ an IO object to write to. Default: STDERR
# * +period+ the amount of time to wait in seconds. Default: 60
def initialize(opts={:factor=>:medium, :flavor=>:rand, :writer=>STDOUT, :period=>nil})
@factor = opts[:factor]
@flavor = Annoy.get_flavor(opts[:flavor])
@writer = opts[:writer]
@period = opts[:period] || @@period
unless Annoy.respond_to?("#{@flavor}_question")
raise "Hey, hey, hey. I don't know that flavor! (#{@flavor})"
end
end
# Generates and returns a question. The correct response is available
# as +@answer+.
def question
q, @answer =Annoy.question(@factor, @flavor)
q
end
# A wrapper for string_question and numberic_question
def Annoy.question(factor=:medium, flavor=:rand)
raise "Come on, you ruined the flavor!" unless flavor
Annoy.send("#{flavor}_question", factor)
end
# Generates a random string
def Annoy.string_question(factor=:medium)
# Strings don't need to be evaluated so the answer is the
# same as the question.
str = strand @@strlen[factor]
[str,str]
end
# * Generates a rudimentary numeric equation in the form: (Integer OPERATOR Integer).
# * Returns [equation, answer]
def Annoy.numeric_question(factor=:medium)
equation = answer = 0
while answer < 10
vals = [rand(@@randsize[factor])+1,
@@operators[factor][ rand(@@operators[factor].size) ],
rand(@@randsize[factor])+1 ]
equation = "(%d %s %d)" % vals
answer = eval(equation)
end
[equation, answer]
end
# Prints a question to +writer+ and waits for a response on STDIN.
# It checks whether STDIN is connected a tty so it doesn't block on gets
# when there's no human around to annoy. It will return <b>TRUE</b> when
# STDIN is NOT connected to a tty (when STDIN.tty? returns false).
# * +msg+ The message to print. Default: "Please confirm."
# Returns true when the answer is correct, otherwise false.
def Annoy.challenge?(msg="Please confirm.", factor=:medium, flavor=:rand, writer=STDOUT, period=nil)
return true unless STDIN.tty? # Humans only!
return true if Annoy.skip?
begin
success = Timeout::timeout(period || @@period) do
flavor = Annoy.get_flavor(flavor)
question, answer = Annoy.question(factor, flavor)
msg = "#{msg} To continue, #{Annoy.verb(flavor)} #{question}: "
#writer.print msg
#if ![:medium, :high, :insane].member?(factor) && flavor == :numeric
#writer.print "(#{answer}) "
#writer.flush
#end
#response = Annoy.get_response(writer)
trap("SIGINT") { raise Annoy::GiveUp }
highline = HighLine.new
response = highline.ask(msg) { |q|
q.echo = '*' # Don't display response
q.overwrite = true # Erase the question afterwards
q.whitespace = :strip # Remove whitespace from the response
q.answer_type = Integer if flavor == :numeric
}
ret = (response == answer)
writer.puts "Incorrect" unless ret
ret
end
rescue Annoy::GiveUp => ex
writer.puts $/, "Giving up!"
false
rescue Timeout::Error => ex
writer.puts $/, "Times up!"
false
end
end
# Runs a challenge with the message, "Are you sure?"
# See: Annoy.challenge?
def Annoy.are_you_sure?(factor=:medium, flavor=:rand, writer=STDOUT)
Annoy.challenge?("Are you sure?", factor, flavor, writer)
end
# Runs a challenge with the message, "Proceed?"
# See: Annoy.challenge?
def Annoy.proceed?(factor=:medium, flavor=:rand, writer=STDOUT)
Annoy.challenge?("Proceed?", factor, flavor, writer)
end
# See: Annoy.challenge?
# Uses the value of @flavor, @factor, and @writer
def challenge?(msg="Please confirm.")
Annoy.challenge?(msg, @factor, @flavor, @writer)
end
# See: Annoy.pose_question
# Uses the value of @writer
def pose_question(msg, regexp)
Annoy.pose_question(msg, regexp, @writer)
end
# Prints a question to writer and waits for a response on STDIN.
# It checks whether STDIN is connected a tty so it doesn't block on gets.
# when there's no human around to annoy. It will return <b>TRUE</b> when
# STDIN is NOT connected to a tty.
# * +msg+ The question to pose to the user
# * +regexp+ The regular expression to match the answer.
def Annoy.pose_question(msg, regexp, writer=STDOUT, period=nil)
return true unless STDIN.tty? # Only ask a question if there's a human
return true if Annoy.skip?
begin
success = Timeout::timeout(period || @@period) do
regexp &&= Regexp.new regexp
writer.print msg
writer.flush if writer.respond_to?(:flush)
response = Annoy.get_response
regexp.match(response)
end
rescue Timeout::Error => ex
writer.puts $/, "Times up!"
false
end
end
private
def Annoy.get_response(writer=STDOUT)
return true unless STDIN.tty? # Humans only
return true if Annoy.skip?
# TODO: Count the number of keystrokes to prevent copy/paste.
# We can probably use Highline.
# We likely need to be more specific but this will do for now.
#if ::SystemInfo.new.os == :unix
# begin
# response = []
# char = nil
# system("stty raw -echo") # Raw mode, no echo
# while char != "\r" || response.size > 5
# char = STDIN.getc.chr
# writer.print char
# writer.flush
# response << char
# end
# writer.print "\n\r"
# response = response.join('')
# rescue => ex
# ensure
# system("stty -raw echo") # Reset terminal mode
# end
#else
response = (STDIN.gets || "")
#end
response.chomp.gsub(/["']/, '')
end
# Returns a verb appropriate to the flavor.
# * :numeric => resolve
# * :string => type
def Annoy.verb(flavor)
case flavor
when :numeric then "resolve"
when :string then "type"
else
nil
end
end
#
# Generates a string of random alphanumeric characters.
# * +len+ is the length, an Integer. Default: 8
# * +safe+ in safe-mode, ambiguous characters are removed (default: true):
# i l o 1 0
def Annoy.strand( len=8, safe=true )
chars = ("a".."z").to_a + ("0".."9").to_a
chars.delete_if { |v| %w(i l o 1 0).member?(v) } if safe
str = ""
1.upto(len) { |i| str << chars[rand(chars.size-1)] }
str
end
# * +f+ a prospective flavor name
def Annoy.get_flavor(f)
f.to_sym == :rand ? flavor_rand : f.to_sym
end
# Return a random flavor
def Annoy.flavor_rand
@@flavors[rand(@@flavors.size)]
end
end
# = Annoy::GiveUp
#
# This is what happens when you don't answer Annoy's questions.
class Annoy::GiveUp < RuntimeError
end