interact 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,450 +1,5 @@
1
1
  # Copyright (c) 2011 Alex Suraci
2
2
 
3
- # Helpers for the main API provided by mixing in +Interactive+.
4
- #
5
- # Internal use only. Not a stable API.
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
@@ -1,5 +1,5 @@
1
1
  # Copyright (c) 2011 Alex Suraci
2
2
 
3
3
  module Interact
4
- VERSION = 0.2
4
+ VERSION = 0.3
5
5
  end
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: 15
4
+ hash: 13
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- version: "0.2"
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-10-26 00:00:00 Z
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.8.6
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.