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