alfred-workflow 1.11.3 → 2.0.0

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