interact 0.1

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