interact 0.2 → 0.3
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.
- 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.
|