interact 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/interact.rb +3 -448
- data/lib/interact/interact.rb +451 -0
- data/lib/interact/interactive.rb +82 -0
- data/lib/version.rb +1 -1
- metadata +9 -5
data/lib/interact.rb
CHANGED
@@ -1,450 +1,5 @@
|
|
1
1
|
# Copyright (c) 2011 Alex Suraci
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
module Interact
|
7
|
-
WINDOWS = !!(RUBY_PLATFORM =~ /mingw|mswin32|cygwin/)
|
8
|
-
|
9
|
-
if defined? callcc
|
10
|
-
HAS_CALLCC = true
|
11
|
-
else
|
12
|
-
begin
|
13
|
-
require "continuation"
|
14
|
-
HAS_CALLCC = true
|
15
|
-
rescue LoadError
|
16
|
-
HAS_CALLCC = false
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
ESCAPES = {
|
21
|
-
"[A" => :up, "H" => :up,
|
22
|
-
"[B" => :down, "P" => :down,
|
23
|
-
"[C" => :right, "M" => :right,
|
24
|
-
"[D" => :left, "K" => :left,
|
25
|
-
"[3~" => :delete, "S" => :delete,
|
26
|
-
"[H" => :home, "G" => :home,
|
27
|
-
"[F" => :end, "O" => :end
|
28
|
-
}
|
29
|
-
|
30
|
-
EVENTS = {
|
31
|
-
"\b" => :backspace,
|
32
|
-
"\t" => :tab,
|
33
|
-
"\x01" => :home,
|
34
|
-
"\x03" => :interrupt,
|
35
|
-
"\x04" => :eof,
|
36
|
-
"\x05" => :end,
|
37
|
-
"\x17" => :kill_word,
|
38
|
-
"\x7f" => :backspace
|
39
|
-
}
|
40
|
-
|
41
|
-
# Used internally to clean up input state before jumping to another prompt.
|
42
|
-
class JumpToPrompt < Exception
|
43
|
-
def initialize(prompt)
|
44
|
-
@prompt = prompt
|
45
|
-
end
|
46
|
-
|
47
|
-
# Print an empty line and jump to the prompt. This is typically called
|
48
|
-
# after the user has pressed the up arrow.
|
49
|
-
def jump
|
50
|
-
print "\n"
|
51
|
-
@prompt[0].call(@prompt)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.handler(which, ans, pos, echo = nil, prompts = [])
|
56
|
-
if block_given?
|
57
|
-
res = yield which, ans, pos, echo
|
58
|
-
return res unless res.nil?
|
59
|
-
end
|
60
|
-
|
61
|
-
case which
|
62
|
-
when :up
|
63
|
-
if back = prompts.pop
|
64
|
-
raise Interact::JumpToPrompt, back
|
65
|
-
end
|
66
|
-
|
67
|
-
when :down
|
68
|
-
# nothing
|
69
|
-
|
70
|
-
when :tab
|
71
|
-
# nothing
|
72
|
-
|
73
|
-
when :right
|
74
|
-
unless pos == ans.size
|
75
|
-
print censor(ans[pos .. pos], echo)
|
76
|
-
return pos + 1
|
77
|
-
end
|
78
|
-
|
79
|
-
when :left
|
80
|
-
unless pos == 0
|
81
|
-
print "\b"
|
82
|
-
return pos - 1
|
83
|
-
end
|
84
|
-
|
85
|
-
when :delete
|
86
|
-
unless pos == ans.size
|
87
|
-
ans.slice!(pos, 1)
|
88
|
-
if Interact::WINDOWS
|
89
|
-
rest = ans[pos .. -1]
|
90
|
-
print(censor(rest, echo) + " \b" + ("\b" * rest.size))
|
91
|
-
else
|
92
|
-
print("\e[P")
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
when :home
|
97
|
-
print("\b" * pos)
|
98
|
-
return 0
|
99
|
-
|
100
|
-
when :end
|
101
|
-
print(censor(ans[pos .. -1], echo))
|
102
|
-
return ans.size
|
103
|
-
|
104
|
-
when :backspace
|
105
|
-
if pos > 0
|
106
|
-
ans.slice!(pos - 1, 1)
|
107
|
-
|
108
|
-
if Interact::WINDOWS
|
109
|
-
rest = ans[pos - 1 .. -1]
|
110
|
-
print("\b" + censor(rest, echo) + " \b" + ("\b" * rest.size))
|
111
|
-
else
|
112
|
-
print("\b\e[P")
|
113
|
-
end
|
114
|
-
|
115
|
-
return pos - 1
|
116
|
-
end
|
117
|
-
|
118
|
-
when :interrupt
|
119
|
-
raise Interrupt.new
|
120
|
-
|
121
|
-
when :eof
|
122
|
-
return false if ans.empty?
|
123
|
-
|
124
|
-
when :kill_word
|
125
|
-
if pos > 0
|
126
|
-
start = /[^\s]*\s*$/ =~ ans[0 .. pos]
|
127
|
-
length = pos - start
|
128
|
-
ans.slice!(start, length)
|
129
|
-
print("\b" * length + " " * length + "\b" * length)
|
130
|
-
return start
|
131
|
-
end
|
132
|
-
|
133
|
-
when Array
|
134
|
-
case which[0]
|
135
|
-
when :key
|
136
|
-
c = which[1]
|
137
|
-
rest = ans[pos .. -1]
|
138
|
-
|
139
|
-
ans.insert(pos, c)
|
140
|
-
|
141
|
-
print(censor(c + rest, echo) + ("\b" * rest.size))
|
142
|
-
|
143
|
-
return pos + 1
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
pos
|
148
|
-
end
|
149
|
-
|
150
|
-
def self.censor(str, with)
|
151
|
-
return str unless with
|
152
|
-
with * str.size
|
153
|
-
end
|
154
|
-
|
155
|
-
def self.ask_default(input, question, default = nil,
|
156
|
-
echo = nil, prompts = [], &callback)
|
157
|
-
while true
|
158
|
-
prompt(question, default)
|
159
|
-
|
160
|
-
ans = ""
|
161
|
-
pos = 0
|
162
|
-
escaped = false
|
163
|
-
escape_seq = ""
|
164
|
-
|
165
|
-
with_char_io(input) do
|
166
|
-
until pos == false or (c = get_character(input)) =~ /[\r\n]/
|
167
|
-
if c == "\e" || c == "\xE0"
|
168
|
-
escaped = true
|
169
|
-
elsif escaped
|
170
|
-
escape_seq << c
|
171
|
-
|
172
|
-
if cmd = Interact::ESCAPES[escape_seq]
|
173
|
-
pos = handler(cmd, ans, pos, echo, prompts, &callback)
|
174
|
-
escaped, escape_seq = false, ""
|
175
|
-
elsif Interact::ESCAPES.select { |k, v|
|
176
|
-
k.start_with? escape_seq
|
177
|
-
}.empty?
|
178
|
-
escaped, escape_seq = false, ""
|
179
|
-
end
|
180
|
-
elsif Interact::EVENTS.key? c
|
181
|
-
pos = handler(
|
182
|
-
Interact::EVENTS[c], ans, pos, echo, prompts, &callback
|
183
|
-
)
|
184
|
-
elsif c < " "
|
185
|
-
# ignore
|
186
|
-
else
|
187
|
-
pos = handler([:key, c], ans, pos, echo, prompts, &callback)
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
print "\n"
|
193
|
-
|
194
|
-
if ans.empty?
|
195
|
-
return default unless default.nil?
|
196
|
-
else
|
197
|
-
return match_type(ans, default)
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
def self.ask_choices(input, question, default, choices, indexed = false,
|
203
|
-
echo = nil, prompts = [], &callback)
|
204
|
-
choices = choices.to_a
|
205
|
-
|
206
|
-
msg = question.dup
|
207
|
-
|
208
|
-
if indexed
|
209
|
-
choices.each.with_index do |o, i|
|
210
|
-
puts "#{i + 1}: #{o}"
|
211
|
-
end
|
212
|
-
else
|
213
|
-
msg << " (#{choices.collect(&:inspect).join ", "})"
|
214
|
-
end
|
215
|
-
|
216
|
-
while true
|
217
|
-
ans = ask_default(input, msg, default, echo, prompts, &callback)
|
218
|
-
|
219
|
-
matches = choices.select { |x| x.start_with? ans }
|
220
|
-
|
221
|
-
if matches.size == 1
|
222
|
-
return matches.first
|
223
|
-
elsif indexed and ans =~ /^\s*\d+\s*$/ and res = choices[ans.to_i - 1]
|
224
|
-
return res
|
225
|
-
elsif matches.size > 1
|
226
|
-
puts "Please disambiguate: #{matches.join " or "}?"
|
227
|
-
else
|
228
|
-
puts "Unknown answer, please try again!"
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
def self.prompt(question, default = nil)
|
234
|
-
msg = question.dup
|
235
|
-
|
236
|
-
case default
|
237
|
-
when true
|
238
|
-
msg << " [Yn]"
|
239
|
-
when false
|
240
|
-
msg << " [yN]"
|
241
|
-
else
|
242
|
-
msg << " [#{default.inspect}]" if default
|
243
|
-
end
|
244
|
-
|
245
|
-
print "#{msg}: "
|
246
|
-
end
|
247
|
-
|
248
|
-
def self.match_type(str, x)
|
249
|
-
case x
|
250
|
-
when Integer
|
251
|
-
str.to_i
|
252
|
-
when true, false
|
253
|
-
str.upcase.start_with? "Y"
|
254
|
-
else
|
255
|
-
str
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
# Definitions for reading character-by-character with no echoing.
|
260
|
-
begin
|
261
|
-
require "Win32API"
|
262
|
-
|
263
|
-
def self.with_char_io(input)
|
264
|
-
yield
|
265
|
-
rescue Interact::JumpToPrompt => e
|
266
|
-
e.jump
|
267
|
-
end
|
268
|
-
|
269
|
-
def self.get_character(input)
|
270
|
-
if input == STDIN
|
271
|
-
begin
|
272
|
-
Win32API.new("msvcrt", "_getch", [], "L").call.chr
|
273
|
-
rescue
|
274
|
-
Win32API.new("crtdll", "_getch", [], "L").call.chr
|
275
|
-
end
|
276
|
-
else
|
277
|
-
input.getc.chr
|
278
|
-
end
|
279
|
-
end
|
280
|
-
rescue LoadError
|
281
|
-
begin
|
282
|
-
require "termios"
|
283
|
-
|
284
|
-
def self.with_char_io(input)
|
285
|
-
return yield unless input.tty?
|
286
|
-
|
287
|
-
before = Termios.getattr(input)
|
288
|
-
|
289
|
-
new = before.dup
|
290
|
-
new.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
|
291
|
-
new.c_cc[Termios::VMIN] = 1
|
292
|
-
|
293
|
-
begin
|
294
|
-
Termios.setattr(input, Termios::TCSANOW, new)
|
295
|
-
yield
|
296
|
-
rescue Interact::JumpToPrompt => e
|
297
|
-
Termios.setattr(input, Termios::TCSANOW, before)
|
298
|
-
e.jump
|
299
|
-
ensure
|
300
|
-
Termios.setattr(input, Termios::TCSANOW, before)
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
def self.get_character(input)
|
305
|
-
input.getc.chr
|
306
|
-
end
|
307
|
-
rescue LoadError
|
308
|
-
def self.with_char_io(input)
|
309
|
-
return yield unless input.tty?
|
310
|
-
|
311
|
-
begin
|
312
|
-
before = `stty -g`
|
313
|
-
system("stty raw -echo -icanon isig")
|
314
|
-
yield
|
315
|
-
rescue Interact::JumpToPrompt => e
|
316
|
-
system("stty #{before}")
|
317
|
-
e.jump
|
318
|
-
ensure
|
319
|
-
system("stty #{before}")
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
|
-
def self.get_character(input)
|
324
|
-
input.getc.chr
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
|
-
|
330
|
-
module Interactive
|
331
|
-
# Allow classes to enable/disable the rewind feature via +disable_rewind+
|
332
|
-
# and +enable_rewind+.
|
333
|
-
def self.included klass
|
334
|
-
class << klass
|
335
|
-
def disable_rewind
|
336
|
-
def self.rewind_enabled?
|
337
|
-
false
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
def enable_rewind
|
342
|
-
def self.rewind_enabled?
|
343
|
-
true
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
def rewind_enabled?
|
348
|
-
true
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
klass.class_eval do
|
353
|
-
def rewind_enabled?
|
354
|
-
self.class.rewind_enabled?
|
355
|
-
end
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
# General-purpose interaction.
|
360
|
-
#
|
361
|
-
# [question] The prompt, without ": " at the end.
|
362
|
-
#
|
363
|
-
# [options] An optional hash containing the following options.
|
364
|
-
#
|
365
|
-
# input::
|
366
|
-
# The input source (defaults to +STDIN+).
|
367
|
-
#
|
368
|
-
# default::
|
369
|
-
# The default value, also used to attempt type conversion of the answer
|
370
|
-
# (e.g. numeric/boolean).
|
371
|
-
#
|
372
|
-
# choices::
|
373
|
-
# An array (or +Enumerable+) of strings to choose from.
|
374
|
-
#
|
375
|
-
# indexed::
|
376
|
-
# Whether to allow choosing from +:choices+ by their index, best for when
|
377
|
-
# there are many choices.
|
378
|
-
#
|
379
|
-
# echo::
|
380
|
-
# A string to echo when showing the input; used for things like censoring
|
381
|
-
# password input.
|
382
|
-
#
|
383
|
-
# forget::
|
384
|
-
# Set to false to prevent rewinding from remembering the answer.
|
385
|
-
#
|
386
|
-
# callback::
|
387
|
-
# A block used to override certain actions.
|
388
|
-
#
|
389
|
-
# The block should take 4 arguments:
|
390
|
-
#
|
391
|
-
# - the event, e.g. +:up+ or +[:key, X]+ where +X+ is a string containing
|
392
|
-
# a single character
|
393
|
-
# - the current answer to the question; you'll probably mutate this
|
394
|
-
# - the current offset from the start of the answer string, e.g. when
|
395
|
-
# typing in the middle of the input, this will be where you insert
|
396
|
-
# characters
|
397
|
-
# - the +:echo+ option from above, may be +nil+
|
398
|
-
#
|
399
|
-
# The block should return the updated +position+, or +nil+ if it didn't
|
400
|
-
# handle the event
|
401
|
-
def ask(question, options = {})
|
402
|
-
rewind = Interact::HAS_CALLCC && rewind_enabled?
|
403
|
-
|
404
|
-
if rewind
|
405
|
-
prompt, answer = callcc { |cc| [cc, nil] }
|
406
|
-
else
|
407
|
-
prompt, answer = nil, nil
|
408
|
-
end
|
409
|
-
|
410
|
-
if answer.nil?
|
411
|
-
default = options[:default]
|
412
|
-
else
|
413
|
-
default = answer
|
414
|
-
end
|
415
|
-
|
416
|
-
choices = options[:choices]
|
417
|
-
indexed = options[:indexed]
|
418
|
-
callback = options[:callback]
|
419
|
-
input = options[:input] || STDIN
|
420
|
-
echo = options[:echo]
|
421
|
-
|
422
|
-
prompts = (@__prompts ||= [])
|
423
|
-
|
424
|
-
if choices
|
425
|
-
ans = Interact.ask_choices(
|
426
|
-
input, question, default, choices, indexed, echo, prompts, &callback
|
427
|
-
)
|
428
|
-
else
|
429
|
-
ans = Interact.ask_default(
|
430
|
-
input, question, default, echo, prompts, &callback
|
431
|
-
)
|
432
|
-
end
|
433
|
-
|
434
|
-
if rewind
|
435
|
-
prompts << [prompt, options[:forget] ? nil : ans]
|
436
|
-
end
|
437
|
-
|
438
|
-
ans
|
439
|
-
end
|
440
|
-
|
441
|
-
# Clear prompts.
|
442
|
-
#
|
443
|
-
# Questions asked after this are rewindable, but questions asked beforehand
|
444
|
-
# are no longer reachable.
|
445
|
-
#
|
446
|
-
# Use this after you've performed some mutation based on the user's input.
|
447
|
-
def finalize
|
448
|
-
@__prompts = []
|
449
|
-
end
|
450
|
-
end
|
3
|
+
here = File.expand_path("../", __FILE__)
|
4
|
+
require "#{here}/interact/interact"
|
5
|
+
require "#{here}/interact/interactive"
|
@@ -0,0 +1,451 @@
|
|
1
|
+
# Copyright (c) 2011 Alex Suraci
|
2
|
+
|
3
|
+
module Interact
|
4
|
+
WINDOWS = !!(RUBY_PLATFORM =~ /mingw|mswin32|cygwin/) #:nodoc:
|
5
|
+
|
6
|
+
if defined? callcc
|
7
|
+
HAS_CALLCC = true #:nodoc:
|
8
|
+
else
|
9
|
+
begin
|
10
|
+
require "continuation"
|
11
|
+
HAS_CALLCC = true #:nodoc:
|
12
|
+
rescue LoadError
|
13
|
+
HAS_CALLCC = false #:nodoc:
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
EVENTS = {
|
18
|
+
"\b" => :backspace,
|
19
|
+
"\t" => :tab,
|
20
|
+
"\x01" => :home,
|
21
|
+
"\x03" => :interrupt,
|
22
|
+
"\x04" => :eof,
|
23
|
+
"\x05" => :end,
|
24
|
+
"\x17" => :kill_word,
|
25
|
+
"\x7f" => :backspace,
|
26
|
+
"\r" => :enter,
|
27
|
+
"\n" => :enter
|
28
|
+
}
|
29
|
+
|
30
|
+
ESCAPES = {
|
31
|
+
"[A" => :up, "H" => :up,
|
32
|
+
"[B" => :down, "P" => :down,
|
33
|
+
"[C" => :right, "M" => :right,
|
34
|
+
"[D" => :left, "K" => :left,
|
35
|
+
"[3~" => :delete, "S" => :delete,
|
36
|
+
"[H" => :home, "G" => :home,
|
37
|
+
"[F" => :end, "O" => :end
|
38
|
+
}
|
39
|
+
|
40
|
+
class JumpToPrompt < Exception #:nodoc:
|
41
|
+
def initialize(prompt)
|
42
|
+
@prompt = prompt
|
43
|
+
end
|
44
|
+
|
45
|
+
def jump
|
46
|
+
print "\n"
|
47
|
+
@prompt[0].call(@prompt)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Wrap around the input options, the current answer, and the current
|
52
|
+
# position.
|
53
|
+
#
|
54
|
+
# Passed to handlers, which are expected to mutate +answer+ and +position+
|
55
|
+
# as they handle incoming events.
|
56
|
+
class InputState
|
57
|
+
attr_accessor :options, :answer, :position
|
58
|
+
|
59
|
+
def initialize(options = {}, answer = "", position = 0)
|
60
|
+
@options = options
|
61
|
+
@answer = answer
|
62
|
+
@position = position
|
63
|
+
@done = false
|
64
|
+
end
|
65
|
+
|
66
|
+
# Call to signal to the input reader that it can stop.
|
67
|
+
def done!
|
68
|
+
@done = true
|
69
|
+
end
|
70
|
+
|
71
|
+
# Is the input finished/complete?
|
72
|
+
def done?
|
73
|
+
@done
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class << self
|
78
|
+
# Read a single character.
|
79
|
+
#
|
80
|
+
# [options] An optional hash containing the following options.
|
81
|
+
#
|
82
|
+
# input::
|
83
|
+
# The input source (defaults to <code>$stdin</code>).
|
84
|
+
def read_char(options = {})
|
85
|
+
input = options[:input] || $stdin
|
86
|
+
|
87
|
+
with_char_io(input) do
|
88
|
+
get_character(input)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Read a single event.
|
93
|
+
#
|
94
|
+
# [options] An optional hash containing the following options.
|
95
|
+
#
|
96
|
+
# input::
|
97
|
+
# The input source (defaults to <code>$stdin</code>).
|
98
|
+
#
|
99
|
+
# callback::
|
100
|
+
# Called with the event.
|
101
|
+
def read_event(options = {}, &callback)
|
102
|
+
input = options[:input] || $stdin
|
103
|
+
callback ||= options[:callback]
|
104
|
+
|
105
|
+
with_char_io(input) do
|
106
|
+
e = get_event(input)
|
107
|
+
|
108
|
+
if callback
|
109
|
+
callback.call(e)
|
110
|
+
else
|
111
|
+
e
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Read a line of input.
|
117
|
+
#
|
118
|
+
# [options] An optional hash containing the following options.
|
119
|
+
#
|
120
|
+
# input::
|
121
|
+
# The input source (defaults to <code>$stdin</code>).
|
122
|
+
#
|
123
|
+
# echo::
|
124
|
+
# A string to echo when showing the input; used for things like hiding
|
125
|
+
# password input.
|
126
|
+
#
|
127
|
+
# callback::
|
128
|
+
# A block used to override certain actions.
|
129
|
+
#
|
130
|
+
# The block should take two arguments:
|
131
|
+
#
|
132
|
+
# - the event, e.g. <code>:up</code> or <code>[:key, X]</code> where
|
133
|
+
# +X+ is a string containing a single character
|
134
|
+
# - the +InputState+
|
135
|
+
#
|
136
|
+
# The block should mutate the given state, and return +true+ if it
|
137
|
+
# handled the event or +false+ if it didn't.
|
138
|
+
def read_line(options = {}, &callback)
|
139
|
+
input = options[:input] || $stdin
|
140
|
+
callback ||= options[:callback]
|
141
|
+
|
142
|
+
state = InputState.new(options)
|
143
|
+
with_char_io(input) do
|
144
|
+
until state.done?
|
145
|
+
handler(get_event(input), state, &callback)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
state.answer
|
150
|
+
end
|
151
|
+
|
152
|
+
# Ask a question and get an answer.
|
153
|
+
#
|
154
|
+
# See Interact#read_line for the other possible values in +options+.
|
155
|
+
#
|
156
|
+
# [question] The prompt, without ": " at the end.
|
157
|
+
#
|
158
|
+
# [options] An optional hash containing the following options.
|
159
|
+
#
|
160
|
+
# default::
|
161
|
+
# The default value, also used to attempt type conversion of the answer
|
162
|
+
# (e.g. numeric/boolean).
|
163
|
+
#
|
164
|
+
# choices::
|
165
|
+
# An array (or +Enumerable+) of strings to choose from.
|
166
|
+
#
|
167
|
+
# indexed::
|
168
|
+
# Use alternative choice listing, and allow choosing by number. Good
|
169
|
+
# for when there are many choices or choices with long names.
|
170
|
+
def ask(question, options = {}, &callback)
|
171
|
+
default = options[:default]
|
172
|
+
choices = options[:choices] && options[:choices].to_a
|
173
|
+
indexed = options[:indexed]
|
174
|
+
callback ||= options[:callback]
|
175
|
+
|
176
|
+
if indexed
|
177
|
+
choices.each_with_index do |o, i|
|
178
|
+
puts "#{i + 1}: #{o}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
while true
|
183
|
+
print prompt(question, default, !indexed && choices)
|
184
|
+
|
185
|
+
ans = read_line(options, &callback)
|
186
|
+
|
187
|
+
print "\n"
|
188
|
+
|
189
|
+
if ans.empty?
|
190
|
+
return default unless default.nil?
|
191
|
+
elsif choices
|
192
|
+
matches = choices.select { |x| x.start_with? ans }
|
193
|
+
|
194
|
+
if matches.size == 1
|
195
|
+
return matches.first
|
196
|
+
elsif indexed and ans =~ /^\s*\d+\s*$/ and \
|
197
|
+
res = choices[ans.to_i - 1]
|
198
|
+
return res
|
199
|
+
elsif matches.size > 1
|
200
|
+
puts "Please disambiguate: #{matches.join " or "}?"
|
201
|
+
else
|
202
|
+
puts "Unknown answer, please try again!"
|
203
|
+
end
|
204
|
+
else
|
205
|
+
return match_type(ans, default)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def get_event(input)
|
213
|
+
escaped = false
|
214
|
+
escape_seq = ""
|
215
|
+
|
216
|
+
while c = get_character(input)
|
217
|
+
if c == "\e" || c == "\xE0"
|
218
|
+
escaped = true
|
219
|
+
elsif escaped
|
220
|
+
escape_seq << c
|
221
|
+
|
222
|
+
if cmd = Interact::ESCAPES[escape_seq]
|
223
|
+
return cmd
|
224
|
+
elsif Interact::ESCAPES.select { |k, v|
|
225
|
+
k.start_with? escape_seq
|
226
|
+
}.empty?
|
227
|
+
escaped, escape_seq = false, ""
|
228
|
+
end
|
229
|
+
elsif Interact::EVENTS.key? c
|
230
|
+
return Interact::EVENTS[c]
|
231
|
+
elsif c < " "
|
232
|
+
# ignore
|
233
|
+
else
|
234
|
+
return [:key, c]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def handler(which, state)
|
240
|
+
if block_given?
|
241
|
+
res = yield which, state
|
242
|
+
return if res
|
243
|
+
end
|
244
|
+
|
245
|
+
echo = state.options[:echo]
|
246
|
+
prompts = state.options[:prompts] || []
|
247
|
+
|
248
|
+
ans = state.answer
|
249
|
+
pos = state.position
|
250
|
+
|
251
|
+
case which
|
252
|
+
when :up
|
253
|
+
if back = prompts.pop
|
254
|
+
raise Interact::JumpToPrompt, back
|
255
|
+
end
|
256
|
+
|
257
|
+
when :down
|
258
|
+
# nothing
|
259
|
+
|
260
|
+
when :tab
|
261
|
+
# nothing
|
262
|
+
|
263
|
+
when :right
|
264
|
+
unless pos == ans.size
|
265
|
+
print censor(ans[pos .. pos], echo)
|
266
|
+
state.position += 1
|
267
|
+
end
|
268
|
+
|
269
|
+
when :left
|
270
|
+
unless position == 0
|
271
|
+
print "\b"
|
272
|
+
state.position -= 1
|
273
|
+
end
|
274
|
+
|
275
|
+
when :delete
|
276
|
+
unless pos == ans.size
|
277
|
+
ans.slice!(pos, 1)
|
278
|
+
if Interact::WINDOWS
|
279
|
+
rest = ans[pos .. -1]
|
280
|
+
print(censor(rest, echo) + " \b" + ("\b" * rest.size))
|
281
|
+
else
|
282
|
+
print("\e[P")
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
when :home
|
287
|
+
print("\b" * pos)
|
288
|
+
state.position = 0
|
289
|
+
|
290
|
+
when :end
|
291
|
+
print(censor(ans[pos .. -1], echo))
|
292
|
+
state.position = ans.size
|
293
|
+
|
294
|
+
when :backspace
|
295
|
+
if pos > 0
|
296
|
+
ans.slice!(pos - 1, 1)
|
297
|
+
|
298
|
+
if Interact::WINDOWS
|
299
|
+
rest = ans[pos - 1 .. -1]
|
300
|
+
print("\b" + censor(rest, echo) + " \b" + ("\b" * rest.size))
|
301
|
+
else
|
302
|
+
print("\b\e[P")
|
303
|
+
end
|
304
|
+
|
305
|
+
state.position -= 1
|
306
|
+
end
|
307
|
+
|
308
|
+
when :interrupt
|
309
|
+
raise Interrupt.new
|
310
|
+
|
311
|
+
when :eof
|
312
|
+
state.done! if ans.empty?
|
313
|
+
|
314
|
+
when :kill_word
|
315
|
+
if pos > 0
|
316
|
+
start = /[^\s]*\s*$/ =~ ans[0 .. pos]
|
317
|
+
length = pos - start
|
318
|
+
ans.slice!(start, length)
|
319
|
+
print("\b" * length + " " * length + "\b" * length)
|
320
|
+
state.position = start
|
321
|
+
end
|
322
|
+
|
323
|
+
when :enter
|
324
|
+
state.done!
|
325
|
+
|
326
|
+
when Array
|
327
|
+
case which[0]
|
328
|
+
when :key
|
329
|
+
c = which[1]
|
330
|
+
rest = ans[pos .. -1]
|
331
|
+
|
332
|
+
ans.insert(pos, c)
|
333
|
+
|
334
|
+
print(censor(c + rest, echo) + ("\b" * rest.size))
|
335
|
+
|
336
|
+
state.position += 1
|
337
|
+
end
|
338
|
+
|
339
|
+
else
|
340
|
+
return false
|
341
|
+
end
|
342
|
+
|
343
|
+
true
|
344
|
+
end
|
345
|
+
|
346
|
+
def censor(str, with)
|
347
|
+
return str unless with
|
348
|
+
with * str.size
|
349
|
+
end
|
350
|
+
|
351
|
+
def prompt(question, default = nil, choices = nil)
|
352
|
+
msg = question.dup
|
353
|
+
|
354
|
+
if choices
|
355
|
+
msg << " (#{choices.collect(&:to_s).join ", "})"
|
356
|
+
end
|
357
|
+
|
358
|
+
case default
|
359
|
+
when true
|
360
|
+
msg << " [Yn]"
|
361
|
+
when false
|
362
|
+
msg << " [yN]"
|
363
|
+
else
|
364
|
+
msg << " [#{default}]" if default
|
365
|
+
end
|
366
|
+
|
367
|
+
"#{msg}: "
|
368
|
+
end
|
369
|
+
|
370
|
+
def match_type(str, x)
|
371
|
+
case x
|
372
|
+
when Integer
|
373
|
+
str.to_i
|
374
|
+
when true, false
|
375
|
+
str.upcase.start_with? "Y"
|
376
|
+
else
|
377
|
+
str
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Definitions for reading character-by-character with no echoing.
|
382
|
+
begin
|
383
|
+
require "Win32API"
|
384
|
+
|
385
|
+
def with_char_io(input)
|
386
|
+
yield
|
387
|
+
rescue Interact::JumpToPrompt => e
|
388
|
+
e.jump
|
389
|
+
end
|
390
|
+
|
391
|
+
def get_character(input)
|
392
|
+
if input == STDIN
|
393
|
+
begin
|
394
|
+
Win32API.new("msvcrt", "_getch", [], "L").call.chr
|
395
|
+
rescue
|
396
|
+
Win32API.new("crtdll", "_getch", [], "L").call.chr
|
397
|
+
end
|
398
|
+
else
|
399
|
+
input.getc.chr
|
400
|
+
end
|
401
|
+
end
|
402
|
+
rescue LoadError
|
403
|
+
begin
|
404
|
+
require "termios"
|
405
|
+
|
406
|
+
def with_char_io(input)
|
407
|
+
return yield unless input.tty?
|
408
|
+
|
409
|
+
before = Termios.getattr(input)
|
410
|
+
|
411
|
+
new = before.dup
|
412
|
+
new.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
|
413
|
+
new.c_cc[Termios::VMIN] = 1
|
414
|
+
|
415
|
+
begin
|
416
|
+
Termios.setattr(input, Termios::TCSANOW, new)
|
417
|
+
yield
|
418
|
+
rescue Interact::JumpToPrompt => e
|
419
|
+
Termios.setattr(input, Termios::TCSANOW, before)
|
420
|
+
e.jump
|
421
|
+
ensure
|
422
|
+
Termios.setattr(input, Termios::TCSANOW, before)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
def get_character(input)
|
427
|
+
input.getc.chr
|
428
|
+
end
|
429
|
+
rescue LoadError
|
430
|
+
def with_char_io(input)
|
431
|
+
return yield unless input.tty?
|
432
|
+
|
433
|
+
begin
|
434
|
+
before = `stty -g`
|
435
|
+
system("stty -echo -icanon isig")
|
436
|
+
yield
|
437
|
+
rescue Interact::JumpToPrompt => e
|
438
|
+
system("stty #{before}")
|
439
|
+
e.jump
|
440
|
+
ensure
|
441
|
+
system("stty #{before}")
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def get_character(input)
|
446
|
+
input.getc.chr
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Copyright (c) 2011 Alex Suraci
|
2
|
+
|
3
|
+
module Interactive
|
4
|
+
# Allow classes to enable/disable the rewind feature via +disable_rewind+
|
5
|
+
# and +enable_rewind+.
|
6
|
+
def self.included klass
|
7
|
+
class << klass
|
8
|
+
def disable_rewind
|
9
|
+
def self.rewind_enabled?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def enable_rewind
|
15
|
+
def self.rewind_enabled?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def rewind_enabled?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
klass.class_eval do
|
26
|
+
def rewind_enabled?
|
27
|
+
self.class.rewind_enabled?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Ask a question and get an answer. Rewind-aware; call +disable_rewind+ on
|
33
|
+
# your class to disable.
|
34
|
+
#
|
35
|
+
# See Interact#ask for the other possible values in +options+.
|
36
|
+
#
|
37
|
+
# [question] The prompt, without ": " at the end.
|
38
|
+
#
|
39
|
+
# [options] An optional hash containing the following options.
|
40
|
+
#
|
41
|
+
# forget::
|
42
|
+
# Set to +true+ to prevent rewinding from remembering the user's answer.
|
43
|
+
def ask(question, options = {}, &callback)
|
44
|
+
rewind = Interact::HAS_CALLCC && rewind_enabled?
|
45
|
+
|
46
|
+
if rewind
|
47
|
+
prompt, answer = callcc { |cc| [cc, nil] }
|
48
|
+
else
|
49
|
+
prompt, answer = nil, nil
|
50
|
+
end
|
51
|
+
|
52
|
+
if answer.nil?
|
53
|
+
default = options[:default]
|
54
|
+
else
|
55
|
+
default = answer
|
56
|
+
end
|
57
|
+
|
58
|
+
prompts = (@__prompts ||= [])
|
59
|
+
|
60
|
+
callback ||= options[:callback]
|
61
|
+
|
62
|
+
options[:prompts] = prompts
|
63
|
+
|
64
|
+
ans = Interact.ask(question, options, &callback)
|
65
|
+
|
66
|
+
if rewind
|
67
|
+
prompts << [prompt, options[:forget] ? nil : ans]
|
68
|
+
end
|
69
|
+
|
70
|
+
ans
|
71
|
+
end
|
72
|
+
|
73
|
+
# Clear prompts.
|
74
|
+
#
|
75
|
+
# Questions asked after this are rewindable, but questions asked beforehand
|
76
|
+
# are no longer reachable.
|
77
|
+
#
|
78
|
+
# Use this after you've performed some mutation based on the user's input.
|
79
|
+
def finalize
|
80
|
+
@__prompts = []
|
81
|
+
end
|
82
|
+
end
|
data/lib/version.rb
CHANGED
metadata
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: interact
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: "0.
|
8
|
+
- 3
|
9
|
+
version: "0.3"
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Alex Suraci
|
@@ -14,7 +14,8 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-11-08 00:00:00 -08:00
|
18
|
+
default_executable:
|
18
19
|
dependencies:
|
19
20
|
- !ruby/object:Gem::Dependency
|
20
21
|
name: rake
|
@@ -58,8 +59,11 @@ files:
|
|
58
59
|
- LICENSE
|
59
60
|
- README.md
|
60
61
|
- Rakefile
|
62
|
+
- lib/interact/interact.rb
|
63
|
+
- lib/interact/interactive.rb
|
61
64
|
- lib/interact.rb
|
62
65
|
- lib/version.rb
|
66
|
+
has_rdoc: true
|
63
67
|
homepage: http://github.com/vito/interact
|
64
68
|
licenses: []
|
65
69
|
|
@@ -89,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
93
|
requirements: []
|
90
94
|
|
91
95
|
rubyforge_project:
|
92
|
-
rubygems_version: 1.
|
96
|
+
rubygems_version: 1.6.2
|
93
97
|
signing_key:
|
94
98
|
specification_version: 3
|
95
99
|
summary: A simple API for command-line interaction.
|