├── .gemtest ├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── .vclog ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── History.txt ├── Manifest.txt ├── README.md ├── Rakefile ├── alfred-workflow.gemspec ├── lib ├── alfred.rb └── alfred │ ├── feedback.rb │ ├── feedback │ ├── file_item.rb │ ├── item.rb │ └── webloc_item.rb │ ├── handler.rb │ ├── handler │ ├── autocomplete.rb │ ├── callback.rb │ ├── cofig.rb │ └── help.rb │ ├── osx.rb │ ├── setting.rb │ ├── ui.rb │ ├── util.rb │ └── version.rb ├── spec ├── alfred │ ├── feedback │ │ └── item_spec.rb │ ├── feedback_spec.rb │ ├── setting_spec.rb │ └── ui_spec.rb ├── alfred_spec.rb └── spec_helper.rb └── test └── workflow ├── info.plist └── setting.yaml /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaocai/alfred-workflow/6d3ae12040721588399538ff6c086f2ea4fcc385/.gemtest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | log 3 | rspec_guard_result 4 | .yardoc 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | system 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - 2.0.0 8 | - ree 9 | - jruby-18mode 10 | - jruby-19mode 11 | - jruby-head 12 | branches: 13 | only: 14 | - master 15 | notifications: 16 | recipients: 17 | - caizhaoff@gmail.com 18 | before_script: 19 | - gem install hoe-travis --no-rdoc --no-ri 20 | after_script: 21 | script: rake spec 22 | -------------------------------------------------------------------------------- /.vclog: -------------------------------------------------------------------------------- 1 | # Heuristics used by VCLog itself. 2 | 3 | type :major, 3, "Major Enhancements" 4 | type :minor, 2, "Minor Enhancements" 5 | type :bug, 1, "Bug Fixes" 6 | type :fix, 1, "Bug Fixes" 7 | type :update, 0, "Nominal Changes" 8 | type :doc, -1, "Documentation Changes" 9 | type :test, -1, "Test/Spec Adjustments" 10 | type :admin, -2, "Administrative Changes" 11 | type :log, -3, "Just a record" 12 | 13 | 14 | on Regexp.union(/^(? \w+):/, /^\[(?\w+)\]/) do |commit, md| 15 | type = md[:type].to_sym 16 | commit.type = type 17 | commit.message = commit.message.sub(md[0],'').strip 18 | end 19 | 20 | on /updated? (README\.md|PROFILE|PACKAGE|VERSION|Manifest\.txt)/ do |commit| 21 | commit.type = :admin 22 | end 23 | 24 | on /(bump|bumped|prepare) version/ do |commit| 25 | commit.type = :admin 26 | end 27 | 28 | colors :grey, :blue, :cyan, :green, :yellow, :red, [:red, :bold] 29 | 30 | 31 | # vim: set ft=ruby ts=2 sw=2 tw=78 fmr=[[[,]]] fdm=syntax : 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | source "https://rubygems.org/" 4 | 5 | gemspec 6 | 7 | # vim: syntax=ruby 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | alfred-workflow (2.0.1.20131023093234) 5 | fuzzy_match (>= 2.0.4) 6 | gyoku (>= 1.1.0) 7 | moneta (>= 0.7.19) 8 | nori (>= 2.3.0) 9 | plist (>= 3.1.0) 10 | terminal-notifier (>= 1.5.0) 11 | 12 | GEM 13 | remote: https://rubygems.org/ 14 | specs: 15 | awesome_print (1.2.0) 16 | builder (3.2.2) 17 | coderay (1.0.9) 18 | diff-lcs (1.2.4) 19 | facets (2.9.3) 20 | ffi (1.9.0) 21 | formatador (0.2.4) 22 | fuzzy_match (2.0.4) 23 | growl (1.0.3) 24 | guard (1.8.3) 25 | formatador (>= 0.2.4) 26 | listen (~> 1.3) 27 | lumberjack (>= 1.0.2) 28 | pry (>= 0.9.10) 29 | thor (>= 0.14.6) 30 | guard-bundler (1.0.0) 31 | bundler (~> 1.0) 32 | guard (~> 1.1) 33 | guard-rspec (3.1.0) 34 | guard (>= 1.8) 35 | rspec (~> 2.13) 36 | gyoku (1.1.0) 37 | builder (>= 2.1.2) 38 | hashr (0.0.22) 39 | highline (1.6.20) 40 | hoe (3.7.1) 41 | rake (>= 0.8, < 11.0) 42 | hoe-gemspec (1.0.0) 43 | hoe (>= 2.2.0) 44 | hoe-git (1.6.0) 45 | highline (>= 1.6.0) 46 | hoe-travis (1.2) 47 | hoe (~> 3.0) 48 | travis-lint (~> 1.2) 49 | hoe-version (1.2.0) 50 | hoe-yard (0.1.2) 51 | yard (>= 0.2.3.1) 52 | listen (1.3.1) 53 | rb-fsevent (>= 0.9.3) 54 | rb-inotify (>= 0.9) 55 | rb-kqueue (>= 0.2) 56 | lumberjack (1.0.4) 57 | method_source (0.8.2) 58 | moneta (0.7.20) 59 | nori (2.3.0) 60 | plist (3.1.0) 61 | pry (0.9.12.2) 62 | coderay (~> 1.0.5) 63 | method_source (~> 0.8) 64 | slop (~> 3.4) 65 | rake (10.1.0) 66 | rb-fsevent (0.9.3) 67 | rb-inotify (0.9.2) 68 | ffi (>= 0.5.0) 69 | rb-kqueue (0.2.0) 70 | ffi (>= 0.5.0) 71 | rspec (2.14.1) 72 | rspec-core (~> 2.14.0) 73 | rspec-expectations (~> 2.14.0) 74 | rspec-mocks (~> 2.14.0) 75 | rspec-core (2.14.6) 76 | rspec-expectations (2.14.3) 77 | diff-lcs (>= 1.1.3, < 2.0) 78 | rspec-mocks (2.14.4) 79 | slop (3.4.6) 80 | terminal-notifier (1.5.1) 81 | terminal-notifier-guard (1.5.3) 82 | thor (0.18.1) 83 | travis-lint (1.7.0) 84 | hashr (~> 0.0.22) 85 | yard (0.8.7.2) 86 | 87 | PLATFORMS 88 | ruby 89 | 90 | DEPENDENCIES 91 | alfred-workflow! 92 | awesome_print (>= 1.2.0) 93 | facets (>= 2.9.0) 94 | growl 95 | guard 96 | guard-bundler 97 | guard-rspec 98 | hoe 99 | hoe-gemspec 100 | hoe-git 101 | hoe-travis 102 | hoe-version 103 | hoe-yard (>= 0.1.2) 104 | rake (>= 10.0.0) 105 | rspec (>= 2.13) 106 | terminal-notifier-guard 107 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | 2 | require 'guard/bundler' 3 | require 'guard/rspec' 4 | 5 | notification :growl 6 | # notification :terminal_notifier 7 | 8 | group :frontend do 9 | guard 'bundler' do 10 | watch('Gemfile') 11 | # Uncomment next line if Gemfile contain `gemspec' command 12 | watch(/^.+\.gemspec/) 13 | end 14 | end 15 | 16 | group :singleruby do 17 | 18 | guard 'rspec', :rvm => ['system'], :notification => true do 19 | 20 | watch(%r{^spec/.+_spec\.rb$}) 21 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 22 | watch('spec/spec_helper.rb') { "spec" } 23 | end 24 | 25 | end 26 | 27 | group :multirubies do 28 | 29 | guard 'rspec', :rvm => ['system', '1.9.3', '1.8.7'], :notification => true do 30 | 31 | watch(%r{^spec/.+_spec\.rb$}) 32 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 33 | watch('spec/spec_helper.rb') { "spec" } 34 | end 35 | 36 | end 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 2.0.0 / 2013-10-02 2 | 3 | Changes: 4 | 5 | * 5 Major Enhancements 6 | 7 | * add callback handler 8 | * add osx_version module 9 | * add autocomplete handler 10 | * get help handler to work 11 | * alfred handler framework ( *WIP* ) 12 | 13 | * 15 Minor Enhancements 14 | 15 | * config handler template 16 | * user constant for handler and help item order 17 | * add yaml and marshal serializatio support for feedback class 18 | * add timestamp to callback entry 19 | * auto-save user settings 20 | * flexible autocomplete items (list of mixed string or hash) 21 | * use fuzzy match for autocomplete handler 22 | * sort help feedback items 23 | * default cached feedback reload option 24 | * add rescue feedback for query parser exception 25 | * merge handler help to Help class 26 | * add on_help for handler help message 27 | * Inherit Setting class from Hash 28 | 29 | remove plist as backend for Setting serialization 30 | * update setting class 31 | * display "right" name for contacts 32 | 33 | * 2 Bug Fixes 34 | 35 | * handle feedback yaml serialization based on ruby version 36 | * help handler for empty item 37 | 38 | === 1.11.3 / 2013-10-02 39 | 40 | prefer opts for file_item title (Zhao Cai ) 41 | 42 | Changes: 43 | 44 | * 1 Bug Fixes 45 | 46 | * prefer opts for file_item title 47 | 48 | Bump version to 1.11.2 49 | 50 | 51 | === 1.11.2 / 2013-10-02 52 | 53 | Changes: 54 | 55 | * 1 Minor Enhancements 56 | 57 | * allow opts to be passed to file item feedback 58 | 59 | 60 | === 1.11.1 / 2013-09-27 61 | 62 | optimize feedback serialization (Zhao Cai ) 63 | 64 | Changes: 65 | 66 | * 1 Minor Enhancements 67 | 68 | * only need to serialize feedback items but self 69 | 70 | * 1 Bug Fixes 71 | 72 | * fix travis build error 73 | 74 | "~/Library/Logs" does not exist in non-Mac machine 75 | 76 | 77 | === 1.11.0 / 2013-09-26 78 | 79 | Changes: 80 | 81 | * 1 Major Enhancements 82 | 83 | * add custom item matcher 84 | 85 | 86 | === 1.10.1 / 2013-09-24 87 | 88 | Hash arg for feedback item should not handled here (Zhao Cai ) 89 | 90 | 91 | === 1.10.0 / 2013-09-24 92 | 93 | support Hash arg for feedback item, prepare for flexible matcher and handler (Zhao Cai ) 94 | 95 | Changes: 96 | 97 | * 1 Minor Enhancements 98 | 99 | * support Hash arg for feedback item 100 | 101 | === 1.9.2 / 2013-09-24 102 | 103 | * 1 Minor Enhancements 104 | 105 | * add #front_appid 106 | 107 | * 3 Bug Fixes 108 | 109 | * travis.yml 110 | * rake travis tasks 111 | * with_rescue_feedback 112 | 113 | 114 | === 1.9.1 / 2013-09-14 115 | 116 | Changes: 117 | 118 | * 1 Minor Enhancements 119 | 120 | * uid (optional in Alfred 2.0.3+) 121 | 122 | 123 | === 1.9.0 / 2013-09-14 124 | 125 | update for Alfred feedback spec changes (Zhao Cai ) 126 | 127 | Changes: 128 | 129 | * 2 Minor Enhancements 130 | 131 | * prefer to 132 | * generic help feedback 133 | 134 | 135 | === 1.8.0 / 2013-05-02 136 | 137 | Changes: 138 | 139 | * 1 Minor Enhancements 140 | 141 | * use default `logger` instead of `logging` gem 142 | 143 | === 1.5.2 / 2013-03-29 144 | 145 | * [New]( Travis CI Status Image ) 146 | * [New]( Automate cached feedback save and load! ) 147 | * [New]( Setting class to save and load user configures ) 148 | * [New]( add load and dump for feedback ) 149 | * [New]( Alfred.search(query) to launch alfred with query ) 150 | 151 | === 1.2.6 / 2013-03-27 152 | 153 | ! automate rescue feedback 154 | * save @bundle_id for reaccess 155 | 156 | === 1.2.3 / 2013-03-25 157 | 158 | * add function to output rescue feedback xml items 159 | * add a few query regexp builders 160 | ! refactor feedback items 161 | ! add autotest support 162 | ! add rspec test 163 | 164 | === 1.1.0 / 2013-03-24 165 | 166 | * reorganize Alfred module to multiple files 167 | * use FileItem < Item classes to manage feedback item 168 | * add default query match for Item and FileItem 169 | 170 | 171 | === 1.0.0 / 2013-03-21 172 | 173 | * Birthday! 174 | 175 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .gemtest 2 | .rspec 3 | .ruby-version 4 | Gemfile 5 | Gemfile.lock 6 | Guardfile 7 | History.txt 8 | Manifest.txt 9 | README.md 10 | Rakefile 11 | alfred-workflow.gemspec 12 | lib/alfred.rb 13 | lib/alfred/feedback.rb 14 | lib/alfred/feedback/file_item.rb 15 | lib/alfred/feedback/item.rb 16 | lib/alfred/feedback/webloc_item.rb 17 | lib/alfred/handler.rb 18 | lib/alfred/handler/autocomplete.rb 19 | lib/alfred/handler/callback.rb 20 | lib/alfred/handler/cofig.rb 21 | lib/alfred/handler/help.rb 22 | lib/alfred/osx.rb 23 | lib/alfred/setting.rb 24 | lib/alfred/ui.rb 25 | lib/alfred/util.rb 26 | lib/alfred/version.rb 27 | spec/alfred/feedback/item_spec.rb 28 | spec/alfred/feedback_spec.rb 29 | spec/alfred/setting_spec.rb 30 | spec/alfred/ui_spec.rb 31 | spec/alfred_spec.rb 32 | spec/spec_helper.rb 33 | test/workflow/info.plist 34 | test/workflow/setting.yaml 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alfred-workflow [![Build Status](https://travis-ci.org/zhaocai/alfred-workflow.png?branch=master)](https://travis-ci.org/zhaocai/alfred-workflow) 2 | 3 | * home :: http://zhaocai.github.com/alfred2-ruby-template/ 4 | * rdoc :: http://rubydoc.info/gems/alfred-workflow/ 5 | * code :: https://github.com/zhaocai/alfred-workflow 6 | * bugs :: https://github.com/zhaocai/alfred-workflow/issues 7 | 8 | 9 | ## DESCRIPTION: 10 | 11 | alfred-workflow is a ruby Gem helper for building [Alfred](http://www.alfredapp.com) workflow. 12 | 13 | 14 | ## FEATURES: 15 | 16 | * Use standard [bundler][gembundler] to easily package, manage, and update ruby gems in the workflow. 17 | * Friendly exception and debug output to the Mac OS X Console. 18 | * Automate saving and loading cached feedback 19 | * Automate rescue feedback items to alfred when something goes wrong. 20 | * Functions to easily load and save user configuration (in YAML) 21 | * Functions for smart case query filter of feedback results. 22 | * Functions for finding the bundle ID, cache and storage paths, and query arguments. 23 | * Functions for reading and writing plist files. 24 | * Functions to simplify generating feedback XML for Alfred. 25 | 26 | ## INSTALL: 27 | 28 | `gem install alfred-workflow` 29 | 30 | ## USAGE: 31 | 32 | * Refer to [alfred2-ruby-template]( https://github.com/zhaocai/alfred2-ruby-template ) for examples and detailed instruction. Also refer to some of the example projects: 33 | 34 | * [alfred2-top-workflow]( https://github.com/zhaocai/alfred2-top-workflow ) 35 | * [alfred2-google-workflow]( https://github.com/zhaocai/alfred2-google-workflow ) 36 | * [alfred2-keylue-workflow]( https://github.com/zhaocai/alfred2-keylue-workflow ) 37 | * [alfred2-sourcetree-workflow]( https://github.com/zhaocai/alfred2-sourcetree-workflow ) 38 | 39 | 40 | ## UPGRADE GUIDE 41 | 42 | ### From version 1.0+ to 2.0+ 43 | 44 | 1. cached feedback are saved and closed automatically, call to `put_cached_feedback` is not required. 45 | 46 | 47 | 48 | ## SYNOPSIS: 49 | 50 | ### The Basic 51 | ```ruby 52 | require 'rubygems' unless defined? Gem 53 | require "bundle/bundler/setup" 54 | require "alfred" 55 | 56 | Alfred.with_friendly_error do |alfred| 57 | fb = alfred.feedback 58 | 59 | fb.add_file_item(File.expand_path "~/Applications/") 60 | 61 | puts fb.to_alfred(ARGV) 62 | end 63 | ``` 64 | 65 | Code are wrapped in `Alfred.with_friendly_error` block. Exceptions and debug messages are logged to Console log file **~/Library/Logs/Alfred-Workflow.log**. 66 | 67 | ### With rescue feedback automatically generated! 68 | 69 | ```ruby 70 | require 'rubygems' unless defined? Gem 71 | require "bundle/bundler/setup" 72 | require "alfred" 73 | 74 | def my_code_with_something_goes_wrong 75 | true 76 | end 77 | 78 | Alfred.with_friendly_error do |alfred| 79 | alfred.with_rescue_feedback = true 80 | 81 | fb = alfred.feedback 82 | 83 | if my_code_with_something_goes_wrong 84 | raise Alfred::NoBundleIDError, "Wrong Bundle ID Test!" 85 | end 86 | end 87 | ``` 88 | 89 | ![](https://raw.github.com/zhaocai/alfred2-ruby-template/master/screenshots/rescue%20feedback.png) 90 | 91 | ### Automate saving and loading cached feedback 92 | ```ruby 93 | require 'rubygems' unless defined? Gem 94 | require "bundle/bundler/setup" 95 | require "alfred" 96 | 97 | Alfred.with_friendly_error do |alfred| 98 | alfred.with_rescue_feedback = true 99 | alfred.with_cached_feedback do 100 | # expire in 1 hour 101 | use_cache_file :expire => 3600 102 | # use_cache_file :file => "/path/to/your/cache_file", :expire => 3600 103 | end 104 | 105 | if fb = alfred.feedback.get_cached_feedback 106 | # cached feedback is valid 107 | puts fb.to_alfred 108 | else 109 | fb = alfred.feedback 110 | # ... generate_feedback as usually 111 | fb.put_cached_feedback 112 | end 113 | end 114 | ``` 115 | 116 | ### Customize feedback item matcher 117 | 118 | ```ruby 119 | fb = alfred.feedback 120 | fb.add_item(:uid => "uid" , 121 | :arg => "arg" , 122 | :autocomplete => "autocomplete" , 123 | :title => "Title" , 124 | :subtitle => "Subtitle" , 125 | :match? => :all_title_match?) 126 | 127 | fb.add_file_item(File.expand_path "~/Applications/", :match? => :all_title_match?) 128 | ``` 129 | 130 | `:title_match?` and `:all_title_match?` are built in. 131 | 132 | To define your new matcher 133 | ```ruby 134 | Module Alfred 135 | class Feedback 136 | class Item 137 | # define new matcher function here 138 | def your_match?(query) 139 | return true 140 | end 141 | end 142 | end 143 | end 144 | ``` 145 | 146 | Check the code in [alfred/feedback/item.rb]( https://github.com/zhaocai/alfred-workflow/blob/master/lib/alfred/feedback/item.rb#L63 ) for more information. 147 | 148 | 149 | 150 | ## Troubleshooting 151 | 152 | 1. ruby crashes 153 | 154 | One of the major reason for ruby crash is native extensions. Check the file `bundle/bundler/setup.rb` under the workflow folder; make sure it does not mixed up with [rvm](https://rvm.io/) like this: 155 | 156 | ```ruby 157 | # ...... 158 | $:.unshift File.expand_path("#{path}/../../../../../../../../.rvm/gems/ruby-2.0.0-p247@global/gems/plist-3.1.0/lib") 159 | $:.unshift File.expand_path("#{path}/../#{ruby_engine}/#{ruby_version}/gems/alfred-workflow-1.11.3/lib") 160 | $:.unshift File.expand_path("#{path}/../../../../../../../../.rvm/gems/ruby-2.0.0-p247@global/gems/json-1.8.0/lib") 161 | ``` 162 | 163 | 164 | 165 | ## DEVELOPERS: 166 | 167 | After checking out the source, run: 168 | 169 | $ rake newb 170 | 171 | This task will install any missing dependencies, run the tests/specs, 172 | and generate the RDoc. 173 | 174 | ## LICENSE: 175 | 176 | Copyright (c) 2013 Zhao Cai 177 | 178 | This program is free software: you can redistribute it and/or modify it under 179 | the terms of the GNU General Public License as published by the Free Software 180 | Foundation, either version 3 of the License, or (at your option) 181 | any later version. 182 | 183 | This program is distributed in the hope that it will be useful, but WITHOUT 184 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 185 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 186 | 187 | You should have received a copy of the GNU General Public License along with 188 | this program. If not, see . 189 | 190 | 191 | [gembundler]: http://gembundler.com/ 192 | [alfredapp]: http://www.alfredapp.com 193 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'rubygems' 4 | require 'hoe' 5 | require 'rake/clean' 6 | 7 | 8 | Hoe.plugin :git 9 | Hoe.plugin :gemspec 10 | Hoe.plugin :version 11 | Hoe.plugin :yard 12 | Hoe.plugin :travis 13 | 14 | Hoe.spec 'alfred-workflow' do 15 | 16 | developer 'Zhao Cai', 'caizhaoff@gmail.com' 17 | 18 | license 'GPL-3' 19 | 20 | extra_deps << ['plist', '>= 3.1.0'] 21 | extra_deps << ['moneta', '>= 0.7.19'] 22 | extra_deps << ['gyoku', '>= 1.1.0'] << ['nori', '>= 2.3.0'] 23 | extra_deps << ['fuzzy_match', '>= 2.0.4'] 24 | extra_deps << ['terminal-notifier', '>= 1.5.0'] 25 | 26 | 27 | extra_dev_deps << ['awesome_print', '>= 1.2.0'] 28 | extra_dev_deps << ['rspec', '>= 2.13'] 29 | extra_dev_deps << ['facets', '>= 2.9.0'] 30 | extra_dev_deps << ['rake', '>= 10.0.0'] 31 | extra_dev_deps << ['hoe'] << ['hoe-gemspec'] << ['hoe-git'] << ['hoe-version'] << ['hoe-yard'] << ['hoe-travis'] 32 | extra_dev_deps << ['guard'] << ['guard-rspec'] << ['guard-bundler'] 33 | extra_dev_deps << ['terminal-notifier-guard'] << ['growl'] 34 | 35 | end 36 | 37 | %w{major minor patch}.each { |v| 38 | desc "Bump #{v.capitalize} Version" 39 | task "bump:#{v}", [:message] => ["version:bump:#{v}"] do |t, args| 40 | m = args[:message] ? args[:message] : "Bump version to #{ENV["VERSION"]}" 41 | sh "git commit -am '#{m}'" 42 | end 43 | } 44 | 45 | 46 | desc "automate guard rspec" 47 | task :guard do 48 | sh %q{bundle exec guard --group=singleruby} 49 | end 50 | 51 | desc "multirubies" 52 | task :multirubies do 53 | sh %q{bundle exec guard --group=multirubies} 54 | end 55 | 56 | CLOBBER.include('log') 57 | CLEAN.include('tmp') 58 | CLEAN.include('test/workflow/tmp') 59 | 60 | 61 | # vim: syntax=ruby 62 | -------------------------------------------------------------------------------- /alfred-workflow.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "alfred-workflow" 5 | s.version = "2.0.1.20131023093234" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Zhao Cai"] 9 | s.date = "2013-10-23" 10 | s.description = "alfred-workflow is a ruby Gem helper for building [Alfred](http://www.alfredapp.com) workflow." 11 | s.email = ["caizhaoff@gmail.com"] 12 | s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.md", "History.txt"] 13 | s.files = [".gemtest", ".rspec", ".ruby-version", "Gemfile", "Gemfile.lock", "Guardfile", "History.txt", "Manifest.txt", "README.md", "Rakefile", "alfred-workflow.gemspec", "lib/alfred.rb", "lib/alfred/feedback.rb", "lib/alfred/feedback/file_item.rb", "lib/alfred/feedback/item.rb", "lib/alfred/feedback/webloc_item.rb", "lib/alfred/handler.rb", "lib/alfred/handler/autocomplete.rb", "lib/alfred/handler/callback.rb", "lib/alfred/handler/cofig.rb", "lib/alfred/handler/help.rb", "lib/alfred/osx.rb", "lib/alfred/setting.rb", "lib/alfred/ui.rb", "lib/alfred/util.rb", "lib/alfred/version.rb", "spec/alfred/feedback/item_spec.rb", "spec/alfred/feedback_spec.rb", "spec/alfred/setting_spec.rb", "spec/alfred/ui_spec.rb", "spec/alfred_spec.rb", "spec/spec_helper.rb", "test/workflow/info.plist", "test/workflow/setting.yaml"] 14 | s.homepage = "http://zhaocai.github.com/alfred2-ruby-template/" 15 | s.licenses = ["GPL-3"] 16 | s.rdoc_options = ["--title", "TestAlfred::TestWorkflow Documentation", "--quiet"] 17 | s.require_paths = ["lib"] 18 | s.rubyforge_project = "alfred-workflow" 19 | s.rubygems_version = "2.0.3" 20 | s.summary = "alfred-workflow is a ruby Gem helper for building [Alfred](http://www.alfredapp.com) workflow." 21 | 22 | if s.respond_to? :specification_version then 23 | s.specification_version = 4 24 | 25 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 26 | s.add_runtime_dependency(%q, [">= 3.1.0"]) 27 | s.add_runtime_dependency(%q, [">= 0.7.19"]) 28 | s.add_runtime_dependency(%q, [">= 1.1.0"]) 29 | s.add_runtime_dependency(%q, [">= 2.3.0"]) 30 | s.add_runtime_dependency(%q, [">= 2.0.4"]) 31 | s.add_runtime_dependency(%q, [">= 1.5.0"]) 32 | s.add_development_dependency(%q, [">= 0.1.2"]) 33 | s.add_development_dependency(%q, [">= 1.2.0"]) 34 | s.add_development_dependency(%q, [">= 2.13"]) 35 | s.add_development_dependency(%q, [">= 2.9.0"]) 36 | s.add_development_dependency(%q, [">= 10.0.0"]) 37 | s.add_development_dependency(%q, [">= 0"]) 38 | s.add_development_dependency(%q, [">= 0"]) 39 | s.add_development_dependency(%q, [">= 0"]) 40 | s.add_development_dependency(%q, [">= 0"]) 41 | s.add_development_dependency(%q, [">= 0"]) 42 | s.add_development_dependency(%q, [">= 0"]) 43 | s.add_development_dependency(%q, [">= 0"]) 44 | s.add_development_dependency(%q, [">= 0"]) 45 | s.add_development_dependency(%q, [">= 0"]) 46 | s.add_development_dependency(%q, [">= 0"]) 47 | else 48 | s.add_dependency(%q, [">= 3.1.0"]) 49 | s.add_dependency(%q, [">= 0.7.19"]) 50 | s.add_dependency(%q, [">= 1.1.0"]) 51 | s.add_dependency(%q, [">= 2.3.0"]) 52 | s.add_dependency(%q, [">= 2.0.4"]) 53 | s.add_dependency(%q, [">= 1.5.0"]) 54 | s.add_dependency(%q, [">= 0.1.2"]) 55 | s.add_dependency(%q, [">= 1.2.0"]) 56 | s.add_dependency(%q, [">= 2.13"]) 57 | s.add_dependency(%q, [">= 2.9.0"]) 58 | s.add_dependency(%q, [">= 10.0.0"]) 59 | s.add_dependency(%q, [">= 0"]) 60 | s.add_dependency(%q, [">= 0"]) 61 | s.add_dependency(%q, [">= 0"]) 62 | s.add_dependency(%q, [">= 0"]) 63 | s.add_dependency(%q, [">= 0"]) 64 | s.add_dependency(%q, [">= 0"]) 65 | s.add_dependency(%q, [">= 0"]) 66 | s.add_dependency(%q, [">= 0"]) 67 | s.add_dependency(%q, [">= 0"]) 68 | s.add_dependency(%q, [">= 0"]) 69 | end 70 | else 71 | s.add_dependency(%q, [">= 3.1.0"]) 72 | s.add_dependency(%q, [">= 0.7.19"]) 73 | s.add_dependency(%q, [">= 1.1.0"]) 74 | s.add_dependency(%q, [">= 2.3.0"]) 75 | s.add_dependency(%q, [">= 2.0.4"]) 76 | s.add_dependency(%q, [">= 1.5.0"]) 77 | s.add_dependency(%q, [">= 0.1.2"]) 78 | s.add_dependency(%q, [">= 1.2.0"]) 79 | s.add_dependency(%q, [">= 2.13"]) 80 | s.add_dependency(%q, [">= 2.9.0"]) 81 | s.add_dependency(%q, [">= 10.0.0"]) 82 | s.add_dependency(%q, [">= 0"]) 83 | s.add_dependency(%q, [">= 0"]) 84 | s.add_dependency(%q, [">= 0"]) 85 | s.add_dependency(%q, [">= 0"]) 86 | s.add_dependency(%q, [">= 0"]) 87 | s.add_dependency(%q, [">= 0"]) 88 | s.add_dependency(%q, [">= 0"]) 89 | s.add_dependency(%q, [">= 0"]) 90 | s.add_dependency(%q, [">= 0"]) 91 | s.add_dependency(%q, [">= 0"]) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/alfred.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' unless defined? Gem # rubygems is only needed in 1.8 2 | 3 | require 'plist' 4 | require 'fileutils' 5 | require 'yaml' 6 | require 'optparse' 7 | require 'ostruct' 8 | require 'gyoku' 9 | require 'nori' 10 | 11 | require 'alfred/ui' 12 | require 'alfred/feedback' 13 | require 'alfred/setting' 14 | require 'alfred/handler/help' 15 | 16 | module Alfred 17 | 18 | class AlfredError < RuntimeError 19 | def self.status_code(code) 20 | define_method(:status_code) { code } 21 | end 22 | end 23 | 24 | class ObjCError < AlfredError; status_code(1) ; end 25 | class NoBundleIDError < AlfredError; status_code(2) ; end 26 | class InvalidArgument < AlfredError; status_code(10) ; end 27 | class InvalidFormat < AlfredError; status_code(11) ; end 28 | class NoMethodError < AlfredError; status_code(13) ; end 29 | class PathError < AlfredError; status_code(14) ; end 30 | 31 | class << self 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 | # 47 | def with_friendly_error(alfred_core = nil, &blk) 48 | begin 49 | if alfred_core.nil? or !alfred_core.is_a?(::Alfred::Core) 50 | alfred = Alfred::Core.new 51 | end 52 | rescue Exception => e 53 | log_file = File.expand_path("~/Library/Logs/Alfred-Workflow.log") 54 | rescue_feedback = %Q{ 55 | 56 | 57 | Alfred Gem Fail to Initialize. 58 | Alfred::NoBundleIDError: Wrong Bundle ID Test! 59 | Check log #{log_file} for extra debug info. 60 | /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns 61 | 62 | 63 | Alfred-Workflow.log 64 | #{log_file} 65 | #{log_file} 66 | /Applications/Utilities/Console.app 67 | 68 | 69 | } 70 | puts rescue_feedback 71 | 72 | File.open(log_file, "a+") do |log| 73 | log.puts "Alfred Gem Fail to Initialize.\n #{e.message}" 74 | log.puts e.backtrace.join(" \n") 75 | log.flush 76 | end 77 | 78 | exit e.status_code 79 | end 80 | 81 | begin 82 | yield alfred 83 | alfred.start_handler 84 | 85 | rescue AlfredError => e 86 | alfred.ui.error e.message 87 | alfred.ui.debug e.backtrace.join("\n") 88 | puts alfred.rescue_feedback( 89 | :title => "#{e.class}: #{e.message}") if alfred.with_rescue_feedback 90 | exit e.status_code 91 | rescue Interrupt => e 92 | alfred.ui.error "\nQuitting..." 93 | alfred.ui.debug e.backtrace.join("\n") 94 | puts alfred.rescue_feedback( 95 | :title => "Interrupt: #{e.message}") if alfred.with_rescue_feedback 96 | exit 1 97 | rescue SystemExit => e 98 | puts alfred.rescue_feedback( 99 | :title => "SystemExit: #{e.status}") if alfred.with_rescue_feedback 100 | alfred.ui.error e.message 101 | alfred.ui.debug e.backtrace.join("\n") 102 | exit e.status 103 | rescue Exception => e 104 | alfred.ui.error( 105 | "A fatal error has occurred. " \ 106 | "You may seek help in the Alfred supporting site, "\ 107 | "forum or raise an issue in the bug tracking site.\n" \ 108 | " #{e.inspect}\n #{e.backtrace.join(" \n")}\n") 109 | puts alfred.rescue_feedback( 110 | :title => "Fatal Error!") if alfred.with_rescue_feedback 111 | exit(-1) 112 | end 113 | end 114 | 115 | 116 | def workflow_folder 117 | Dir.pwd 118 | end 119 | 120 | 121 | # launch alfred with query 122 | def search(query = "") 123 | %x{osascript <<__APPLESCRIPT__ 124 | tell application "Alfred 2" 125 | search "#{query.gsub('"','\"')}" 126 | end tell 127 | __APPLESCRIPT__} 128 | end 129 | 130 | def front_appname 131 | %x{osascript <<__APPLESCRIPT__ 132 | name of application (path to frontmost application as text) 133 | __APPLESCRIPT__}.chop 134 | end 135 | 136 | def front_appid 137 | %x{osascript <<__APPLESCRIPT__ 138 | id of application (path to frontmost application as text) 139 | __APPLESCRIPT__}.chop 140 | end 141 | 142 | end 143 | 144 | class Core 145 | attr_accessor :with_rescue_feedback, :with_help_feedback 146 | attr_accessor :cached_feedback_reload_option 147 | 148 | attr_reader :handler_controller 149 | attr_reader :query, :raw_query 150 | 151 | 152 | def initialize(&blk) 153 | @with_rescue_feedback = true 154 | @with_help_feedback = false 155 | @cached_feedback_reload_option = { 156 | :use_reload_option => false, 157 | :use_exclamation_mark => false 158 | } 159 | 160 | @query = ARGV 161 | @raw_query = ARGV.dup 162 | 163 | @handler_controller = ::Alfred::Handler::Controller.new 164 | 165 | instance_eval(&blk) if block_given? 166 | 167 | raise NoBundleIDError unless bundle_id 168 | end 169 | 170 | 171 | def debug? 172 | ui.level >= LogUI::WARN 173 | end 174 | 175 | # 176 | # Main loop to work with handlers 177 | # 178 | def start_handler 179 | 180 | if @with_help_feedback 181 | ::Alfred::Handler::Help.new(self, :with_handler_help => true).register 182 | end 183 | 184 | return if @handler_controller.empty? 185 | 186 | # step 1: register option parser for handlers 187 | @handler_controller.each do |handler| 188 | handler.on_parser 189 | end 190 | 191 | begin 192 | query_parser.parse! 193 | rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e 194 | ui.warn( 195 | "Fail to parse user query.\n" \ 196 | " #{e.inspect}\n #{e.backtrace.join(" \n")}\n") if debug? 197 | end 198 | 199 | if @cached_feedback_reload_option[:use_exclamation_mark] && !options.should_reload_cached_feedback 200 | if ARGV[0].eql?('!') 201 | ARGV.shift 202 | options.should_reload_cached_feedback = true 203 | elsif ARGV[-1].eql?('!') 204 | ARGV.delete_at(-1) 205 | options.should_reload_cached_feedback = true 206 | end 207 | end 208 | 209 | @query = ARGV 210 | 211 | # step 2: dispatch options to handler for FEEDBACK or ACTION 212 | case options.workflow_mode 213 | when :feedback 214 | @handler_controller.each_handler do |handler| 215 | handler.on_feedback 216 | end 217 | 218 | puts feedback.to_alfred(@query) 219 | when :action 220 | arg = @query 221 | if @query.length == 1 222 | if hsh = xml_parser(@query[0]) 223 | arg = hsh 224 | end 225 | end 226 | 227 | if arg.is_a?(Hash) 228 | @handler_controller.each_handler do |handler| 229 | handler.on_action(arg) 230 | end 231 | else 232 | #fallback default action 233 | arg.each do |a| 234 | if File.exist? a 235 | %x{open "#{a}"} 236 | end 237 | end 238 | end 239 | else 240 | raise InvalidArgument, "#{options.workflow_mode} mode is not supported." 241 | end 242 | 243 | # step 3: close 244 | close 245 | @handler_controller.each_handler do |handler| 246 | handler.on_close 247 | end 248 | 249 | end 250 | 251 | def close 252 | @feedback.close if @feedback 253 | @setting.close if @setting 254 | # @workflow_setting.close if @workflow_setting 255 | end 256 | 257 | 258 | # 259 | # User query without reload options 260 | # 261 | def user_query 262 | q = @raw_query.dup 263 | 264 | if cached_feedback? 265 | if @cached_feedback_reload_option[:use_exclamation_mark] 266 | if q[0].eql?('!') 267 | q.shift 268 | elsif q[-1].eql?('!') 269 | q.delete_at(-1) 270 | end 271 | end 272 | 273 | if @cached_feedback_reload_option[:use_reload_option] 274 | q.delete_if do |v| 275 | ['-r', '--reload'].include? v 276 | end 277 | end 278 | end 279 | 280 | q 281 | end 282 | 283 | # 284 | # Parse and return user query to three parts 285 | # 286 | # [ [before], last option, tail ] 287 | # 288 | def last_option 289 | (@raw_query.size - 1).downto(0) do |i| 290 | if @raw_query[i].start_with? '-' 291 | if @raw_query[i] == @raw_query[-1] 292 | return @raw_query[0...i], '', @raw_query[i] 293 | else 294 | return @raw_query[0..i], @raw_query[i], @raw_query[(i + 1)..-1].join(' ') 295 | end 296 | end 297 | end 298 | 299 | return [], '', @raw_query.join(' ') 300 | end 301 | 302 | 303 | 304 | def options(opts = {}) 305 | @options ||= OpenStruct.new(opts) 306 | end 307 | 308 | def query_parser 309 | @query_parser ||= init_query_parser 310 | end 311 | 312 | def xml_parser(xml) 313 | @xml_parser ||= Nori.new(:parser => :rexml, 314 | :convert_tags_to => lambda { |tag| tag.to_sym }) 315 | begin 316 | hsh = @xml_parser.parse(xml) 317 | return hsh[:root] 318 | rescue REXML::ParseException, Nokogiri::XML::SyntaxError 319 | return nil 320 | end 321 | end 322 | 323 | def xml_builder(arg) 324 | Gyoku.xml(:root => arg) 325 | end 326 | 327 | def ui 328 | @ui ||= LogUI.new(bundle_id) 329 | end 330 | 331 | 332 | # 333 | # workflow setting is stored in the workflow_folder 334 | # 335 | def workflow_setting(opts = {}) 336 | @workflow_setting ||= new_setting(opts) 337 | end 338 | 339 | # 340 | # user setting is stored in the storage_path by default 341 | # 342 | def user_setting(&blk) 343 | @setting ||= new_setting( 344 | :file => File.join(storage_path, "setting.yaml") 345 | ) 346 | end 347 | alias_method :setting, :user_setting 348 | 349 | 350 | def feedback(opts = {}, &blk) 351 | @feedback ||= new_feedback(opts, &blk) 352 | end 353 | 354 | alias_method :with_cached_feedback, :feedback 355 | 356 | def info_plist 357 | @info_plist ||= Plist::parse_xml('info.plist') 358 | end 359 | 360 | # Returns nil if not set. 361 | def bundle_id 362 | @bundle_id ||= info_plist['bundleid'] unless info_plist['bundleid'].empty? 363 | end 364 | 365 | def volatile_storage_path 366 | path = "#{ENV['HOME']}/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/#{bundle_id}" 367 | unless File.directory?(path) 368 | FileUtils.mkdir_p(path) 369 | end 370 | path 371 | end 372 | 373 | # Non-volatile storage directory for this bundle 374 | def storage_path 375 | path = "#{ENV['HOME']}/Library/Application Support/Alfred 2/Workflow Data/#{bundle_id}" 376 | unless File.exist?(path) 377 | FileUtils.mkdir_p(path) 378 | end 379 | path 380 | end 381 | 382 | 383 | def cached_feedback? 384 | @cached_feedback_reload_option.values.any? 385 | end 386 | 387 | 388 | def rescue_feedback(opts = {}) 389 | default_opts = { 390 | :title => "Failed Query!" , 391 | :subtitle => "Check log #{ui.log_file} for extra debug info." , 392 | :uid => 'Rescue Feedback' , 393 | :valid => 'no' , 394 | :autocomplete => '' , 395 | :icon => Feedback.CoreServicesIcon('AlertStopIcon') 396 | } 397 | if @with_help_feedback 398 | default_opts[:autocomplete] = '-h' 399 | end 400 | opts = default_opts.update(opts) 401 | 402 | items = [] 403 | items << Feedback::Item.new(opts[:title], opts) 404 | log_item = Feedback::FileItem.new(ui.log_file) 405 | log_item.uid = nil 406 | items << log_item 407 | 408 | feedback.to_alfred('', items) 409 | end 410 | 411 | def on_help 412 | reload_help_item 413 | end 414 | 415 | 416 | def new_feedback(opts, &blk) 417 | ::Alfred::Feedback.new(self, opts, &blk) 418 | end 419 | 420 | 421 | def new_setting(opts) 422 | default_opts = { 423 | :file => File.join(Alfred.workflow_folder, "setting.yaml"), 424 | :format => 'yaml', 425 | } 426 | opts = default_opts.update(opts) 427 | 428 | ::Alfred::Setting.new(self) do 429 | @backend_file = opts[:file] 430 | @formt = opts[:format] 431 | end 432 | end 433 | 434 | private 435 | 436 | def reload_help_item 437 | title = [] 438 | if @cached_feedback_reload_option[:use_exclamation_mark] 439 | title.push "!" 440 | end 441 | 442 | if @cached_feedback_reload_option[:use_reload_option] 443 | title.push "-r, --reload" 444 | end 445 | 446 | unless title.empty? 447 | return { 448 | :kind => 'text', 449 | :order => (Handler::HelpItem::Base_Order * 10), 450 | :title => "#{title.join(', ')} [Reload cached feedback unconditionally]" , 451 | :subtitle => %q{The '!' mark must be at the beginning or end of the query.} , 452 | } 453 | else 454 | return nil 455 | end 456 | end 457 | 458 | def init_query_parser 459 | options.workflow_mode = :feedback 460 | options.modifier = :none 461 | options.should_reload_cached_feedback = false 462 | 463 | modifiers = [:command, :alt, :control, :shift, :fn, :none] 464 | OptionParser.new do |opts| 465 | opts.separator "" 466 | opts.separator "Built-in Options:" 467 | 468 | opts.on("--workflow-mode [TYPE]", [:feedback, :action], 469 | "Alfred handler working mode (feedback, action)") do |t| 470 | options.workflow_mode = t 471 | end 472 | 473 | opts.on("--modifier [MODIFIER]", modifiers, 474 | "Alfred action modifier (#{modifiers})") do |t| 475 | options.modifier = t 476 | end 477 | 478 | if @cached_feedback_reload_option[:use_reload_option] 479 | opts.on("-r", "--reload", "Reload cached feedback") do 480 | options.should_reload_cached_feedback = true 481 | end 482 | end 483 | opts.separator "" 484 | opts.separator "Handler Options:" 485 | end 486 | 487 | end 488 | end 489 | end 490 | 491 | -------------------------------------------------------------------------------- /lib/alfred/feedback.rb: -------------------------------------------------------------------------------- 1 | require "rexml/document" 2 | require 'alfred/feedback/item' 3 | require 'alfred/feedback/file_item' 4 | require 'alfred/feedback/webloc_item' 5 | 6 | module Alfred 7 | 8 | class Feedback 9 | attr_accessor :items 10 | attr_reader :backend_file 11 | 12 | def initialize(alfred, opts = {}, &blk) 13 | @items = [] 14 | @core = alfred 15 | use_backend(opts) 16 | instance_eval(&blk) if block_given? 17 | 18 | end 19 | 20 | def add_item(opts = {}) 21 | raise ArgumentError, "Feedback item must have title!" if opts[:title].nil? 22 | @items << Item.new(opts[:title], opts) 23 | end 24 | 25 | def add_file_item(path, opts = {}) 26 | @items << FileItem.new(path, opts) 27 | end 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 | 36 | def to_xml(with_query = '', items = @items) 37 | document = REXML::Element.new("items") 38 | @items.sort! 39 | 40 | if with_query.empty? 41 | items.each do |item| 42 | document << item.to_xml 43 | end 44 | else 45 | items.each do |item| 46 | document << item.to_xml if item.match?(with_query) 47 | end 48 | end 49 | document.to_s 50 | end 51 | 52 | alias_method :to_alfred, :to_xml 53 | 54 | # 55 | # Merge with other feedback 56 | # 57 | def merge!(other) 58 | if other.is_a? Array 59 | @items |= other 60 | elsif other.is_a? Alfred::Feedback 61 | @items |= other.items 62 | else 63 | raise ArgumentError, "Feedback can not merge with #{other.class}" 64 | end 65 | end 66 | 67 | # 68 | # The workflow is about to complete 69 | # 70 | # - save cached feedback if necessary 71 | # 72 | def close 73 | put_cached_feedback if @backend_file 74 | end 75 | 76 | # 77 | # ## helper class method for icon 78 | # 79 | def self.CoreServicesIcon(name) 80 | { 81 | :type => "default" , 82 | :name => "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/#{name}.icns" 83 | } 84 | end 85 | 86 | def self.Icon(name) 87 | { 88 | :type => "default" , 89 | :name => name , 90 | } 91 | end 92 | def self.FileIcon(path) 93 | { 94 | :type => "fileicon" , 95 | :name => path , 96 | } 97 | end 98 | 99 | 100 | # 101 | # ## serialization 102 | # 103 | 104 | def use_backend(opts = {}) 105 | @backend_file = opts[:file] if opts[:file] 106 | @should_expire_after_second = opts[:expire].to_i if opts[:expire] 107 | end 108 | alias_method :use_cache_file, :use_backend 109 | 110 | def backend_file 111 | @backend_file ||= File.join(@core.volatile_storage_path, "cached_feedback") 112 | end 113 | 114 | def expired? 115 | return false unless @should_expire_after_second 116 | Time.now - File.ctime(backend_file) > @should_expire_after_second 117 | end 118 | 119 | def get_cached_feedback 120 | return nil unless File.exist?(backend_file) 121 | return nil if expired? 122 | 123 | load(@backend_file) 124 | self 125 | end 126 | 127 | def put_cached_feedback 128 | dump(backend_file) 129 | end 130 | 131 | def dump(to_file) 132 | File.open(to_file, "wb") { |f| Marshal.dump(@items, f) } 133 | end 134 | 135 | def load(from_file) 136 | @items = File.open(from_file, "rb") { |f| Marshal.load(f) } 137 | end 138 | 139 | def append(from_file) 140 | @items << File.open(from_file, "rb") { |f| Marshal.load(f) } 141 | end 142 | 143 | # 144 | # Provides yaml serialization support 145 | # 146 | if RUBY_VERSION < "1.9" 147 | def to_yaml_properties 148 | [ '@items' ] 149 | end 150 | else 151 | def encode_with(coder) 152 | coder['items'] = @items 153 | end 154 | end 155 | 156 | # 157 | # Provides marshalling support for use by the Marshal library. 158 | # 159 | def marshal_dump 160 | @items 161 | end 162 | 163 | # 164 | # Provides marshalling support for use by the Marshal library. 165 | # 166 | def marshal_load(x) 167 | @items = x 168 | end 169 | 170 | end 171 | 172 | end 173 | -------------------------------------------------------------------------------- /lib/alfred/feedback/file_item.rb: -------------------------------------------------------------------------------- 1 | require "rexml/document" 2 | require "alfred/feedback/item" 3 | 4 | module Alfred 5 | class Feedback 6 | class FileItem < Item 7 | 8 | def initialize(path, opts = {}) 9 | if opts[:title] 10 | @title = opts[:title] 11 | elsif ['.ennote', '.webbookmark', '.vcf', '.abcdp', '.olk14Contact'].include? File.extname(path) 12 | @title = %x{/usr/bin/mdls -name kMDItemDisplayName -raw '#{path}'} 13 | else 14 | @title = File.basename(path) 15 | end 16 | @subtitle = path 17 | @uid = path 18 | @arg = path 19 | @icon = {:type => "fileicon", :name => path} 20 | @valid = 'yes' 21 | @autocomplete = @title 22 | @type = 'file' 23 | 24 | super @title, opts 25 | end 26 | 27 | end 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /lib/alfred/feedback/item.rb: -------------------------------------------------------------------------------- 1 | require "rexml/document" 2 | 3 | module Alfred 4 | class Feedback 5 | class Item 6 | attr_accessor :uid, :arg, :valid, :autocomplete, :title, :subtitle, :icon, :type 7 | attr_accessor :order 8 | 9 | Default_Order = 256 10 | 11 | def initialize(title, opts = {}) 12 | @title = title 13 | @subtitle = opts[:subtitle] if opts[:subtitle] 14 | 15 | if opts[:icon] 16 | @icon = opts[:icon] 17 | else 18 | @icon ||= {:type => "default", :name => "icon.png"} 19 | end 20 | 21 | if opts[:uid] 22 | @uid = opts[:uid] 23 | end 24 | 25 | if opts[:arg] 26 | @arg = opts[:arg] 27 | else 28 | @arg ||= @title 29 | end 30 | 31 | if opts[:type] 32 | @type = opts[:type] 33 | else 34 | @type ||= 'default' 35 | end 36 | 37 | if opts[:valid] 38 | @valid = opts[:valid] 39 | else 40 | @valid ||= 'yes' 41 | end 42 | 43 | if opts[:autocomplete] 44 | @autocomplete = opts[:autocomplete] 45 | end 46 | 47 | if opts[:match?] 48 | @matcher = opts[:match?].to_sym 49 | else 50 | @matcher ||= :title_match? 51 | end 52 | 53 | if opts[:order] 54 | @order = opts[:order] 55 | else 56 | @order = Default_Order 57 | end 58 | end 59 | 60 | 61 | # sort function 62 | def <=>(other) 63 | @order <=> other.order 64 | end 65 | 66 | 67 | ## To customize a new matcher?, define it. 68 | # 69 | # Module Alfred 70 | # class Feedback 71 | # class Item 72 | # def your_match?(query) 73 | # # define new matcher here 74 | # end 75 | # end 76 | # end 77 | # end 78 | def match?(query) 79 | send(@matcher, query) 80 | end 81 | 82 | # 83 | # Matchers 84 | # 85 | def always_match?(query) 86 | true 87 | end 88 | 89 | def title_match?(query) 90 | return true if query.empty? 91 | if smartcase_query(query).match(@title) 92 | return true 93 | else 94 | return false 95 | end 96 | end 97 | 98 | def all_title_match?(query) 99 | return true if query.empty? 100 | if query.is_a? String 101 | query = query.split("\s") 102 | end 103 | 104 | queries = [] 105 | query.each { |q| 106 | queries << smartcase_query(q) 107 | } 108 | 109 | queries.delete_if { |q| 110 | q.match(@title) or q.match(@subtitle) 111 | } 112 | 113 | if queries.empty? 114 | return true 115 | else 116 | return false 117 | end 118 | end 119 | 120 | 121 | def to_xml 122 | xml_element = REXML::Element.new('item') 123 | if @uid 124 | xml_element.add_attributes({ 125 | 'uid' => @uid, 126 | 'valid' => @valid, 127 | 'autocomplete' => @autocomplete 128 | }) 129 | else 130 | xml_element.add_attributes({ 131 | 'valid' => @valid, 132 | 'autocomplete' => @autocomplete 133 | }) 134 | 135 | end 136 | xml_element.add_attributes('type' => 'file') if @type == "file" 137 | 138 | REXML::Element.new("title", xml_element).text = @title 139 | REXML::Element.new("arg", xml_element).text = @arg 140 | REXML::Element.new("subtitle", xml_element).text = @subtitle 141 | 142 | icon = REXML::Element.new("icon", xml_element) 143 | icon.text = @icon[:name] 144 | icon.add_attributes('type' => 'fileicon') if @icon[:type] == "fileicon" 145 | 146 | xml_element 147 | end 148 | 149 | protected 150 | 151 | # 152 | # Regex helpers 153 | # 154 | def build_regexp(query, option) 155 | begin 156 | Regexp.compile(".*#{query.gsub(/\s+/,'.*')}.*", option) 157 | rescue RegexpError 158 | Regexp.compile(".*#{Regexp.escape(query)}.*", option) 159 | end 160 | end 161 | 162 | def smartcase_query(query) 163 | if query.is_a? Array 164 | query = query.join(" ") 165 | end 166 | option = Regexp::IGNORECASE 167 | if /[[:upper:]]/.match(query) 168 | option = nil 169 | end 170 | build_regexp(query, option) 171 | end 172 | 173 | def ignorecase_query(query) 174 | if query.is_a? Array 175 | query = query.join(" ") 176 | end 177 | option = Regexp::IGNORECASE 178 | build_regexp(query, option) 179 | end 180 | 181 | def default_query(query) 182 | if query.is_a? Array 183 | query = query.join(" ") 184 | end 185 | build_regexp(query, nil) 186 | end 187 | 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /lib/alfred/feedback/webloc_item.rb: -------------------------------------------------------------------------------- 1 | require "alfred/feedback/file_item" 2 | require 'alfred/util' 3 | 4 | module Alfred 5 | class Feedback 6 | class WeblocItem < FileItem 7 | 8 | def initialize(title, opts = {}) 9 | unless File.exist? opts[:webloc] 10 | opts[:webloc] = ::Alfred::Util.make_webloc( 11 | opts[:title], opts[:url], opts[:folder]) 12 | end 13 | 14 | @subtitle = opts[:url] 15 | @uid = opts[:url] 16 | 17 | super title, opts 18 | end 19 | 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/alfred/handler.rb: -------------------------------------------------------------------------------- 1 | require 'alfred/util' 2 | 3 | require 'set' 4 | require "rexml/document" 5 | 6 | module Alfred 7 | 8 | module Handler 9 | class Base 10 | Base_Invoke_Order = 100 11 | 12 | attr_reader :status, :order 13 | 14 | def initialize(alfred, opts = {}) 15 | @core = alfred 16 | @order = Base_Invoke_Order 17 | @status = :initialize 18 | end 19 | 20 | 21 | def on_parser 22 | ; 23 | end 24 | 25 | def on_help 26 | [] 27 | end 28 | 29 | def feedback? 30 | true 31 | end 32 | def on_feedback 33 | raise NotImplementedError 34 | end 35 | 36 | def action?(arg) 37 | arg.is_a?(Hash) && arg[:handler].eql?(@settings[:handler]) 38 | end 39 | 40 | def on_action(arg) 41 | ; 42 | end 43 | 44 | def on_close 45 | ; 46 | end 47 | 48 | def register 49 | @core.handler_controller.register(self) 50 | end 51 | 52 | def <=>(other) 53 | order <=> other.order 54 | end 55 | 56 | 57 | def status_message(text, exitstatus) 58 | if exitstatus == 0 59 | return "⭕ #{text}" 60 | else 61 | return "❌ #{text}" 62 | end 63 | end 64 | 65 | 66 | # from alfred core 67 | def xml_builder(arg) 68 | @core.xml_builder(arg) 69 | end 70 | 71 | def options 72 | @core.options 73 | end 74 | 75 | def parser 76 | @core.query_parser 77 | end 78 | 79 | def query 80 | @core.query 81 | end 82 | 83 | def ui 84 | @core.ui 85 | end 86 | 87 | def feedback 88 | @core.feedback 89 | end 90 | end 91 | 92 | 93 | class Controller 94 | ## handlers are called based on handler.order 95 | # 1-10 : critical handler 96 | # 100 : base order 97 | 98 | include Enumerable 99 | 100 | def initialize 101 | @handlers = SortedSet.new 102 | @status = {:break => [:break, :exclusive]} 103 | end 104 | 105 | def register(handler) 106 | raise InvalidArgument unless handler.is_a? ::Alfred::Handler::Base 107 | @handlers.add(handler) 108 | end 109 | 110 | def empty? 111 | @handlers.empty? 112 | end 113 | 114 | def each 115 | return enum_for(__method__) unless block_given? 116 | 117 | @handlers.each do |h| 118 | yield(h) 119 | end 120 | end 121 | 122 | def each_handler 123 | return enum_for(__method__) unless block_given? 124 | 125 | @handlers.each do |h| 126 | yield(h) 127 | break if @status[:break].include?(h.status) 128 | end 129 | end 130 | 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/alfred/handler/autocomplete.rb: -------------------------------------------------------------------------------- 1 | require 'alfred/handler' 2 | require 'fuzzy_match' 3 | 4 | module Alfred 5 | module Handler 6 | 7 | class Autocomplete < Base 8 | def initialize(alfred, opts = {}) 9 | super 10 | @settings = { 11 | :handler => 'Autocomplete' , 12 | :items => {} , 13 | :fuzzy_score => 0.5 , 14 | }.update(opts) 15 | 16 | if @settings[:items].empty? 17 | @load_from_workflow_setting = true 18 | else 19 | @load_from_workflow_setting = false 20 | end 21 | end 22 | 23 | 24 | 25 | def on_feedback 26 | if @load_from_workflow_setting 27 | @settings[:items].merge! @core.workflow_setting[:autocomplete] 28 | end 29 | 30 | before, option, tail = @core.last_option 31 | 32 | base_item ={ 33 | :match? => :always_match? , 34 | :subtitle => "↩ to autocomplete" , 35 | :valid => 'no' , 36 | :icon => ::Alfred::Feedback.CoreServicesIcon('ForwardArrowIcon') , 37 | } 38 | 39 | if @settings[:items].has_key? tail 40 | unify_items(@settings[:items][tail]).each do |item| 41 | base_item[:autocomplete] = "#{(before + [tail, item[:complete]]).join(' ')} " 42 | feedback.add_item(base_item.update(item)) 43 | end 44 | else 45 | add_fuzzy_match_feedback(unify_items(@settings[:items][option]), 46 | before, tail, base_item, feedback) 47 | end 48 | end 49 | 50 | 51 | def add_fuzzy_match_feedback(items, before, query, base_item, to_feedback) 52 | matcher = FuzzyMatch.new(items, :read => :complete) 53 | matcher.find_all_with_score(query).each do |item, dice_similar, leven_similar| 54 | next if item[:complete].size < query.size 55 | 56 | if (item[:complete].start_with?(query) or 57 | dice_similar > @settings[:fuzzy_score] or 58 | leven_similar > @settings[:fuzzy_score]) 59 | 60 | base_item[:autocomplete] = "#{(before + [item[:complete]]).join(' ')} " 61 | to_feedback.add_item(base_item.update(item)) 62 | end 63 | end 64 | end 65 | 66 | def unify_items(items) 67 | return [] unless items 68 | items.map do |item| 69 | if item.is_a? String 70 | {:title => item, :complete => item} 71 | elsif item.is_a? Hash 72 | unless item.has_key? :complete 73 | item[:complete] = item[:title] 74 | end 75 | item 76 | else 77 | raise InvalidArgument, "autocomplete handler can only accept string or hash" 78 | end 79 | end 80 | end 81 | 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/alfred/handler/callback.rb: -------------------------------------------------------------------------------- 1 | require 'moneta' 2 | require 'alfred/util' 3 | 4 | # 5 | # = Alfred Callback Hander 6 | # 7 | # Each callback is stored using Moneta via YAML backend.! 8 | # 9 | # == Example: 10 | # Suppose we have a callback with key "demo" 11 | # 12 | # - @backend[ENTRIES_KEY] => { 13 | # 'demo' => {:key => 'demo', :title => 'title', :subtitle => ...} 14 | # } 15 | # 16 | # - @backend['demo'] => the feedback items 17 | # 18 | module Alfred::Handler 19 | 20 | class Callback < Base 21 | ENTRIES_KEY = 'feedback_entries' 22 | 23 | def initialize(alfred, opts = {}) 24 | super 25 | @settings = { 26 | :handler => 'Callback' , 27 | :exclusive? => true , 28 | :backend_dir => @core.volatile_storage_path , 29 | :backend_file => 'callback.yaml' , 30 | :handler_order => ( Base_Invoke_Order / 12 ) 31 | }.update(opts) 32 | 33 | @order = @settings[:handler_order] 34 | end 35 | 36 | 37 | def on_parser 38 | parser.on("--callback [CALLBACK]", "Alfred callback feedback") do |v| 39 | options.callback = v || '' 40 | end 41 | end 42 | 43 | def feedback? 44 | options.callback 45 | end 46 | 47 | def on_feedback 48 | return unless feedback? 49 | if entries[options.callback] 50 | feedback.merge! backend[options.callback] 51 | @status = :exclusive if @settings[:exclusive?] 52 | 53 | elsif entries.empty? 54 | # show a warn feedback item 55 | feedback.add_item( 56 | { 57 | :title => 'No available callback!' , 58 | :valid => 'no' , 59 | :autocomplete => '' , 60 | :subtitle => 'Please check it later. Background task may still be running.', 61 | :icon => ::Alfred::Feedback.CoreServicesIcon('Unsupported') , 62 | } 63 | ) 64 | else 65 | # list available callbacks 66 | entries.each do |key, entry| 67 | feedback.add_item( 68 | { 69 | :title => "Feedback Callback: #{key}" , 70 | :subtitle => "#{entry[:timestamp]}", 71 | :valid => 'no' , 72 | :autocomplete => "--callback '#{key}'" , 73 | :icon => ::Alfred::Feedback.CoreServicesIcon('AliasBadgeIcon') , 74 | }.merge(entry) 75 | ) 76 | end 77 | @status = :exclusive if @settings[:exclusive?] 78 | end 79 | end 80 | 81 | 82 | def on_close 83 | backend.close 84 | end 85 | 86 | 87 | def on_callback(keyword, entry, feedback_items) 88 | add_entry(entry, feedback_items) 89 | Alfred::Util.notify("#{keyword} --callback '#{entry[:key]}'", 90 | entry[:title] || entry[:key], 91 | entry) 92 | end 93 | 94 | 95 | def add_entry(entry, feedback_items) 96 | entry.merge!(:timestamp => Time.now) 97 | key = entry[:key] 98 | new_entries = entries.merge(key => entry) 99 | backend[ENTRIES_KEY] = new_entries 100 | backend[key] = feedback_items 101 | end 102 | 103 | def remove_entry(key) 104 | new_entries = entries.delete(key) 105 | backend[ENTRIES_KEY] = new_entries 106 | backend.delete(key) 107 | end 108 | 109 | 110 | 111 | def entries 112 | backend[ENTRIES_KEY] 113 | end 114 | 115 | 116 | def backend 117 | @backend ||= Moneta.new(:YAML, 118 | :file => File.join(@settings[:backend_dir], 119 | @settings[:backend_file])) 120 | 121 | unless @backend.key?(ENTRIES_KEY) 122 | @backend[ENTRIES_KEY] = {} 123 | end 124 | @backend 125 | end 126 | 127 | private 128 | 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/alfred/handler/cofig.rb: -------------------------------------------------------------------------------- 1 | module Alfred::Handler 2 | 3 | class Config < Base 4 | def initialize(alfred, opts = {}) 5 | super 6 | @order = 20 7 | @settings = { 8 | :setting => alfred.workflow_setting , 9 | :break? => true , 10 | :handler => 'Config' 11 | }.update(opts) 12 | 13 | end 14 | 15 | def on_parser 16 | opts.on("-c", "--config CONFIG", "Config Workflow Settings") do |v| 17 | options.config = v 18 | end 19 | end 20 | 21 | def on_help 22 | { 23 | :kind => 'text' , 24 | :title => '-c, --config [query]' , 25 | :subtitle => 'Config Workflow Settings' , 26 | } 27 | end 28 | 29 | def on_feedback 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/alfred/handler/help.rb: -------------------------------------------------------------------------------- 1 | require 'alfred/handler' 2 | 3 | 4 | module Alfred 5 | module Handler 6 | 7 | class HelpItem < ::Hash 8 | Base_Order = 10 9 | def initialize(attributes = {}, &block) 10 | super(&block) 11 | initialize_attributes(attributes) 12 | end 13 | 14 | def <=>(other) 15 | self[:order] <=> other[:order] 16 | end 17 | 18 | private 19 | 20 | def initialize_attributes(attributes) 21 | attributes.each_pair do |att, value| 22 | self[att] = value 23 | end if attributes 24 | self[:order] = Base_Order unless self[:order] 25 | end 26 | end 27 | 28 | 29 | class Help < Base 30 | def initialize(alfred, opts = {}) 31 | super 32 | @settings = { 33 | :handler => 'Help' , 34 | :exclusive? => true , 35 | :with_handler_help => true , 36 | :items => [] , 37 | :handler_order => ( Base_Invoke_Order / 10 ) 38 | }.update(opts) 39 | 40 | @order = @settings[:handler_order] 41 | 42 | if @settings[:items].empty? 43 | @load_from_workflow_setting = true 44 | else 45 | @load_from_workflow_setting = false 46 | end 47 | 48 | end 49 | 50 | def on_parser 51 | parser.on_tail('-?', '-h', '--help', 'Workflow Helper') do 52 | options.help = true 53 | end 54 | end 55 | 56 | def on_help 57 | { 58 | :kind => 'text' , 59 | :valid => 'no' , 60 | :autocomplete => '-h' , 61 | :match? => :always_match? , 62 | :order => (HelpItem::Base_Order * 12) , 63 | :title => '-?, -h, --help [Show Workflow Usage Help]' , 64 | :subtitle => 'Other feedbacks are blocked.' , 65 | } 66 | end 67 | 68 | def feedback? 69 | options.help 70 | end 71 | 72 | def on_feedback 73 | return unless feedback? 74 | 75 | if @settings[:with_handler_help] 76 | @settings[:items].push @core.on_help 77 | @core.handler_controller.each do |h| 78 | @settings[:items].push h.on_help 79 | end 80 | end 81 | 82 | if @load_from_workflow_setting 83 | if @core.workflow_setting.has_key?(:help) 84 | @settings[:items].push @core.workflow_setting[:help] 85 | end 86 | end 87 | 88 | @settings[:items].flatten!.compact! 89 | @settings[:items].map! { |i| HelpItem.new(i) }.sort! 90 | 91 | @settings[:items].each do |item| 92 | 93 | case item[:kind] 94 | when 'file' 95 | item[:path] = File.expand_path(item[:path]) 96 | # action is handled by fallback action in the main loop 97 | feedback.add_file_item(item[:path], item) 98 | when 'url' 99 | item[:arg] = xml_builder( 100 | :handler => @settings[:handler] , 101 | :kind => item[:kind] , 102 | :url => item[:url] 103 | ) 104 | 105 | feedback.add_item( 106 | { 107 | :icon => ::Alfred::Feedback.CoreServicesIcon('BookmarkIcon') 108 | }.merge(item) 109 | ) 110 | 111 | when 'text', 'message' 112 | item[:arg] = xml_builder( 113 | { 114 | :handler => @settings[:handler] , 115 | :kind => item[:kind] , 116 | } 117 | ) 118 | 119 | feedback.add_item( 120 | { 121 | :valid => 'no' , 122 | :autocomplete => '' , 123 | :icon => ::Alfred::Feedback.CoreServicesIcon('ClippingText') , 124 | }.merge(item) 125 | ) 126 | 127 | else 128 | if item.has_key? :title 129 | item[:arg] = xml_builder( 130 | { 131 | :handler => @settings[:handler] , 132 | :kind => item[:kind] , 133 | }.merge(item) 134 | ) 135 | 136 | feedback.add_item( 137 | { 138 | :icon => ::Alfred::Feedback.CoreServicesIcon('HelpIcon'), 139 | }.merge(item) 140 | ) 141 | end 142 | end 143 | end 144 | 145 | @status = :exclusive if @settings[:exclusive?] 146 | end 147 | 148 | 149 | def on_action(arg) 150 | return unless action?(arg) 151 | 152 | case arg[:kind] 153 | when 'url' 154 | ::Alfred::Util.open_url(arg[:url]) 155 | when 'file' 156 | %x{open "#{arg[:path]}"} 157 | end 158 | end 159 | end 160 | 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/alfred/osx.rb: -------------------------------------------------------------------------------- 1 | module Alfred 2 | module OSX 3 | 4 | class << self 5 | 6 | def version 7 | @version ||= get_osx_version 8 | end 9 | 10 | def version_number 11 | "#{version[:major]}.#{version[:minor]}" 12 | end 13 | 14 | def notification_center? 15 | version[:major] >= 10.8 16 | end 17 | 18 | 19 | def full_name 20 | "Mac OS X " + version_number 21 | end 22 | 23 | 24 | def short_name 25 | case version[:major] 26 | when 10.4 27 | short_name = "Tiger" 28 | when 10.5 29 | short_name = "Leopard" 30 | when 10.6 31 | short_name = "Snow Leopard" 32 | when 10.7 33 | short_name = "Lion" 34 | when 10.8 35 | short_name = "Mountain Lion" 36 | when 10.9 37 | short_name = "Mavericks" 38 | when 10.10 39 | short_name = "Yosemite" 40 | end 41 | 42 | return short_name 43 | end 44 | 45 | 46 | private 47 | 48 | def get_osx_version 49 | begin 50 | version = %x{/usr/bin/sw_vers -productVersion}.chop 51 | rescue Errno::ENOENT => e 52 | raise Errno::ENOENT, "This computer is not running Mac OS X becasue #{e.message}" 53 | end 54 | 55 | segments = version.split('.')[0,3].map!{|p| p.to_i} 56 | { 57 | :major => "#{segments[0]}.#{segments[1]}".to_f, 58 | :minor => segments[2], 59 | } 60 | end 61 | 62 | end 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /lib/alfred/setting.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Alfred 4 | 5 | class Setting 6 | attr_accessor :backend_file 7 | attr_reader :format 8 | 9 | def initialize(alfred, &block) 10 | @core = alfred 11 | @table = {} 12 | 13 | instance_eval(&block) if block_given? 14 | 15 | @format ||= "yaml" 16 | @backend_file ||= File.join(@core.storage_path, "setting.#{@format}") 17 | 18 | raise InvalidFormat, "#{format} is not suported." unless validate_format 19 | 20 | unless File.exist?(@backend_file) 21 | @table.merge!({:id => @core.bundle_id}) 22 | dump(:flush => true) 23 | else 24 | load 25 | end 26 | end 27 | 28 | 29 | def validate_format 30 | ['yaml'].include?(format) 31 | end 32 | 33 | 34 | def load 35 | send("load_from_#{format}".to_sym) 36 | end 37 | 38 | 39 | def dump(opts = {}) 40 | send("dump_to_#{format}".to_sym, opts) 41 | end 42 | 43 | alias_method :close, :dump 44 | 45 | # 46 | # Provides yaml serialization support 47 | # 48 | if RUBY_VERSION < "1.9" 49 | def to_yaml_properties 50 | [ '@table' ] 51 | end 52 | else 53 | def encode_with(coder) 54 | coder['table'] = @table 55 | end 56 | end 57 | 58 | # 59 | # Provides marshalling support for use by the Marshal library. 60 | # 61 | def marshal_dump 62 | @table 63 | end 64 | 65 | # 66 | # Provides marshalling support for use by the Marshal library. 67 | # 68 | def marshal_load(x) 69 | @table.merge! x 70 | end 71 | # 72 | # Converts to hash 73 | # 74 | def to_h 75 | @table.dup 76 | end 77 | 78 | def each_pair 79 | return to_enum __method__ unless block_given? 80 | @table.each_pair{|p| yield p} 81 | end 82 | 83 | 84 | def [](name) 85 | @table[name] 86 | end 87 | 88 | # 89 | # Sets the value of a member. 90 | # 91 | # person = Alfred::Setting.new('name' => 'John Smith', 'age' => 70) 92 | # person[:age] = 42 93 | # 94 | def []=(name, value) 95 | @table[name] = value 96 | end 97 | 98 | def has_key?(key) 99 | @table.has_key?(key) 100 | end 101 | alias_method :key?, :has_key? 102 | 103 | 104 | def ==(other) 105 | return false unless other.kind_of?(Alfred::Setting) 106 | @table == other.table 107 | end 108 | 109 | def eql?(other) 110 | return false unless other.kind_of?(Alfred::Setting) 111 | @table.eql?(other.table) 112 | end 113 | 114 | attr_reader :table # :nodoc: 115 | protected :table 116 | 117 | 118 | # 119 | # Send missing method to @table to mimic a hash 120 | # 121 | def method_missing (name, *args, &block) # :nodoc: 122 | @table.send(name, *args, &block) 123 | end 124 | 125 | protected 126 | 127 | def load_from_yaml 128 | @table.merge!(YAML::load_file(@backend_file)) 129 | end 130 | 131 | def dump_to_yaml(opts = {}) 132 | File.open(@backend_file, File::WRONLY|File::TRUNC|File::CREAT) { |f| 133 | YAML::dump(@table, f) 134 | f.flush if opts[:flush] 135 | } 136 | end 137 | 138 | end 139 | end 140 | 141 | 142 | -------------------------------------------------------------------------------- /lib/alfred/ui.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'fileutils' 3 | 4 | 5 | module Alfred 6 | 7 | class LogUI < ::Logger 8 | attr_reader :logdev 9 | 10 | def initialize(id, to_file=nil) 11 | if to_file 12 | @log_file = to_file 13 | log_dir = File.dirname(log_file) 14 | FileUtils.mkdir_p log_dir unless File.exists? log_dir 15 | end 16 | 17 | super log_file, 'weekly' 18 | 19 | @progname = id 20 | @default_formatter.datetime_format = '%Y-%m-%d %H:%M:%S ' 21 | end 22 | 23 | def log_file 24 | @log_file ||= File.expand_path("~/Library/Logs/Alfred-Workflow.log") 25 | end 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/alfred/util.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'terminal-notifier' 3 | require 'alfred/osx' 4 | 5 | class String 6 | def strip_heredoc 7 | indent = scan(/^[ \t]*(?=\S)/).min.size || 0 8 | gsub(/^[ \t]{#{indent}}/, '') 9 | end 10 | end 11 | 12 | module Alfred 13 | 14 | module Util 15 | 16 | class << self 17 | # escape text for use in an AppleScript string 18 | def escape_applescript(str) 19 | str.to_s.gsub(/(?=["\\])/, '\\') 20 | end 21 | 22 | def make_webloc(name, url, folder=nil, comment = '') 23 | date = Time.now.strftime("%m-%d-%Y %I:%M%p") 24 | folder = Alfred.workflow_folder unless folder 25 | folder, name, url, comment = [folder, name, url, comment].map do |t| 26 | escape_applescript(t) 27 | end 28 | 29 | return %x{ 30 | osascript << __APPLESCRIPT__ 31 | tell application "Finder" 32 | set webloc to make new internet location file at (POSIX file "#{folder}") ¬ 33 | to "#{url}" with properties ¬ 34 | {name:"#{name}",creation date:(AppleScript's date "#{date}"), ¬ 35 | comment:"#{comment}"} 36 | end tell 37 | return POSIX path of (webloc as string) 38 | __APPLESCRIPT__} 39 | end 40 | 41 | 42 | def open_url(url) 43 | uri = URI.parse(url) 44 | %x{/usr/bin/open #{uri.to_s}} 45 | end 46 | 47 | def google(query) 48 | open_url %Q{http://www.google.com/search?as_q=#{URI.escape(query)}&lr=lang_} 49 | end 50 | 51 | def open_with(app, path) 52 | %x{osascript <<__APPLESCRIPT__ 53 | tell application "#{app}" 54 | try 55 | open "#{path}" 56 | activate 57 | on error err_msg number err_num 58 | return err_msg 59 | end try 60 | end tell 61 | __APPLESCRIPT__} 62 | end 63 | 64 | def reveal_in_finder(path) 65 | raise InvalidArgument, "#{path} does not exist." unless File.exist? path 66 | %x{osascript <<__APPLESCRIPT__ 67 | tell application "Finder" 68 | try 69 | reveal POSIX file "#{path}" 70 | activate 71 | on error err_msg number err_num 72 | return err_msg 73 | end try 74 | end tell 75 | __APPLESCRIPT__} 76 | end 77 | 78 | 79 | def search_command(query = '') 80 | %Q{osascript <<__APPLESCRIPT__ 81 | tell application "Alfred 2" 82 | search "#{escape_applescript(query)}" 83 | end tell 84 | __APPLESCRIPT__} 85 | end 86 | 87 | 88 | def notify(query, message, opts = {}) 89 | if Alfred::OSX.notification_center? 90 | notifier_options = { 91 | :title => 'Alfred Notification' , 92 | :sound => 'default' , 93 | :execute => search_command(query) , 94 | }.merge!(opts) 95 | p notifier_options 96 | TerminalNotifier.notify(message, notifier_options) 97 | else 98 | system search_command(query) 99 | end 100 | end 101 | 102 | end 103 | end 104 | 105 | end 106 | -------------------------------------------------------------------------------- /lib/alfred/version.rb: -------------------------------------------------------------------------------- 1 | module Alfred 2 | VERSION = '2.0.5' 3 | end 4 | -------------------------------------------------------------------------------- /spec/alfred/feedback/item_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Feedback Item" do 4 | it "should return raise ArgumentError without Item title" do 5 | expect { Alfred::Feedback::Item.new }.to raise_error(ArgumentError) 6 | end 7 | 8 | it "should match default xml item tags" do 9 | item = Alfred::Feedback::Item.new("title") 10 | item.title.should eql "title" 11 | item.subtitle.should eql nil 12 | item.autocomplete.should eql nil 13 | item.arg.should eql item.title 14 | item.valid.should eql "yes" 15 | item.type.should eql "default" 16 | 17 | default_icon = {:type => "default", :name => "icon.png"} 18 | item.icon.should eql default_icon 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/alfred/feedback_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Feedback" do 4 | 5 | before :all do 6 | setup_workflow 7 | 8 | @alfred = Alfred::Core.new 9 | @feedback = Alfred::Feedback.new(@alfred) 10 | 11 | @item_elements = %w{title subtitle icon} 12 | @item_attributes = %w{uid arg autocomplete} 13 | end 14 | 15 | context "Feedback" do 16 | 17 | it "should create a basic XML response" do 18 | @feedback.add_item(:uid => "uid" , 19 | :arg => "arg" , 20 | :autocomplete => "autocomplete" , 21 | :title => "Title" , 22 | :subtitle => "Subtitle") 23 | 24 | xml_data = <<-END.strip_heredoc 25 | 26 | 27 | 28 | Title 29 | Arg 30 | Subtitle 31 | icon.png 32 | 33 | 34 | END 35 | 36 | compare_xml(xml_data, @feedback.to_xml).should == true 37 | end 38 | 39 | it "should not have uid if not presented" do 40 | 41 | @feedback.items = [] 42 | @feedback.add_item(:arg => "arg" , 43 | :autocomplete => "autocomplete" , 44 | :title => "Title" , 45 | :subtitle => "Subtitle") 46 | 47 | xml_data = <<-END.strip_heredoc 48 | 49 | 50 | 51 | Title 52 | Arg 53 | Subtitle 54 | icon.png 55 | 56 | 57 | END 58 | 59 | compare_xml(xml_data, @feedback.to_xml).should == true 60 | end 61 | 62 | end 63 | 64 | context "Cached Feedback" do 65 | it "should have correct default cache file" do 66 | @alfred.with_cached_feedback do 67 | use_cache_file :expire => 10 68 | end 69 | fb = @alfred.feedback 70 | fb.backend_file.should == File.join(@alfred.volatile_storage_path, "cached_feedback") 71 | end 72 | 73 | it "should set correct cache file" do 74 | alfred = Alfred::Core.new 75 | alfred.with_cached_feedback do 76 | use_cache_file :file => "new_cached_feedback" 77 | end 78 | fb = alfred.feedback 79 | fb.backend_file.should == "new_cached_feedback" 80 | end 81 | 82 | context "With Valid Cached File" do 83 | before :all do 84 | @alfred.with_cached_feedback do 85 | use_cache_file 86 | end 87 | 88 | fb = @alfred.feedback 89 | 90 | fb.add_item(:uid => "uid" , 91 | :arg => "arg" , 92 | :autocomplete => "autocomplete" , 93 | :title => "Title" , 94 | :subtitle => "Subtitle") 95 | 96 | @xml_data = <<-END.strip_heredoc 97 | 98 | 99 | 100 | Title 101 | Arg 102 | Subtitle 103 | icon.png 104 | 105 | 106 | END 107 | fb.put_cached_feedback 108 | end 109 | 110 | 111 | it "should correctly load cached feedback" do 112 | alfred = Alfred::Core.new 113 | alfred.with_cached_feedback do 114 | use_cache_file 115 | end 116 | 117 | fb = alfred.feedback 118 | 119 | compare_xml(@xml_data, fb.get_cached_feedback.to_xml).should == true 120 | end 121 | 122 | it "should expire as defined" do 123 | alfred = Alfred::Core.new 124 | alfred.with_cached_feedback do 125 | use_cache_file :expire => 1 126 | end 127 | sleep 1 128 | fb = alfred.feedback 129 | fb.get_cached_feedback.should == nil 130 | 131 | end 132 | 133 | end 134 | end 135 | 136 | after :all do 137 | @alfred.with_cached_feedback 138 | fb = @alfred.feedback 139 | File.unlink(fb.backend_file) 140 | reset_workflow 141 | end 142 | 143 | end 144 | -------------------------------------------------------------------------------- /spec/alfred/setting_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Setting with yaml as backend" do 4 | before :all do 5 | setup_workflow 6 | @alfred = Alfred::Core.new 7 | end 8 | 9 | it "should use yaml as defualt backend" do 10 | @alfred.workflow_setting.format.should == "yaml" 11 | end 12 | 13 | it "should correctly load settings" do 14 | @alfred.workflow_setting[:id].should == "me.zhaowu.alfred-workflow-gem" 15 | end 16 | 17 | it "should correctly save settings" do 18 | rand = rand(10**24-10) 19 | 20 | @alfred.workflow_setting[:rand] = rand 21 | @alfred.workflow_setting.dump(:flush => true) 22 | 23 | @alfred.workflow_setting.load 24 | @alfred.workflow_setting[:rand].should == rand 25 | 26 | end 27 | 28 | it "should handle common hash methods" do 29 | @alfred.workflow_setting.delete :rand 30 | 31 | @alfred.workflow_setting[:rand].should == nil 32 | end 33 | 34 | after :all do 35 | reset_workflow 36 | end 37 | 38 | end 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /spec/alfred/ui_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "UI" do 4 | 5 | before :all do 6 | setup_workflow 7 | @alfred = Alfred::Core.new do |a| 8 | @ui = Alfred::LogUI.new(bundle_id, "/tmp/#{bundle_id}.log") 9 | end 10 | end 11 | 12 | it "should use bundle id as log progname " do 13 | @alfred.ui.progname.should == @alfred.bundle_id 14 | end 15 | 16 | after :all do 17 | reset_workflow 18 | File.unlink(@alfred.ui.log_file) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/alfred_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Alfred" do 4 | 5 | before :all do 6 | setup_workflow 7 | @alfred = Alfred::Core.new 8 | end 9 | 10 | it "should return a valid bundle id" do 11 | @alfred.bundle_id.should == "me.zhaowu.alfred-workflow-gem" 12 | end 13 | 14 | it "should have correct default setting file" do 15 | @alfred.workflow_setting.backend_file.should eq File.join(Alfred.workflow_folder, "setting.yaml") 16 | end 17 | 18 | after :all do 19 | reset_workflow 20 | end 21 | 22 | end 23 | 24 | 25 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 2 | 3 | require "rspec" 4 | require 'fileutils' 5 | require 'awesome_print' 6 | 7 | 8 | require "alfred" 9 | 10 | RSpec.configure do |c| 11 | c.color_enabled = true 12 | 13 | # Use color not only in STDOUT but also in pagers and files 14 | c.tty = true 15 | 16 | c.formatter = :documentation # :progress, :html, :textmate 17 | 18 | c.mock_with :rspec 19 | end 20 | 21 | class String 22 | def strip_heredoc 23 | indent = scan(/^[ \t]*(?=\S)/).min.size || 0 24 | gsub(/^[ \t]{#{indent}}/, '') 25 | end 26 | end 27 | 28 | $rspec_dir = Dir.pwd 29 | $workflow_dir = File.expand_path('test/workflow/') 30 | def setup_workflow 31 | FileUtils.mkdir_p($workflow_dir) 32 | Dir.chdir($workflow_dir) 33 | end 34 | 35 | def reset_workflow 36 | Dir.chdir($rspec_dir) 37 | end 38 | 39 | def compare_xml(expected_xml_data, feedback_xml_data) 40 | item_elements = %w{title subtitle icon} 41 | item_attributes = %w{uid arg autocomplete} 42 | 43 | expected_xml = REXML::Document.new(expected_xml_data) 44 | feedback_xml = REXML::Document.new(feedback_xml_data) 45 | 46 | expected_item = expected_xml.get_elements('/items/item')[0] 47 | feedback_item = feedback_xml.get_elements('/items/item')[0] 48 | 49 | item_elements.each { |i| 50 | unless expected_item.elements[i].text.eql? feedback_item.elements[i].text 51 | return false 52 | end 53 | } 54 | item_attributes.each { |i| 55 | unless expected_item.attributes[i].eql? feedback_item.attributes[i] 56 | return false 57 | end 58 | } 59 | true 60 | end 61 | -------------------------------------------------------------------------------- /test/workflow/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | me.zhaowu.alfred-workflow-gem 7 | connections 8 | 9 | 21557827-8003-42B7-A042-16D4C9278FEC 10 | 11 | 12 | destinationuid 13 | 42D05639-3F0F-4760-8214-317167E4173D 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | 19 | 20 | BA17BE41-176A-4E9A-B958-A3F0BEA36829 21 | 22 | 23 | destinationuid 24 | 42D05639-3F0F-4760-8214-317167E4173D 25 | modifiers 26 | 0 27 | modifiersubtext 28 | 29 | 30 | 31 | 32 | createdby 33 | Zhao Cai 34 | description 35 | A starting place for developing Ruby based workflow in Alfred 36 | disabled 37 | 38 | name 39 | Ruby Based Workflow Template 40 | objects 41 | 42 | 43 | config 44 | 45 | argumenttype 46 | 1 47 | escaping 48 | 62 49 | keyword 50 | test feedback 51 | runningsubtext 52 | working... 53 | script 54 | /usr/bin/ruby ./main.rb {query} 55 | subtext 56 | Ruby Based Workflow Template 57 | title 58 | Test Alfred Feedback 59 | type 60 | 0 61 | withspace 62 | 63 | 64 | type 65 | alfred.workflow.input.scriptfilter 66 | uid 67 | 21557827-8003-42B7-A042-16D4C9278FEC 68 | 69 | 70 | config 71 | 72 | lastpathcomponent 73 | 74 | onlyshowifquerypopulated 75 | 76 | output 77 | 0 78 | removeextension 79 | 80 | sticky 81 | 82 | text 83 | {query} 84 | title 85 | Ruby Script 86 | 87 | type 88 | alfred.workflow.output.notification 89 | uid 90 | 42D05639-3F0F-4760-8214-317167E4173D 91 | 92 | 93 | config 94 | 95 | argumenttype 96 | 2 97 | escaping 98 | 63 99 | keyword 100 | test rescue feedback 101 | runningsubtext 102 | ... 103 | script 104 | /usr/bin/ruby ./main_with_rescue_feedback.rb {query} 105 | subtext 106 | Ruby Based Workflow Template 107 | title 108 | Test Alfred Rescue Feedback 109 | type 110 | 0 111 | withspace 112 | 113 | 114 | type 115 | alfred.workflow.input.scriptfilter 116 | uid 117 | BA17BE41-176A-4E9A-B958-A3F0BEA36829 118 | 119 | 120 | readme 121 | 122 | uidata 123 | 124 | 21557827-8003-42B7-A042-16D4C9278FEC 125 | 126 | ypos 127 | 250 128 | 129 | 42D05639-3F0F-4760-8214-317167E4173D 130 | 131 | ypos 132 | 140 133 | 134 | BA17BE41-176A-4E9A-B958-A3F0BEA36829 135 | 136 | ypos 137 | 370 138 | 139 | 140 | webaddress 141 | http://github.com/zhaocai/alfred2-ruby-template 142 | 143 | 144 | -------------------------------------------------------------------------------- /test/workflow/setting.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | :help: 3 | - :webloc: homepage.webloc 4 | :title: Check the Homepage for help 5 | :kind: url 6 | :url: https://github.com/zhaocai/alfred-workflow 7 | :subtitle: https://github.com/zhaocai/alfred-workflow 8 | - :path: README.pdf 9 | :kind: file 10 | :rand: 606365356008868852060429 11 | :id: me.zhaowu.alfred-workflow-gem 12 | --------------------------------------------------------------------------------