alfred-workflow 1.11.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,10 +3,15 @@ require 'rubygems' unless defined? Gem # rubygems is only needed in 1.8
3
3
  require 'plist'
4
4
  require 'fileutils'
5
5
  require 'yaml'
6
+ require 'optparse'
7
+ require 'ostruct'
8
+ require 'gyoku'
9
+ require 'nori'
6
10
 
7
11
  require 'alfred/ui'
8
12
  require 'alfred/feedback'
9
13
  require 'alfred/setting'
14
+ require 'alfred/handler/help'
10
15
 
11
16
  module Alfred
12
17
 
@@ -18,15 +23,33 @@ module Alfred
18
23
 
19
24
  class ObjCError < AlfredError; status_code(1) ; end
20
25
  class NoBundleIDError < AlfredError; status_code(2) ; end
26
+ class InvalidArgument < AlfredError; status_code(10) ; end
21
27
  class InvalidFormat < AlfredError; status_code(11) ; end
22
28
  class NoMethodError < AlfredError; status_code(13) ; end
23
29
  class PathError < AlfredError; status_code(14) ; end
24
30
 
25
31
  class << self
26
32
 
33
+ #
34
+ # Default entry point to build alfred workflow with this gem
35
+ #
36
+ # Example:
37
+ #
38
+ # class MyHandler < ::Alfred::Handler::Base
39
+ # # ......
40
+ # end
41
+ # Alfred.with_friendly_error do |alfred|
42
+ # alfred.with_rescue_feedback = true
43
+ # alfred.with_help_feedback = true
44
+ # MyHandler.new(alfred).register
45
+ # end
46
+ #
27
47
  def with_friendly_error(alfred = Alfred::Core.new, &blk)
28
48
  begin
49
+
29
50
  yield alfred
51
+ alfred.start_handler
52
+
30
53
  rescue AlfredError => e
31
54
  alfred.ui.error e.message
32
55
  alfred.ui.debug e.backtrace.join("\n")
@@ -42,6 +65,8 @@ module Alfred
42
65
  rescue SystemExit => e
43
66
  puts alfred.rescue_feedback(
44
67
  :title => "SystemExit: #{e.status}") if alfred.with_rescue_feedback
68
+ alfred.ui.error e.message
69
+ alfred.ui.debug e.backtrace.join("\n")
45
70
  exit e.status
46
71
  rescue Exception => e
47
72
  alfred.ui.error(
@@ -83,41 +108,190 @@ __APPLESCRIPT__}.chop
83
108
  end
84
109
 
85
110
  class Core
86
- attr_accessor :with_rescue_feedback
87
- attr_accessor :with_help_feedback
111
+ attr_accessor :with_rescue_feedback, :with_help_feedback
112
+ attr_accessor :cached_feedback_reload_option
88
113
 
89
- def initialize(with_help_feedback = false,
90
- with_rescue_feedback = false,
91
- &blk)
92
- @workflow_dir = Dir.pwd
93
- @with_rescue_feedback = with_rescue_feedback
94
- @with_help_feedback = with_rescue_feedback
114
+ attr_reader :handler_controller
115
+ attr_reader :query, :raw_query
116
+
117
+
118
+ def initialize(&blk)
119
+ @with_rescue_feedback = true
120
+ @with_help_feedback = false
121
+ @cached_feedback_reload_option = {
122
+ :use_reload_option => false,
123
+ :use_exclamation_mark => false
124
+ }
125
+
126
+ @query = ARGV
127
+ @raw_query = ARGV.dup
128
+
129
+ @handler_controller = ::Alfred::Handler::Controller.new
95
130
 
96
131
  instance_eval(&blk) if block_given?
132
+
133
+ raise NoBundleIDError unless bundle_id
134
+ end
135
+
136
+
137
+ def debug?
138
+ ui.level >= LogUI::WARN
139
+ end
140
+
141
+ #
142
+ # Main loop to work with handlers
143
+ #
144
+ def start_handler
145
+
146
+ if @with_help_feedback
147
+ ::Alfred::Handler::Help.new(self, :with_handler_help => true).register
148
+ end
149
+
150
+ return if @handler_controller.empty?
151
+
152
+ # step 1: register option parser for handlers
153
+ @handler_controller.each do |handler|
154
+ handler.on_parser
155
+ end
156
+
157
+ begin
158
+ query_parser.parse!
159
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
160
+ ui.warn(
161
+ "Fail to parse user query.\n" \
162
+ " #{e.inspect}\n #{e.backtrace.join(" \n")}\n") if debug?
163
+ end
164
+
165
+ if @cached_feedback_reload_option[:use_exclamation_mark] && !options.should_reload_cached_feedback
166
+ if ARGV[0].eql?('!')
167
+ ARGV.shift
168
+ options.should_reload_cached_feedback = true
169
+ elsif ARGV[-1].eql?('!')
170
+ ARGV.delete_at(-1)
171
+ options.should_reload_cached_feedback = true
172
+ end
173
+ end
174
+
175
+ @query = ARGV
176
+
177
+ # step 2: dispatch options to handler for FEEDBACK or ACTION
178
+ case options.workflow_mode
179
+ when :feedback
180
+ @handler_controller.each_handler do |handler|
181
+ handler.on_feedback
182
+ end
183
+
184
+ puts feedback.to_alfred(@query)
185
+ when :action
186
+ arg = @query
187
+ if @query.length == 1
188
+ if hsh = xml_parser(@query[0])
189
+ arg = hsh
190
+ end
191
+ end
192
+
193
+ if arg.is_a?(Hash)
194
+ @handler_controller.each_handler do |handler|
195
+ handler.on_action(arg)
196
+ end
197
+ else
198
+ #fallback default action
199
+ arg.each do |a|
200
+ if File.exist? a
201
+ %x{open "#{a}"}
202
+ end
203
+ end
204
+ end
205
+ else
206
+ raise InvalidArgument, "#{options.workflow_mode} mode is not supported."
207
+ end
208
+
209
+ # step 3: close
210
+ close
211
+ @handler_controller.each_handler do |handler|
212
+ handler.on_close
213
+ end
214
+
215
+ end
216
+
217
+ def close
218
+ @feedback.close if @feedback
219
+ @setting.close if @setting
220
+ # @workflow_setting.close if @workflow_setting
221
+ end
222
+
223
+
224
+ #
225
+ # Parse and return user query to three parts
226
+ #
227
+ # [ [before], last option, tail ]
228
+ #
229
+ def last_option
230
+ (@raw_query.size - 1).downto(0) do |i|
231
+ if @raw_query[i].start_with? '-'
232
+ if @raw_query[i] == @raw_query[-1]
233
+ return @raw_query[0...i], '', @raw_query[i]
234
+ else
235
+ return @raw_query[0..i], @raw_query[i], @raw_query[(i + 1)..-1].join(' ')
236
+ end
237
+ end
238
+ end
239
+
240
+ return [], '', @raw_query.join(' ')
241
+ end
242
+
243
+ def options(opts = {})
244
+ @options ||= OpenStruct.new(opts)
245
+ end
246
+
247
+ def query_parser
248
+ @query_parser ||= init_query_parser
249
+ end
250
+
251
+ def xml_parser(xml)
252
+ @xml_parser ||= Nori.new(:parser => :rexml,
253
+ :convert_tags_to => lambda { |tag| tag.to_sym })
254
+ begin
255
+ hsh = @xml_parser.parse(xml)
256
+ return hsh[:root]
257
+ rescue REXML::ParseException, Nokogiri::XML::SyntaxError
258
+ return nil
259
+ end
260
+ end
261
+
262
+ def xml_builder(arg)
263
+ Gyoku.xml(:root => arg)
97
264
  end
98
265
 
99
266
  def ui
100
- raise NoBundleIDError unless bundle_id
101
267
  @ui ||= LogUI.new(bundle_id)
102
268
  end
103
269
 
104
- def setting(&blk)
105
- @setting ||= Setting.new(self, &blk)
106
- end
107
270
 
271
+ #
272
+ # workflow setting is stored in the workflow_folder
273
+ #
108
274
  def workflow_setting(opts = {})
109
- @workflow_setting ||= init_workflow_setting(opts)
275
+ @workflow_setting ||= new_setting(opts)
110
276
  end
111
277
 
112
- def with_cached_feedback(&blk)
113
- @feedback = CachedFeedback.new(self, &blk)
278
+ #
279
+ # user setting is stored in the storage_path by default
280
+ #
281
+ def user_setting(&blk)
282
+ @setting ||= new_setting(
283
+ :file => File.join(storage_path, "setting.yaml")
284
+ )
114
285
  end
286
+ alias_method :setting, :user_setting
115
287
 
116
- def feedback
117
- raise NoBundleIDError unless bundle_id
118
- @feedback ||= Feedback.new
288
+
289
+ def feedback(opts = {}, &blk)
290
+ @feedback ||= new_feedback(opts, &blk)
119
291
  end
120
292
 
293
+ alias_method :with_cached_feedback, :feedback
294
+
121
295
  def info_plist
122
296
  @info_plist ||= Plist::parse_xml('info.plist')
123
297
  end
@@ -128,9 +302,8 @@ __APPLESCRIPT__}.chop
128
302
  end
129
303
 
130
304
  def volatile_storage_path
131
- raise NoBundleIDError unless bundle_id
132
305
  path = "#{ENV['HOME']}/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/#{bundle_id}"
133
- unless File.exist?(path)
306
+ unless File.directory?(path)
134
307
  FileUtils.mkdir_p(path)
135
308
  end
136
309
  path
@@ -138,7 +311,6 @@ __APPLESCRIPT__}.chop
138
311
 
139
312
  # Non-volatile storage directory for this bundle
140
313
  def storage_path
141
- raise NoBundleIDError unless bundle_id
142
314
  path = "#{ENV['HOME']}/Library/Application Support/Alfred 2/Workflow Data/#{bundle_id}"
143
315
  unless File.exist?(path)
144
316
  FileUtils.mkdir_p(path)
@@ -147,55 +319,111 @@ __APPLESCRIPT__}.chop
147
319
  end
148
320
 
149
321
 
150
- def help_feedback(opts = {})
151
- ws = workflow_setting.load
152
- if ws.has_key? :help
153
- ws[:help].map do |item|
154
- case item[:kind]
155
- when 'url'
156
- item[:folder] = storage_path
157
- Feedback::UrlItem.new(item)
158
- end
159
- end
160
- end
322
+ def cached_feedback?
323
+ @cached_feedback_reload_option.values.any?
161
324
  end
162
325
 
163
326
 
164
327
  def rescue_feedback(opts = {})
165
328
  default_opts = {
166
- :title => "Failed Query!",
167
- :subtitle => "Check the log file below for extra debug info.",
168
- :uid => 'Rescue Feedback',
169
- :icon => {
170
- :type => "default" ,
171
- :name => "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns"
172
- }
329
+ :title => "Failed Query!" ,
330
+ :subtitle => "Check log #{ui.log_file} for extra debug info." ,
331
+ :uid => 'Rescue Feedback' ,
332
+ :valid => 'no' ,
333
+ :autocomplete => '' ,
334
+ :icon => Feedback.CoreServicesIcon('AlertStopIcon')
173
335
  }
336
+ if @with_help_feedback
337
+ default_opts[:autocomplete] = '-h'
338
+ end
174
339
  opts = default_opts.update(opts)
175
340
 
176
341
  items = []
177
342
  items << Feedback::Item.new(opts[:title], opts)
178
- items << Feedback::FileItem.new(ui.log_file)
343
+ log_item = Feedback::FileItem.new(ui.log_file)
344
+ log_item.uid = nil
345
+ items << log_item
179
346
 
180
347
  feedback.to_alfred('', items)
181
348
  end
182
349
 
183
- private
350
+ def on_help
351
+ reload_help_item
352
+ end
353
+
184
354
 
185
- def init_workflow_setting(opts)
355
+ def new_feedback(opts, &blk)
356
+ ::Alfred::Feedback.new(self, opts, &blk)
357
+ end
358
+
359
+
360
+ def new_setting(opts)
186
361
  default_opts = {
187
- :file => "setting.yaml",
362
+ :file => File.join(Alfred.workflow_folder, "setting.yaml"),
188
363
  :format => 'yaml',
189
364
  }
190
365
  opts = default_opts.update(opts)
191
366
 
192
- @workflow_setting = Setting.new(self) do
193
- use_setting_file opts
367
+ ::Alfred::Setting.new(self) do
368
+ @backend_file = opts[:file]
369
+ @formt = opts[:format]
194
370
  end
195
- @workflow_setting
196
371
  end
197
372
 
198
- end
373
+ private
374
+
375
+ def reload_help_item
376
+ title = []
377
+ if @cached_feedback_reload_option[:use_exclamation_mark]
378
+ title.push "!"
379
+ end
380
+
381
+ if @cached_feedback_reload_option[:use_reload_option]
382
+ title.push "-r, --reload"
383
+ end
384
+
385
+ unless title.empty?
386
+ return {
387
+ :kind => 'text',
388
+ :order => (Handler::HelpItem::Base_Order * 10),
389
+ :title => "#{title.join(', ')} [Reload cached feedback unconditionally]" ,
390
+ :subtitle => %q{The '!' mark must be at the beginning or end of the query.} ,
391
+ }
392
+ else
393
+ return nil
394
+ end
395
+ end
199
396
 
397
+ def init_query_parser
398
+ options.workflow_mode = :feedback
399
+ options.modifier = :none
400
+ options.should_reload_cached_feedback = false
401
+
402
+ modifiers = [:command, :alt, :control, :shift, :fn, :none]
403
+ OptionParser.new do |opts|
404
+ opts.separator ""
405
+ opts.separator "Built-in Options:"
406
+
407
+ opts.on("--workflow-mode [TYPE]", [:feedback, :action],
408
+ "Alfred handler working mode (feedback, action)") do |t|
409
+ options.workflow_mode = t
410
+ end
411
+
412
+ opts.on("--modifier [MODIFIER]", modifiers,
413
+ "Alfred action modifier (#{modifiers})") do |t|
414
+ options.modifier = t
415
+ end
416
+
417
+ if @cached_feedback_reload_option[:use_reload_option]
418
+ opts.on("-r", "--reload", "Reload cached feedback") do
419
+ options.should_reload_cached_feedback = true
420
+ end
421
+ end
422
+ opts.separator ""
423
+ opts.separator "Handler Options:"
424
+ end
425
+
426
+ end
427
+ end
200
428
  end
201
429
 
@@ -1,14 +1,20 @@
1
1
  require "rexml/document"
2
2
  require 'alfred/feedback/item'
3
3
  require 'alfred/feedback/file_item'
4
+ require 'alfred/feedback/webloc_item'
4
5
 
5
6
  module Alfred
6
7
 
7
8
  class Feedback
8
9
  attr_accessor :items
10
+ attr_reader :backend_file
9
11
 
10
- def initialize
12
+ def initialize(alfred, opts = {}, &blk)
11
13
  @items = []
14
+ @core = alfred
15
+ use_backend(opts)
16
+ instance_eval(&blk) if block_given?
17
+
12
18
  end
13
19
 
14
20
  def add_item(opts = {})
@@ -20,6 +26,13 @@ module Alfred
20
26
  @items << FileItem.new(path, opts)
21
27
  end
22
28
 
29
+ def add_webloc_item(path, opts = {})
30
+ unless opts[:folder]
31
+ opts[:folder] = @core.storage_path
32
+ end
33
+ @items << WeblocItem.new(path, opts)
34
+ end
35
+
23
36
  def to_xml(with_query = '', items = @items)
24
37
  document = REXML::Element.new("items")
25
38
  if with_query.empty?
@@ -36,47 +49,122 @@ module Alfred
36
49
 
37
50
  alias_method :to_alfred, :to_xml
38
51
 
52
+ #
53
+ # Merge with other feedback
54
+ #
55
+ def merge!(other)
56
+ if other.is_a? Array
57
+ @items |= other
58
+ elsif other.is_a? Alfred::Feedback
59
+ @items |= other.items
60
+ else
61
+ raise ArgumentError, "Feedback can not merge with #{other.class}"
62
+ end
63
+ end
39
64
 
65
+ #
66
+ # The workflow is about to complete
67
+ #
68
+ # - save cached feedback if necessary
69
+ #
70
+ def close
71
+ put_cached_feedback if @backend_file
72
+ end
40
73
 
41
- # serialize
42
- def dump(to_file)
43
- File.open(to_file, "wb") { |f| Marshal.dump(@items, f) }
74
+ #
75
+ # ## helper class method for icon
76
+ #
77
+ def self.CoreServicesIcon(name)
78
+ {
79
+ :type => "default" ,
80
+ :name => "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/#{name}.icns"
81
+ }
44
82
  end
45
83
 
46
- def load(from_file)
47
- @items = File.open(from_file, "rb") { |f| Marshal.load(f) }
84
+ def self.Icon(name)
85
+ {
86
+ :type => "default" ,
87
+ :name => name ,
88
+ }
89
+ end
90
+ def self.FileIcon(path)
91
+ {
92
+ :type => "fileicon" ,
93
+ :name => path ,
94
+ }
48
95
  end
49
- end
50
96
 
51
97
 
52
- class CachedFeedback < Feedback
53
- def initialize(alfred, &blk)
54
- super()
55
- @core = alfred
98
+ #
99
+ # ## serialization
100
+ #
56
101
 
57
- instance_eval(&blk) if block_given?
102
+ def use_backend(opts = {})
103
+ @backend_file = opts[:file] if opts[:file]
104
+ @should_expire_after_second = opts[:expire].to_i if opts[:expire]
58
105
  end
106
+ alias_method :use_cache_file, :use_backend
59
107
 
60
- def use_cache_file(opts = {})
61
- @cf_file = opts[:file] if opts[:file]
62
- @cf_file_valid_time = opts[:expire] if opts[:expire]
108
+ def backend_file
109
+ @backend_file ||= File.join(@core.volatile_storage_path, "cached_feedback")
63
110
  end
64
111
 
65
- def cache_file
66
- @cf_file ||= File.join(@core.volatile_storage_path, "cached_feedback")
112
+ def expired?
113
+ return false unless @should_expire_after_second
114
+ Time.now - File.ctime(backend_file) > @should_expire_after_second
67
115
  end
116
+
68
117
  def get_cached_feedback
69
- return nil unless File.exist?(cache_file)
70
- if @cf_file_valid_time
71
- return nil if Time.now - File.ctime(cache_file) > @cf_file_valid_time
72
- end
73
- load(@cf_file)
74
- return self
118
+ return nil unless File.exist?(backend_file)
119
+ return nil if expired?
120
+
121
+ load(@backend_file)
122
+ self
75
123
  end
76
124
 
77
125
  def put_cached_feedback
78
- dump(cache_file)
126
+ dump(backend_file)
127
+ end
128
+
129
+ def dump(to_file)
130
+ File.open(to_file, "wb") { |f| Marshal.dump(@items, f) }
131
+ end
132
+
133
+ def load(from_file)
134
+ @items = File.open(from_file, "rb") { |f| Marshal.load(f) }
135
+ end
136
+
137
+ def append(from_file)
138
+ @items << File.open(from_file, "rb") { |f| Marshal.load(f) }
139
+ end
140
+
141
+ #
142
+ # Provides yaml serialization support
143
+ #
144
+ if RUBY_VERSION < "1.9"
145
+ def to_yaml_properties
146
+ [ '@items' ]
147
+ end
148
+ else
149
+ def encode_with(coder)
150
+ coder['items'] = @items
151
+ end
79
152
  end
153
+
154
+ #
155
+ # Provides marshalling support for use by the Marshal library.
156
+ #
157
+ def marshal_dump
158
+ @items
159
+ end
160
+
161
+ #
162
+ # Provides marshalling support for use by the Marshal library.
163
+ #
164
+ def marshal_load(x)
165
+ @items = x
166
+ end
167
+
80
168
  end
81
169
 
82
170
  end