interact 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +30 -0
  2. data/README.md +102 -0
  3. data/Rakefile +19 -0
  4. data/lib/interact.rb +436 -0
  5. data/lib/version.rb +5 -0
  6. metadata +99 -0
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c)2011, Alex Suraci
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of Alex Suraci nor the names of other
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Interact
2
+
3
+ Another interaction library for Ruby, with an interesting twist: the user can
4
+ go back in time to re-answer questions.
5
+
6
+ *Copyright 2011, Alex Suraci. Licensed under the MIT license, please see the
7
+ LICENSE file. All rights reserved.*
8
+
9
+
10
+ ## Basic Usage
11
+
12
+ ```ruby
13
+ require "rubygems"
14
+ require "interact"
15
+
16
+ class MyInteractiveClass
17
+ include Interactive
18
+
19
+ def run
20
+ first = ask "Some question?"
21
+ second = ask "Boolean default?", :default => true
22
+ third = ask "Stringy default?", :default => "foo"
23
+
24
+ fourth = ask "Multiple choice?", :choices => ["foo", "bar", "fizz"]
25
+
26
+ some_mutation = []
27
+
28
+ fifth = ask "Multiple choice, indexed list?",
29
+ :choices => ["foo", "bar", "fizz"],
30
+ :indexed => true
31
+
32
+ some_mutation << fifth
33
+
34
+ finalize
35
+
36
+ sixth = ask "Password", :echo => "*", :forget => true
37
+
38
+ p [first, second, third, fourth, fifth, sixth]
39
+ end
40
+ end
41
+
42
+ MyInteractiveClass.new.run
43
+ ```
44
+
45
+ After running this, the user will be prompted with each question one-by-one.
46
+ Interact supports basic editing features such as going to the start/end,
47
+ editing in the middle of the text, backspace, forward delete, and
48
+ backwards-kill-word.
49
+
50
+ In addition, the user can hit the up arrow to go "back in time" and re-answer
51
+ questins.
52
+
53
+ Make sure you call `finalize` after doing any mutation performed based on user
54
+ input; this will prevent them from rewinding to before this took place. Or you
55
+ can just disable rewinding if it's too complicated (see below).
56
+
57
+ Anyway, here's an example session:
58
+
59
+ ```
60
+ Some question?: hello<enter>
61
+ Boolean default? [Yn]: <up>
62
+ Some question? ["hello"]: trying again<enter>
63
+ Boolean default? [Yn]: n<enter>
64
+ Stringy default? ["foo"]: <up>
65
+ Boolean default? [yN]: y<enter>
66
+ Stringy default? ["foo"]: <enter>
67
+ Multiple choice? ("foo", "bar", "fizz"): f<enter>
68
+ Please disambiguate: foo or fizz?
69
+ Multiple choice? ("foo", "bar", "fizz"): fo<enter>
70
+ 1: foo
71
+ 2: bar
72
+ 3: fizz
73
+ Multiple choice, indexed list?: 2<enter>
74
+ Password: ******<enter>
75
+ ["trying again", true, "foo", "foo", "bar", "secret"]
76
+ ```
77
+
78
+ Note that the user's previous answers become the new defaults for the question
79
+ if they rewind.
80
+
81
+ ## Disabling Rewinding
82
+
83
+ Interact provides a nifty user-friendly "rewinding" feature, which allows the user to go back in time and re-answer a question. If you don't want this feature, simply set `@@allow_rewind` to `false` in your class.
84
+
85
+ ```ruby
86
+ class NoRewind
87
+ include Interactive
88
+ @@allow_rewind = false
89
+
90
+ def run
91
+ res = ask "Is there no return?", :default => true
92
+
93
+ if res == allow_rewind
94
+ puts "You're right!"
95
+ else
96
+ puts "Nope! It's disabled."
97
+ end
98
+ end
99
+ end
100
+
101
+ NoRewind.new.run
102
+ ```
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'rake'
2
+
3
+ task :default => "spec"
4
+
5
+ desc "Run specs"
6
+ task "spec" => ["bundler:install", "test:spec"]
7
+
8
+ namespace "bundler" do
9
+ desc "Install gems"
10
+ task "install" do
11
+ sh("bundle install")
12
+ end
13
+ end
14
+
15
+ namespace "test" do
16
+ task "spec" do |t|
17
+ sh("cd spec && rake spec")
18
+ end
19
+ end
data/lib/interact.rb ADDED
@@ -0,0 +1,436 @@
1
+ # Copyright (c) 2011 Alex Suraci
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 disable the rewind feature via the +allow_rewind+ class
332
+ # variable.
333
+ def self.included klass
334
+ klass.send :class_variable_set, :@@allow_rewind, true
335
+
336
+ klass.class_eval do
337
+ # Accessor for the +allow_rewind+ class variable, which determines
338
+ # whether to enable the rewinding feature.
339
+ def allow_rewind
340
+ self.class.send :class_variable_get, :@@allow_rewind
341
+ end
342
+ end
343
+ end
344
+
345
+ # General-purpose interaction.
346
+ #
347
+ # [question] The prompt, without ": " at the end.
348
+ #
349
+ # [options] An optional hash containing the following options.
350
+ #
351
+ # input::
352
+ # The input source (defaults to +STDIN+).
353
+ #
354
+ # default::
355
+ # The default value, also used to attempt type conversion of the answer
356
+ # (e.g. numeric/boolean).
357
+ #
358
+ # choices::
359
+ # An array (or +Enumerable+) of strings to choose from.
360
+ #
361
+ # indexed::
362
+ # Whether to allow choosing from +:choices+ by their index, best for when
363
+ # there are many choices.
364
+ #
365
+ # echo::
366
+ # A string to echo when showing the input; used for things like censoring
367
+ # password input.
368
+ #
369
+ # forget::
370
+ # Set to false to prevent rewinding from remembering the answer.
371
+ #
372
+ # callback::
373
+ # A block used to override certain actions.
374
+ #
375
+ # The block should take 4 arguments:
376
+ #
377
+ # - the event, e.g. +:up+ or +[:key, X]+ where +X+ is a string containing
378
+ # a single character
379
+ # - the current answer to the question; you'll probably mutate this
380
+ # - the current offset from the start of the answer string, e.g. when
381
+ # typing in the middle of the input, this will be where you insert
382
+ # characters
383
+ # - the +:echo+ option from above, may be +nil+
384
+ #
385
+ # The block should return the updated +position+, or +nil+ if it didn't
386
+ # handle the event
387
+ def ask(question, options = {})
388
+ rewind = Interact::HAS_CALLCC && allow_rewind
389
+
390
+ if rewind
391
+ prompt, answer = callcc { |cc| [cc, nil] }
392
+ else
393
+ prompt, answer = nil, nil
394
+ end
395
+
396
+ if answer.nil?
397
+ default = options[:default]
398
+ else
399
+ default = answer
400
+ end
401
+
402
+ choices = options[:choices]
403
+ indexed = options[:indexed]
404
+ callback = options[:callback]
405
+ input = options[:input] || STDIN
406
+ echo = options[:echo]
407
+
408
+ prompts = (@__prompts ||= [])
409
+
410
+ if choices
411
+ ans = Interact.ask_choices(
412
+ input, question, default, choices, indexed, echo, prompts, &callback
413
+ )
414
+ else
415
+ ans = Interact.ask_default(
416
+ input, question, default, echo, prompts, &callback
417
+ )
418
+ end
419
+
420
+ if rewind
421
+ prompts << [prompt, options[:forget] ? nil : ans]
422
+ end
423
+
424
+ ans
425
+ end
426
+
427
+ # Clear prompts.
428
+ #
429
+ # Questions asked after this are rewindable, but questions asked beforehand
430
+ # are no longer reachable.
431
+ #
432
+ # Use this after you've performed some mutation based on the user's input.
433
+ def finalize
434
+ @__prompts = []
435
+ end
436
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2011 Alex Suraci
2
+
3
+ module Interact
4
+ VERSION = 0.1
5
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interact
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - Alex Suraci
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-10-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 2
45
+ - 0
46
+ version: "2.0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: A simple API for command-line interaction. Provides a novel 'rewinding' feature, allowing users to go back in time and re-enter a botched answer. Supports multiple-choice, password prompting, overriding input events, defaults, etc.
50
+ email: i.am@toogeneric.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README.md
57
+ - LICENSE
58
+ files:
59
+ - LICENSE
60
+ - README.md
61
+ - Rakefile
62
+ - lib/interact.rb
63
+ - lib/version.rb
64
+ has_rdoc: true
65
+ homepage: http://github.com/vito/interact
66
+ licenses: []
67
+
68
+ post_install_message:
69
+ rdoc_options: []
70
+
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.6.2
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: A simple API for command-line interaction.
98
+ test_files: []
99
+