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.
@@ -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.