├── .gitignore ├── .ruby-version ├── Hatmaker.alfredworkflow ├── README.md ├── Rakefile ├── config.yml ├── remote.json ├── update.json └── workflow ├── Gemfile ├── Gemfile.lock ├── icon.png ├── info.plist ├── lib ├── alfred_workflow.rb ├── hatmaker.rb └── hatmaker │ ├── alfred.rb │ ├── alfred │ ├── workflow.rb │ └── yaml_end.rb │ └── workflow.rb └── main.rb /.gitignore: -------------------------------------------------------------------------------- 1 | workflow/.bundle 2 | workflow/bundle 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | system 2 | -------------------------------------------------------------------------------- /Hatmaker.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpinto/hatmaker/6cbabfd2099d46e918a3c8b4bae085e3a50f2a7e/Hatmaker.alfredworkflow -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hatmaker: Workflows Installer & Updater 2 | ======== 3 | 4 | Install & update alfred workflows straight from Alfred. Currently only [alfredworkflow.com](http://alfredworkflow.com) workflows are supported. 5 | 6 | ![Install example](http://f.cl.ly/items/0e231A3B163v0p2h0e3C/Screen%20Shot%202013-04-20%20at%2004.33.06%20.png) 7 | ![Outdated example](http://f.cl.ly/items/2k0G353j3w0z2j1d0v3Q/Screen%20Shot%202013-04-20%20at%2004.33.41%20.png) 8 | 9 | 10 | ## Download the workflow 11 | 12 | Download the workflow below and open in Alfred. 13 | 14 | [![Download Workflow](http://tombeynon.github.io/alfred-pow/images/alfred-pow/alfred-workflow-icon.png)](https://github.com/bpinto/hatmaker/raw/master/Hatmaker.alfredworkflow) 15 | 16 | 17 | ## Features 18 | 19 | Currently only supports installing and updating. The rest will be added soon. 20 | 21 | ### Install a workflow 22 | ``` 23 | install [workflow name] 24 | ``` 25 | 26 | ### List outdated workflows 27 | ``` 28 | outdated 29 | ``` 30 | 31 | ### Update a workflow 32 | List outdated workflows (```outdated```) and then select the outdated workflow 33 | 34 | ### Open workflow homepage 35 | Hold ```option``` key when installing or listing outdated workflows. 36 | 37 | ## Credits 38 | 39 | Everyone at the alfred community. 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' unless defined? Gem # rubygems is only needed in 1.8 2 | 3 | require 'yaml' 4 | require 'plist' 5 | 6 | config_file = 'config.yml' 7 | 8 | workflow_home=File.expand_path("~/Documents/Bruno/Alfred/Alfred.alfredpreferences/workflows") 9 | 10 | task :config do 11 | $config = YAML.load_file(config_file) 12 | $config["bundleid"] = "#{$config["domain"]}.#{$config["id"]}" 13 | $config["plist"] = File.join($config["path"], "info.plist") 14 | 15 | info = Plist::parse_xml($config["plist"]) 16 | unless info['bundleid'].eql?($config["bundleid"]) 17 | info['bundleid'] = $config["bundleid"] 18 | File.open($config["plist"], "wb") { |file| file.write(info.to_plist) } 19 | end 20 | end 21 | 22 | task :chdir => [:config] do 23 | chdir $config['path'] 24 | end 25 | 26 | desc "Install Gems" 27 | task "bundle:install" => [:chdir] do 28 | sh %Q{bundle install --standalone --clean} do |ok, res| 29 | if ! ok 30 | puts "fail to install gems (status = #{res.exitstatus})" 31 | end 32 | end 33 | end 34 | 35 | desc "Update Gems" 36 | task "bundle:update" => [:chdir] do 37 | sh %Q{bundle update && bundle install --standalone --clean} do |ok, res| 38 | if ! ok 39 | puts "fail to update gems (status = #{res.exitstatus})" 40 | end 41 | end 42 | end 43 | 44 | desc "Install to Alfred" 45 | task :install => [:config] do 46 | ln_sf File.expand_path($config["path"]), File.join(workflow_home, $config["bundleid"]) 47 | end 48 | 49 | desc "Unlink from Alfred" 50 | task :uninstall => [:config] do 51 | rm File.join(workflow_home, $config["bundleid"]) 52 | end 53 | 54 | 55 | 56 | 57 | desc "Clean up all the extras" 58 | task :clean => [:config] do 59 | end 60 | 61 | desc "Remove any generated file" 62 | task :clobber => [:clean] do 63 | rmtree File.join($config["path"], ".bundle") 64 | rmtree File.join($config["path"], "bundle") 65 | end 66 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # bundle_id = "domain.id" 2 | # path is the relative path to the workflow in the project root 3 | --- 4 | path: workflow 5 | domain: com.github.bpinto 6 | id: hatmaker 7 | 8 | -------------------------------------------------------------------------------- /remote.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.3, 3 | "download_url": "https://github.com/bpinto/hatmaker/raw/master/Hatmaker.alfredworkflow", 4 | "description": "Open workflow homepage when holding option key" 5 | } 6 | -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.3, 3 | "remote_json": "https://raw.github.com/bpinto/hatmaker/master/remote.json" 4 | } 5 | -------------------------------------------------------------------------------- /workflow/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'alfred-workflow', '>= 1.6.0' 4 | gem 'logging' 5 | gem 'oj' 6 | gem 'plist' 7 | -------------------------------------------------------------------------------- /workflow/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | alfred-workflow (1.7.0) 5 | logging (>= 1.8.0) 6 | plist (>= 3.1.0) 7 | little-plugger (1.1.3) 8 | logging (1.8.1) 9 | little-plugger (>= 1.1.3) 10 | multi_json (>= 1.3.6) 11 | multi_json (1.7.2) 12 | oj (2.0.10) 13 | plist (3.1.0) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | alfred-workflow (>= 1.6.0) 20 | logging 21 | oj 22 | plist 23 | -------------------------------------------------------------------------------- /workflow/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpinto/hatmaker/6cbabfd2099d46e918a3c8b4bae085e3a50f2a7e/workflow/icon.png -------------------------------------------------------------------------------- /workflow/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.github.bpinto.hatmaker 7 | connections 8 | 9 | 21557827-8003-42B7-A042-16D4C9278FEC 10 | 11 | 12 | destinationuid 13 | 2B7727E9-426C-4825-944C-B4A2BE25F5C4 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | 19 | 20 | destinationuid 21 | 462EFDCE-440B-4F52-A77E-2E9BD07771B4 22 | modifiers 23 | 524288 24 | modifiersubtext 25 | Open workflow home page. 26 | 27 | 28 | destinationuid 29 | F7BAA784-6EF8-4C37-A584-75A7251DD2B1 30 | modifiers 31 | 0 32 | modifiersubtext 33 | 34 | 35 | 36 | 2B7727E9-426C-4825-944C-B4A2BE25F5C4 37 | 38 | 39 | destinationuid 40 | 1A75DEFB-3A66-426E-92DE-76F1F6107336 41 | modifiers 42 | 0 43 | modifiersubtext 44 | 45 | 46 | 47 | 462EFDCE-440B-4F52-A77E-2E9BD07771B4 48 | 49 | 4FFC79AD-9487-4FDC-8FD9-4A5F39D99639 50 | 51 | 52 | destinationuid 53 | 2B7727E9-426C-4825-944C-B4A2BE25F5C4 54 | modifiers 55 | 0 56 | modifiersubtext 57 | 58 | 59 | 60 | destinationuid 61 | 462EFDCE-440B-4F52-A77E-2E9BD07771B4 62 | modifiers 63 | 524288 64 | modifiersubtext 65 | Open workflow home page. 66 | 67 | 68 | destinationuid 69 | F7BAA784-6EF8-4C37-A584-75A7251DD2B1 70 | modifiers 71 | 0 72 | modifiersubtext 73 | 74 | 75 | 76 | F7BAA784-6EF8-4C37-A584-75A7251DD2B1 77 | 78 | 79 | destinationuid 80 | B7C0E631-2B98-424D-BC1F-7925B8B6BBD2 81 | modifiers 82 | 0 83 | modifiersubtext 84 | 85 | 86 | 87 | 88 | createdby 89 | Bruno Pinto 90 | description 91 | Manage Alfred workflows. 92 | Manage Alfred workflows. 93 | disabled 94 | 95 | name 96 | Hatmaker 97 | objects 98 | 99 | 100 | config 101 | 102 | argumenttype 103 | 0 104 | escaping 105 | 0 106 | keyword 107 | install 108 | runningsubtext 109 | Searching... 110 | script 111 | /usr/bin/ruby ./main.rb search "{query}" 112 | subtext 113 | Search for and install a workflow ; + Option to open workflow's home page 114 | 115 | Search for and install a workflow ; + Option to open workflow's home page 116 | 117 | Search for and install a workflow ; + Option to open workflow's home page 118 | title 119 | Install a workflow 120 | type 121 | 0 122 | withspace 123 | 124 | 125 | type 126 | alfred.workflow.input.scriptfilter 127 | uid 128 | 21557827-8003-42B7-A042-16D4C9278FEC 129 | 130 | 131 | config 132 | 133 | lastpathcomponent 134 | 135 | onlyshowifquerypopulated 136 | 137 | output 138 | 0 139 | removeextension 140 | 141 | sticky 142 | 143 | text 144 | {query} 145 | title 146 | Hatmaker 147 | 148 | type 149 | alfred.workflow.output.notification 150 | uid 151 | 1A75DEFB-3A66-426E-92DE-76F1F6107336 152 | 153 | 154 | config 155 | 156 | escaping 157 | 68 158 | script 159 | /usr/bin/ruby ./main.rb install "{query}" 160 | type 161 | 0 162 | 163 | type 164 | alfred.workflow.action.script 165 | uid 166 | 2B7727E9-426C-4825-944C-B4A2BE25F5C4 167 | 168 | 169 | config 170 | 171 | escaping 172 | 68 173 | script 174 | echo "{query}" | awk -F=":" -v RS="," '$1~/"homepage"/ {print}' | sed s/\"homepage\"://g | sed s/\"//g | xargs open 175 | type 176 | 0 177 | 178 | type 179 | alfred.workflow.action.script 180 | uid 181 | 462EFDCE-440B-4F52-A77E-2E9BD07771B4 182 | 183 | 184 | config 185 | 186 | argumenttype 187 | 2 188 | escaping 189 | 0 190 | keyword 191 | outdated 192 | runningsubtext 193 | Searching... 194 | script 195 | /usr/bin/ruby ./main.rb outdated 196 | echo "abc" 197 | title 198 | Outdated workflows 199 | type 200 | 0 201 | withspace 202 | 203 | 204 | type 205 | alfred.workflow.input.scriptfilter 206 | uid 207 | 4FFC79AD-9487-4FDC-8FD9-4A5F39D99639 208 | 209 | 210 | config 211 | 212 | lastpathcomponent 213 | 214 | onlyshowifquerypopulated 215 | 216 | output 217 | 0 218 | removeextension 219 | 220 | sticky 221 | 222 | text 223 | Downloading {query}... 224 | title 225 | Hatmaker 226 | 227 | type 228 | alfred.workflow.output.notification 229 | uid 230 | B7C0E631-2B98-424D-BC1F-7925B8B6BBD2 231 | 232 | 233 | config 234 | 235 | escaping 236 | 68 237 | script 238 | echo "{query}" | awk -F=":" -v RS="," '$1~/"name"/ {print}' | sed s/\"name\"://g 239 | type 240 | 0 241 | 242 | type 243 | alfred.workflow.action.script 244 | uid 245 | F7BAA784-6EF8-4C37-A584-75A7251DD2B1 246 | 247 | 248 | readme 249 | 250 | uidata 251 | 252 | 1A75DEFB-3A66-426E-92DE-76F1F6107336 253 | 254 | ypos 255 | 10 256 | 257 | 21557827-8003-42B7-A042-16D4C9278FEC 258 | 259 | ypos 260 | 10 261 | 262 | 2B7727E9-426C-4825-944C-B4A2BE25F5C4 263 | 264 | ypos 265 | 10 266 | 267 | 462EFDCE-440B-4F52-A77E-2E9BD07771B4 268 | 269 | ypos 270 | 130 271 | 272 | 4FFC79AD-9487-4FDC-8FD9-4A5F39D99639 273 | 274 | ypos 275 | 130 276 | 277 | B7C0E631-2B98-424D-BC1F-7925B8B6BBD2 278 | 279 | ypos 280 | 250 281 | 282 | F7BAA784-6EF8-4C37-A584-75A7251DD2B1 283 | 284 | ypos 285 | 250 286 | 287 | 288 | webaddress 289 | http://bpinto.github.io 290 | 291 | 292 | -------------------------------------------------------------------------------- /workflow/lib/alfred_workflow.rb: -------------------------------------------------------------------------------- 1 | class AlfredWorkflow 2 | URL = 'https://raw.github.com/hzlzh/AlfredWorkflow.com/master/workflow_api.json' 3 | 4 | def self.all 5 | json = open(URL).read 6 | Oj.load(json).map { |data| Hatmaker::Workflow.new parse_workflow data } 7 | end 8 | 9 | private 10 | 11 | def self.parse_workflow(data) 12 | { 13 | 'author' => data['workflow_author_name'], 14 | 'description' => data['workflow_description_small'], 15 | 'download_link' => data['workflow_download_link'], 16 | 'filename' => 'workflow.alfredworkflow', 17 | 'homepage' => data['workflow_release_page'], 18 | 'name' => data['workflow_name'], 19 | 'version' => data['workflow_version'] 20 | } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /workflow/lib/hatmaker.rb: -------------------------------------------------------------------------------- 1 | module Hatmaker 2 | BUNDLE_ID = 'com.github.bpinto.hatmaker' 3 | 4 | def self.setting 5 | Hatmaker::Alfred::YamlEnd.load("#{storage_path}/setting.yaml") 6 | end 7 | 8 | private 9 | 10 | def self.path 11 | workflow.path 12 | end 13 | 14 | def self.storage_path 15 | workflow.storage_path 16 | end 17 | 18 | def self.workflow 19 | @workflow ||= Hatmaker::Alfred::Workflow.all.find { |workflow| workflow.bundle_id == BUNDLE_ID } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /workflow/lib/hatmaker/alfred.rb: -------------------------------------------------------------------------------- 1 | class Hatmaker::Alfred 2 | STORAGE_PATH = "#{ENV['HOME']}/Library/Application Support/Alfred 2/Workflow Data" 3 | WORKFLOWS_PATH = File.join(File.dirname(__FILE__), '../../..') 4 | 5 | def self.info(msg) 6 | logger.info msg.to_s 7 | end 8 | 9 | def self.error(msg) 10 | logger.error msg.to_s 11 | end 12 | 13 | private 14 | 15 | def self.logger 16 | @logger ||= Alfred::Logger.new(100) #TODO: remove this pog 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /workflow/lib/hatmaker/alfred/workflow.rb: -------------------------------------------------------------------------------- 1 | class Hatmaker::Alfred::Workflow 2 | attr_reader :author, :bundle_id, :description, :name 3 | 4 | def initialize(folder_name) 5 | @folder_name = folder_name 6 | 7 | @author = info['createdby'] 8 | @bundle_id = info['bundleid'] 9 | @description = info['description'] 10 | @disabled = info['disabled'] 11 | @name = info['name'] 12 | end 13 | 14 | def last_release 15 | @last_release ||= Hatmaker::Workflow.find self 16 | end 17 | 18 | def outdated? 19 | last_release && last_release.version > version 20 | end 21 | 22 | def path 23 | "#{Hatmaker::Alfred::WORKFLOWS_PATH}/#{@folder_name}" 24 | end 25 | 26 | def storage_path 27 | folder = "#{Hatmaker::Alfred::STORAGE_PATH}/#{@bundle_id}" 28 | FileUtils.mkdir_p(folder) unless File.exist? folder 29 | 30 | folder 31 | end 32 | 33 | def version 34 | @version ||= (Hatmaker.setting[@name] || alleyoop['version']).to_f 35 | end 36 | 37 | def self.all 38 | Dir.foreach(Hatmaker::Alfred::WORKFLOWS_PATH).map do |folder_name| 39 | next if folder_name =~ /^\./ or folder_name == 'hatmaker' 40 | new folder_name rescue nil 41 | end.compact 42 | end 43 | 44 | private 45 | 46 | def alleyoop 47 | @alleyoop ||= Oj.parse(File.read("#{path}/update.json")) rescue {} 48 | end 49 | 50 | def info 51 | @info ||= Plist::parse_xml(File.read("#{path}/info.plist")) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /workflow/lib/hatmaker/alfred/yaml_end.rb: -------------------------------------------------------------------------------- 1 | class Hatmaker::Alfred::YamlEnd 2 | def initialize(content, file_path) 3 | @content = content 4 | @file_path = file_path 5 | end 6 | 7 | def self.load(file_path) 8 | content = YAML::load(File.read file_path) rescue {} 9 | new content, file_path 10 | end 11 | 12 | def [](key) 13 | @content[key] 14 | end 15 | 16 | def []=(key, value) 17 | @content[key] = value 18 | dump 19 | end 20 | 21 | def dump(opts = {}) 22 | File.open(@file_path, 'wb') do |file| 23 | YAML::dump(@content, file) 24 | file.flush if opts[:flush] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /workflow/lib/hatmaker/workflow.rb: -------------------------------------------------------------------------------- 1 | class Hatmaker::Workflow 2 | attr_reader :author, :description, :download_link, :uid, :name, :version 3 | 4 | def initialize(params) 5 | @author = params['author'] 6 | @description = params['description'] 7 | @download_link = params['download_link'] 8 | @filename = params['filename'] 9 | @homepage = params['homepage'] 10 | @name = params['name'] 11 | @version = params['version'].to_f 12 | 13 | @uid = "#{@author}_#{@name}" 14 | end 15 | 16 | def download(&block) 17 | File.open("/tmp/#{@filename}", 'wb') do |saved_file| 18 | open(@download_link, 'rb') { |file| saved_file.write file.read } 19 | end 20 | 21 | yield self if block_given? 22 | end 23 | 24 | def install 25 | Hatmaker.setting[@name] = @version 26 | `open /tmp/#{@filename}` 27 | end 28 | 29 | def to_json 30 | Oj.dump(self) 31 | end 32 | 33 | def self.find(workflow) 34 | results = Hatmaker::Workflow.search workflow.name 35 | results.find { |result| result.name == workflow.name && result.author == workflow.author } 36 | end 37 | 38 | def self.search(query) 39 | query = Regexp.escape(query) 40 | 41 | @workflows ||= AlfredWorkflow.all 42 | @workflows.select { |workflow| workflow.name =~ /#{query}/i } 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /workflow/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | require 'rubygems' unless defined? Gem # rubygems is only needed in 1.8 5 | require 'bundle/bundler/setup' 6 | require 'alfred' 7 | require 'oj' 8 | require 'open-uri' 9 | 10 | require File.join(File.dirname(__FILE__), 'lib/alfred_workflow') 11 | require File.join(File.dirname(__FILE__), 'lib/hatmaker') 12 | require File.join(File.dirname(__FILE__), 'lib/hatmaker/alfred') 13 | require File.join(File.dirname(__FILE__), 'lib/hatmaker/alfred/workflow') 14 | require File.join(File.dirname(__FILE__), 'lib/hatmaker/alfred/yaml_end') 15 | require File.join(File.dirname(__FILE__), 'lib/hatmaker/workflow') 16 | 17 | def search(query, feedback) 18 | workflows = Hatmaker::Workflow.search(query) 19 | workflows.each do |workflow| 20 | feedback.add_item( 21 | :uid => workflow.uid, 22 | :title => workflow.name, 23 | :subtitle => workflow.description, 24 | :arg => workflow.to_json 25 | ) 26 | end 27 | end 28 | 29 | def install(json, feedback) 30 | begin 31 | workflow = Oj.load(json) 32 | workflow.download { |workflow| workflow.install } 33 | rescue OpenURI::HTTPError => ex 34 | puts "Error while downloading #{workflow.name}, please try again later." 35 | end 36 | end 37 | 38 | def outdated(feedback) 39 | Hatmaker::Alfred::Workflow.all.each do |installed_workflow| 40 | if installed_workflow.outdated? 41 | new_release = installed_workflow.last_release 42 | 43 | feedback.add_item( 44 | :uid => new_release.uid, 45 | :title => new_release.name, 46 | :subtitle => "v#{new_release.version} by #{new_release.author}", 47 | :arg => new_release.to_json 48 | ) 49 | end 50 | end 51 | end 52 | 53 | Alfred.with_friendly_error do |alfred| 54 | alfred.with_rescue_feedback = true 55 | feedback = alfred.feedback 56 | 57 | command = ARGV[0] 58 | arguments = ARGV[1] 59 | 60 | case command 61 | when /search/ 62 | search arguments, feedback 63 | when /install/ 64 | install arguments, feedback 65 | when /outdated/ 66 | outdated feedback 67 | end 68 | 69 | if feedback.items.none? 70 | feedback.add_item( 71 | :uid => 'nothingfound', 72 | :title => arguments ? "No workflows found with '#{arguments}'" : 'All workflows are up to date!', 73 | :valid => 'no' 74 | ) 75 | end 76 | 77 | puts feedback.to_xml if command != 'install' 78 | end 79 | 80 | --------------------------------------------------------------------------------