ktec-commander 3.3.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.
@@ -0,0 +1,422 @@
1
+
2
+ module Commander
3
+
4
+ ##
5
+ # = User Interaction
6
+ #
7
+ # Commander's user interaction module mixes in common
8
+ # methods which extend HighLine's functionality such
9
+ # as a #password method rather than calling #ask directly.
10
+
11
+ module UI
12
+
13
+ module_function
14
+
15
+ #--
16
+ # Auto include growl when available.
17
+ #++
18
+
19
+ begin
20
+ require 'growl'
21
+ rescue LoadError
22
+ # Do nothing
23
+ else
24
+ include Growl
25
+ end
26
+
27
+ ##
28
+ # Ask the user for a password. Specify a custom
29
+ # _message_ other than 'Password: ' or override the
30
+ # default _mask_ of '*'.
31
+
32
+ def password message = 'Password: ', mask = '*'
33
+ pass = ask(message) { |q| q.echo = mask }
34
+ pass = password message, mask if pass.nil? || pass.empty?
35
+ pass
36
+ end
37
+
38
+ ##
39
+ # Choose from a set array of _choices_.
40
+
41
+ def choose message, *choices
42
+ say message
43
+ super *choices
44
+ end
45
+
46
+ ##
47
+ # 'Log' an _action_ to the terminal. This is typically used
48
+ # for verbose output regarding actions performed. For example:
49
+ #
50
+ # create path/to/file.rb
51
+ # remove path/to/old_file.rb
52
+ # remove path/to/old_file2.rb
53
+ #
54
+
55
+ def log action, *args
56
+ say '%15s %s' % [action, args.join(' ')]
57
+ end
58
+
59
+ ##
60
+ # Speak _message_ using _voice_ which defaults
61
+ # to 'Alex', which is one of the better voices.
62
+ #
63
+ # === Examples
64
+ #
65
+ # speak 'What is your favorite food? '
66
+ # food = ask 'favorite food?: '
67
+ # speak "wow, I like #{food} too. We have so much alike."
68
+ #
69
+ # === Notes
70
+ #
71
+ # * MacOS only
72
+ #
73
+
74
+ def speak message, voice = :Alex
75
+ Thread.new { applescript "say #{message.inspect} using #{voice.to_s.inspect}" }
76
+ end
77
+
78
+ ##
79
+ # Converse with speech recognition.
80
+ #
81
+ # Currently a "poorman's" DSL to utilize applescript and
82
+ # the MacOS speech recognition server.
83
+ #
84
+ # === Examples
85
+ #
86
+ # case converse 'What is the best food?', :cookies => 'Cookies', :unknown => 'Nothing'
87
+ # when :cookies
88
+ # speak 'o.m.g. you are awesome!'
89
+ # else
90
+ # case converse 'That is lame, shall I convince you cookies are the best?', :yes => 'Ok', :no => 'No', :maybe => 'Maybe another time'
91
+ # when :yes
92
+ # speak 'Well you see, cookies are just fantastic.'
93
+ # else
94
+ # speak 'Ok then, bye.'
95
+ # end
96
+ # end
97
+ #
98
+ # === Notes
99
+ #
100
+ # * MacOS only
101
+ #
102
+
103
+ def converse prompt, responses = {}
104
+ i, commands = 0, responses.map { |key, value| value.inspect }.join(',')
105
+ statement = responses.inject '' do |statement, (key, value)|
106
+ statement << (((i += 1) == 1 ?
107
+ %(if response is "#{value}" then\n):
108
+ %(else if response is "#{value}" then\n))) <<
109
+ %(do shell script "echo '#{key}'"\n)
110
+ end
111
+ applescript(%(
112
+ tell application "SpeechRecognitionServer"
113
+ set response to listen for {#{commands}} with prompt "#{prompt}"
114
+ #{statement}
115
+ end if
116
+ end tell
117
+ )).strip.to_sym
118
+ end
119
+
120
+ ##
121
+ # Execute apple _script_.
122
+
123
+ def applescript script
124
+ `osascript -e "#{ script.gsub('"', '\"') }"`
125
+ end
126
+
127
+ ##
128
+ # Normalize IO streams, allowing for redirection of
129
+ # +input+ and/or +output+, for example:
130
+ #
131
+ # $ foo # => read from terminal I/O
132
+ # $ foo in # => read from 'in' file, output to terminal output stream
133
+ # $ foo in out # => read from 'in' file, output to 'out' file
134
+ # $ foo < in > out # => equivalent to above (essentially)
135
+ #
136
+ # Optionally a +block+ may be supplied, in which case
137
+ # IO will be reset once the block has executed.
138
+ #
139
+ # === Examples
140
+ #
141
+ # command :foo do |c|
142
+ # c.syntax = 'foo [input] [output]'
143
+ # c.when_called do |args, options|
144
+ # # or io(args.shift, args.shift)
145
+ # io *args
146
+ # str = $stdin.gets
147
+ # puts 'input was: ' + str.inspect
148
+ # end
149
+ # end
150
+ #
151
+
152
+ def io input = nil, output = nil, &block
153
+ $stdin = File.new(input) if input
154
+ $stdout = File.new(output, 'r+') if output
155
+ if block
156
+ yield
157
+ reset_io
158
+ end
159
+ end
160
+
161
+ ##
162
+ # Reset IO to initial constant streams.
163
+
164
+ def reset_io
165
+ $stdin, $stdout = STDIN, STDOUT
166
+ end
167
+
168
+ ##
169
+ # Prompt _editor_ for input. Optionally supply initial
170
+ # _input_ which is written to the editor.
171
+ #
172
+ # The _editor_ defaults to the EDITOR environment variable
173
+ # when present, or 'mate' for TextMate.
174
+ #
175
+ # === Examples
176
+ #
177
+ # ask_editor # => prompts EDITOR with no input
178
+ # ask_editor('foo') # => prompts EDITOR with default text of 'foo'
179
+ # ask_editor('foo', :mate) # => prompts TextMate with default text of 'foo'
180
+ #
181
+
182
+ def ask_editor input = nil, editor = ENV['EDITOR'] || 'mate'
183
+ IO.popen(editor.to_s, 'w+') do |pipe|
184
+ pipe.puts input.to_s unless input.nil?
185
+ pipe.close_write
186
+ pipe.read
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Enable paging of output after called.
192
+
193
+ def enable_paging
194
+ return unless $stdout.tty?
195
+ read, write = IO.pipe
196
+
197
+ if Kernel.fork
198
+ $stdin.reopen read
199
+ read.close; write.close
200
+ Kernel.select [$stdin]
201
+ ENV['LESS'] = 'FSRX'
202
+ pager = ENV['PAGER'] || 'less'
203
+ exec pager rescue exec '/bin/sh', '-c', pager
204
+ else
205
+ $stdout.reopen write
206
+ $stderr.reopen write if $stderr.tty?
207
+ read.close; write.close
208
+ return
209
+ end
210
+ end
211
+
212
+ ##
213
+ # Output progress while iterating _arr_.
214
+ #
215
+ # === Examples
216
+ #
217
+ # uris = %w( http://vision-media.ca http://google.com )
218
+ # progress uris, :format => "Remaining: :time_remaining" do |uri|
219
+ # res = open uri
220
+ # end
221
+ #
222
+
223
+ def progress arr, options = {}, &block
224
+ bar = ProgressBar.new arr.length, options
225
+ arr.each { |v| bar.increment yield(v) }
226
+ end
227
+
228
+ ##
229
+ # Implements ask_for_CLASS methods.
230
+
231
+ module AskForClass
232
+ def method_missing meth, *args, &block
233
+ case meth.to_s
234
+ when /^ask_for_([\w]+)/ ; $terminal.ask(args.first, eval($1.capitalize))
235
+ else super
236
+ end
237
+ end
238
+ end
239
+
240
+ ##
241
+ # Substitute _hash_'s keys with their associated values in _str_.
242
+
243
+ def replace_tokens str, hash #:nodoc:
244
+ hash.inject str do |str, (key, value)|
245
+ str.gsub ":#{key}", value.to_s
246
+ end
247
+ end
248
+
249
+ ##
250
+ # = Progress Bar
251
+ #
252
+ # Terminal progress bar utility. In its most basic form
253
+ # requires that the developer specifies when the bar should
254
+ # be incremented. Note that a hash of tokens may be passed to
255
+ # #increment, (or returned when using Object#progress).
256
+ #
257
+ # uris = %w(
258
+ # http://vision-media.ca
259
+ # http://yahoo.com
260
+ # http://google.com
261
+ # )
262
+ #
263
+ # bar = Commander::UI::ProgressBar.new uris.length, options
264
+ # threads = []
265
+ # uris.each do |uri|
266
+ # threads << Thread.new do
267
+ # begin
268
+ # res = open uri
269
+ # bar.increment :uri => uri
270
+ # rescue Exception => e
271
+ # bar.increment :uri => "#{uri} failed"
272
+ # end
273
+ # end
274
+ # end
275
+ # threads.each { |t| t.join }
276
+ #
277
+ # The Object method #progress is also available:
278
+ #
279
+ # progress uris, :width => 10 do |uri|
280
+ # res = open uri
281
+ # { :uri => uri } # Can now use :uri within :format option
282
+ # end
283
+ #
284
+
285
+ class ProgressBar
286
+
287
+ ##
288
+ # Creates a new progress bar.
289
+ #
290
+ # === Options
291
+ #
292
+ # :title Title, defaults to "Progress"
293
+ # :width Width of :progress_bar
294
+ # :progress_str Progress string, defaults to "="
295
+ # :incomplete_str Incomplete bar string, defaults to '.'
296
+ # :format Defaults to ":title |:progress_bar| :percent_complete% complete "
297
+ # :tokens Additional tokens replaced within the format string
298
+ # :complete_message Defaults to "Process complete"
299
+ #
300
+ # === Tokens
301
+ #
302
+ # :title
303
+ # :percent_complete
304
+ # :progress_bar
305
+ # :step
306
+ # :steps_remaining
307
+ # :total_steps
308
+ # :time_elapsed
309
+ # :time_remaining
310
+ #
311
+
312
+ def initialize total, options = {}
313
+ @total_steps, @step, @start_time = total, 0, Time.now
314
+ @title = options.fetch :title, 'Progress'
315
+ @width = options.fetch :width, 25
316
+ @progress_str = options.fetch :progress_str, '='
317
+ @incomplete_str = options.fetch :incomplete_str, '.'
318
+ @complete_message = options.fetch :complete_message, 'Process complete'
319
+ @format = options.fetch :format, ':title |:progress_bar| :percent_complete% complete '
320
+ @tokens = options.fetch :tokens, {}
321
+ end
322
+
323
+ ##
324
+ # Completion percentage.
325
+
326
+ def percent_complete
327
+ @step * 100 / @total_steps
328
+ end
329
+
330
+ ##
331
+ # Time that has elapsed since the operation started.
332
+
333
+ def time_elapsed
334
+ Time.now - @start_time
335
+ end
336
+
337
+ ##
338
+ # Estimated time remaining.
339
+
340
+ def time_remaining
341
+ (time_elapsed / @step) * steps_remaining
342
+ end
343
+
344
+ ##
345
+ # Number of steps left.
346
+
347
+ def steps_remaining
348
+ @total_steps - @step
349
+ end
350
+
351
+ ##
352
+ # Formatted progress bar.
353
+
354
+ def progress_bar
355
+ (@progress_str * (@width * percent_complete / 100)).ljust @width, @incomplete_str
356
+ end
357
+
358
+ ##
359
+ # Generates tokens for this step.
360
+
361
+ def generate_tokens
362
+ {
363
+ :title => @title,
364
+ :percent_complete => percent_complete,
365
+ :progress_bar => progress_bar,
366
+ :step => @step,
367
+ :steps_remaining => steps_remaining,
368
+ :total_steps => @total_steps,
369
+ :time_elapsed => "%0.2fs" % time_elapsed,
370
+ :time_remaining => "%0.2fs" % time_remaining,
371
+ }.
372
+ merge! @tokens
373
+ end
374
+
375
+ ##
376
+ # Output the progress bar.
377
+
378
+ def show
379
+ unless finished?
380
+ erase_line
381
+ if completed?
382
+ $terminal.say UI.replace_tokens(@complete_message, generate_tokens) if @complete_message.is_a? String
383
+ else
384
+ $terminal.say UI.replace_tokens(@format, generate_tokens) << ' '
385
+ end
386
+ end
387
+ end
388
+
389
+ ##
390
+ # Whether or not the operation is complete, and we have finished.
391
+
392
+ def finished?
393
+ @step == @total_steps + 1
394
+ end
395
+
396
+ ##
397
+ # Whether or not the operation has completed.
398
+
399
+ def completed?
400
+ @step == @total_steps
401
+ end
402
+
403
+ ##
404
+ # Increment progress. Optionally pass _tokens_ which
405
+ # can be displayed in the output format.
406
+
407
+ def increment tokens = {}
408
+ @step += 1
409
+ @tokens.merge! tokens if tokens.is_a? Hash
410
+ show
411
+ end
412
+
413
+ ##
414
+ # Erase previous terminal line.
415
+
416
+ def erase_line
417
+ # highline does not expose the output stream
418
+ $terminal.instance_variable_get('@output').print "\r\e[K"
419
+ end
420
+ end
421
+ end
422
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module Commander
3
+ VERSION = '3.3.0'
4
+ end
@@ -0,0 +1,125 @@
1
+
2
+ describe Commander::Command do
3
+
4
+ before :each do
5
+ mock_terminal
6
+ create_test_command
7
+ end
8
+
9
+ describe 'Options' do
10
+ before :each do
11
+ @options = Commander::Command::Options.new
12
+ end
13
+
14
+ it "should act like an open struct" do
15
+ @options.send = 'mail'
16
+ @options.call = true
17
+ @options.send.should == 'mail'
18
+ @options.call.should == true
19
+ end
20
+
21
+ it "should allow __send__ to function as always" do
22
+ @options.send = 'foo'
23
+ @options.__send__(:send).should == 'foo'
24
+ end
25
+ end
26
+
27
+ describe "#option" do
28
+ it "should add options" do
29
+ lambda { @command.option '--recursive' }.should change(@command.options, :length).from(1).to(2)
30
+ end
31
+
32
+ it "should allow procs as option handlers" do
33
+ @command.option('--recursive') { |recursive| recursive.should be_true }
34
+ @command.run '--recursive'
35
+ end
36
+
37
+ it "should allow usage of common method names" do
38
+ @command.option '--open file'
39
+ @command.when_called { |_, options| options.open.should == 'foo' }
40
+ @command.run '--open', 'foo'
41
+ end
42
+ end
43
+
44
+ describe "#run" do
45
+ describe "should invoke #when_called" do
46
+ it "with arguments seperated from options" do
47
+ @command.when_called { |args, options| args.join(' ').should == 'just some args' }
48
+ @command.run '--verbose', 'just', 'some', 'args'
49
+ end
50
+
51
+ it "calling the #call method by default when an object is called" do
52
+ object = mock 'Object'
53
+ object.should_receive(:call).once
54
+ @command.when_called object
55
+ @command.run 'foo'
56
+ end
57
+
58
+ it "calling an arbitrary method when an object is called" do
59
+ object = mock 'Object'
60
+ object.should_receive(:foo).once
61
+ @command.when_called object, :foo
62
+ @command.run 'foo'
63
+ end
64
+
65
+ it "should raise an error when no handler is present" do
66
+ lambda { @command.when_called }.should raise_error(ArgumentError)
67
+ end
68
+ end
69
+
70
+ describe "should populate options with" do
71
+ it "boolean values" do
72
+ @command.option '--[no-]toggle'
73
+ @command.when_called { |_, options| options.toggle.should be_true }
74
+ @command.run '--toggle'
75
+ @command.when_called { |_, options| options.toggle.should be_false }
76
+ @command.run '--no-toggle'
77
+ end
78
+
79
+ it "manditory arguments" do
80
+ @command.option '--file FILE'
81
+ @command.when_called { |_, options| options.file.should == 'foo' }
82
+ @command.run '--file', 'foo'
83
+ lambda { @command.run '--file' }.should raise_error(OptionParser::MissingArgument)
84
+ end
85
+
86
+ it "optional arguments" do
87
+ @command.option '--use-config [file] '
88
+ @command.when_called { |_, options| options.use_config.should == 'foo' }
89
+ @command.run '--use-config', 'foo'
90
+ @command.when_called { |_, options| options.use_config.should be_nil }
91
+ @command.run '--use-config'
92
+ @command.option '--interval N', Integer
93
+ @command.when_called { |_, options| options.interval.should == 5 }
94
+ @command.run '--interval', '5'
95
+ lambda { @command.run '--interval', 'invalid' }.should raise_error(OptionParser::InvalidArgument)
96
+ end
97
+
98
+ it "lists" do
99
+ @command.option '--fav COLORS', Array
100
+ @command.when_called { |_, options| options.fav.should == ['red', 'green', 'blue'] }
101
+ @command.run '--fav', 'red,green,blue'
102
+ end
103
+
104
+ it "lists with multi-word items" do
105
+ @command.option '--fav MOVIES', Array
106
+ @command.when_called { |_, options| options.fav.should == ['super\ bad', 'nightmare'] }
107
+ @command.run '--fav', 'super\ bad,nightmare'
108
+ end
109
+
110
+ it "defaults" do
111
+ @command.option '--files LIST', Array
112
+ @command.option '--interval N', Integer
113
+ @command.when_called do |_, options|
114
+ options.default \
115
+ :files => ['foo', 'bar'],
116
+ :interval => 5
117
+ options.files.should == ['foo', 'bar']
118
+ options.interval.should == 15
119
+ end
120
+ @command.run '--interval', '15'
121
+ end
122
+ end
123
+ end
124
+
125
+ end