├── README.markdown ├── alfred_feedback.rb ├── config.rb ├── default_action.rb ├── download_progress.rb ├── icon.png ├── info.plist ├── installSoftware.sh └── recent_downloads.rb /README.markdown: -------------------------------------------------------------------------------- 1 | # Recent Downloads Workflow 2 | This is an [Alfred v2](http://www.alfredapp.com) workflow to access 3 | the most recent downloads in the Downloads folder. 4 | 5 | The items are sorted in decreasing order based on the time they 6 | are added to the folder. The item is filtered by testing whether 7 | the query is a subsequence (need not be consecutive) of it. 8 | 9 | There are two operations on the selected item: 10 | 11 | 1. open with default application (default) 12 | - if the item can be installed (an application, an dmg file, a zip 13 | file containing those files, etc), the workflow will prompt the user 14 | whether to install it. After the installation, if the item installed 15 | is an application, the workflow will prompt the user whether to launch 16 | it 17 | 2. reveal in Finder (holding "option" key) 18 | 3. delete (holding "ctrl" key) 19 | 4. move to trash (holding "cmd" key) 20 | 21 | The installation behavior can be controlled by `config.yaml` at 22 | `~/Library/Application Support/Alfred 2/Workflow Data/recentdownloads.ddjfreedom/config.yaml`: 23 | 24 | | Name | Possible Values | 25 | |:-----|:----------------| 26 | | `install_action` | `ask`, `install`, `open` | 27 | | `auto_start` | `ask`, `always`, `never`| 28 | | `subfolders` | `:all`, `:none`, a list of entries | 29 | | `max-entries`| `:all`, integer for the number of results | 30 | 31 | ## Subfolders 32 | Recent Downloads workflow can display items in some specified subfolders along with everything in `~/Downloads`. The value for `subfolders` in `config.yaml` can be `:all`, `:none`, or a list of entries. Each entry can be a path or a hash: 33 | 34 | ```no-highlight 35 | folder: 36 | depth: = 1> (default to 1) 37 | exclude: (default to false) 38 | ``` 39 | where `depth` controls how deep to go down the file system tree starting from `folder`, and `exclude` controls whether the `folder` itself is included in the result. `:all` and `:none` will override all other settings (whichever appears first). 40 | 41 | ### Examples 42 | Suppose `~/Downloads` is as following 43 | ```no-highlight 44 | +-~/Downloads 45 | +-a/ 46 | | +-aa.pdf 47 | | +-ab/ 48 | | +-pic.jpg 49 | | +-e/ 50 | | +-f.img 51 | +-b/ 52 | | +-ba/ 53 | | +-baa/ 54 | | | +-foo/ 55 | | | | +-bar.c 56 | | | +-bar/ 57 | | | +-foo/ 58 | | | +-foo.c 59 | | +-bab.xml 60 | +-c/ 61 | +-imgs/ 62 | +-img.png 63 | ``` 64 | If `config.yaml` has the following value 65 | ```no-highlight 66 | subfolders: 67 | - 68 | folder: "a" 69 | exclude: false 70 | depth: 2 71 | - 72 | folder: "b" 73 | exclude: true 74 | - 75 | "c" 76 | ``` 77 | the result will be 78 | ```no-highlight 79 | a, a/aa.pdf, a/ab, a/ab/pic.jpg, a/ab/e 80 | b/ba 81 | c, c/imgs 82 | ``` 83 | # Installation 84 | ## AlfPT 85 | Recent Downloads is now on AlfPT: 86 | 87 | `alfpt install Recent Downloads` 88 | 89 | ## Manual 90 | 1. Download and unpack the .zip archive. 91 | 2. Double-click the "Recent Downloads.alfredworkflow" to install. 92 | 93 | # Acknowledgement 94 | This workflow uses [alfred_feedback.rb](https://gist.github.com/4555836) 95 | with some modifications to make it compatible with Ruby 1.8.7. 96 | The script for installation is written by Okke Tijhuis ([@otijhuis](https://twitter.com/@otijhuis), 97 | [otijhuis.tumblr.com](http://otijhuis.tumblr.com)), and [David](http://jdfwarrior.tumblr.com). 98 | -------------------------------------------------------------------------------- /alfred_feedback.rb: -------------------------------------------------------------------------------- 1 | require "rexml/document" 2 | 3 | class Feedback 4 | 5 | attr_accessor :items 6 | @@time = Time.now.to_s 7 | 8 | def initialize 9 | @items = [] 10 | end 11 | 12 | def add_item(opts = {}) 13 | opts[:subtitle] ||= "" 14 | opts[:icon] ||= {:type => "default", :name => "icon.png"} 15 | opts[:uid] ||= opts[:title] 16 | opts[:uid] += @@time 17 | opts[:arg] ||= opts[:title] 18 | opts[:valid] ||= "yes" 19 | opts[:autocomplete] ||= opts[:title] 20 | opts[:type] = "file" 21 | @items << opts unless opts[:title].nil? 22 | end 23 | 24 | def to_xml(items = @items) 25 | document = REXML::Element.new("items") 26 | items.each do |item| 27 | new_item = REXML::Element.new('item') 28 | new_item.add_attributes({ 29 | 'uid' => item[:uid], 30 | 'arg' => item[:arg], 31 | 'valid' => item[:valid], 32 | 'autocomplete' => item[:autocomplete], 33 | 'type' => item[:type] 34 | }) 35 | 36 | REXML::Element.new("title", new_item).text = item[:title] 37 | REXML::Element.new("subtitle", new_item).text = item[:subtitle] 38 | 39 | icon = REXML::Element.new("icon", new_item) 40 | icon.text = item[:icon][:name] 41 | icon.add_attributes('type' => 'fileicon') if item[:icon][:type] == "fileicon" 42 | 43 | document << new_item 44 | end 45 | 46 | document.to_s 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module RDW 4 | def RDW.directory?(path, base = "") 5 | File.directory?(File.expand_path path, base) && 6 | !RDW.hidden?(path) && 7 | ![".app", ".lpdf", ".mpkg", ".prefpane"].include?(File.extname(path).downcase) 8 | end 9 | 10 | def RDW.hidden?(path) 11 | File.basename(path).start_with?(".") 12 | end 13 | 14 | def RDW.entries(path, base = "") 15 | Dir.entries(File.expand_path path, base).delete_if {|f| RDW.hidden? f} 16 | end 17 | 18 | class Config 19 | VOLATILE_DIR = ENV['alfred_workflow_cache'] 20 | NONVOLATILE_DIR = ENV['alfred_workflow_data'] 21 | Dir.mkdir VOLATILE_DIR unless File.exist? VOLATILE_DIR 22 | Dir.mkdir NONVOLATILE_DIR unless File.exist? NONVOLATILE_DIR 23 | 24 | attr_reader :base_dir, :config_file_path 25 | 26 | def initialize 27 | @config_file_path = File.expand_path "config.yaml", NONVOLATILE_DIR 28 | @base_dir = File.expand_path "~/Downloads" 29 | @config = {} 30 | @config = File.open(@config_file_path, "r") {|f| YAML.load f} if File.exist? @config_file_path 31 | original_config = @config.dup 32 | @config['install_action'] ||= 'open' 33 | @config['auto_start'] ||= 'never' 34 | @config['subfolders'] ||= :none 35 | @config['max-entries'] ||= 20 36 | self.commit unless @config == original_config 37 | self.standardize 38 | end 39 | 40 | def [](key) 41 | @config[key] 42 | end 43 | 44 | def method_missing(sym, *arg) 45 | @config[sym.to_s] 46 | end 47 | 48 | def commit 49 | File.open(@config_file_path, "w") {|f| YAML.dump @config, f} 50 | end 51 | 52 | def standardize 53 | @config["subfolders"] ||= :none 54 | @config["subfolders"] = [@config["subfolders"]] unless @config["subfolders"].is_a? Array 55 | 56 | i = 0 57 | while i < @config["subfolders"].count 58 | entry = @config["subfolders"][i] 59 | hash = entry.is_a?(Hash) ? entry : {"folder" => entry} 60 | 61 | hash["exclude"] = false unless hash["exclude"] == true 62 | hash["depth"] ||= 1 63 | if hash["folder"] == :all 64 | dirs = RDW.entries(@base_dir).delete_if do |f| 65 | !RDW.directory? f, @base_dir 66 | end 67 | dirs.map! do |f| 68 | {"folder" => f, 69 | "depth" => hash["depth"], 70 | "exclude" => hash["exclude"]} 71 | end 72 | @config["subfolders"] = dirs 73 | break 74 | elsif hash["folder"] == :none 75 | @config["subfolders"] = [] 76 | break 77 | end 78 | hash["folder"] = hash["folder"] 79 | 80 | @config["subfolders"][i] = hash 81 | i += 1 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /default_action.rb: -------------------------------------------------------------------------------- 1 | load "config.rb" 2 | config = RDW::Config.new 3 | unless ["ask", "install", "open"].include?(config["install_action"]) && 4 | ["ask", "always", "never"].include?(config["auto_start"]) 5 | puts "Error: Invalid Configuration" 6 | exit 0 7 | end 8 | 9 | action = config["install_action"].capitalize 10 | 11 | if action == "Ask" || action == "Install" 12 | need_install = case File.extname ARGV[0] 13 | when ".app" 14 | $QUESTION = "#{ARGV[0]} is an application.\n\nDo you want to open it at the downloaded location or install it?" 15 | true 16 | when ".dmg" 17 | $QUESTION = "#{ARGV[0]} is a disk image.\n\nDo you want to open it or install any applications it contains?" 18 | true 19 | when ".zip" 20 | $QUESTION = "#{File.basename ARGV[0]} contains one or more applications.\n\nWhat would you like to do?" 21 | "0" != `zipinfo -1 "#{ARGV[0]}" | grep -e "\.app/$" -e "\.pkg$" -e "\.mpkg/$" -e "\.dmg$" -e "\.prefPane/$" | grep -v -e ".*\.mpkg/.*\.pkg$" -e ".*\.app/.*\.app/$" -e ".*\.prefPane/.*\.app/$" -e ".*\.prefPane/.*\.mpkg/$" -e ".*\.prefPane/.*\.pkg$" | grep -i -v "__MACOSX" | sed -e 's#/$##' | wc -l`.strip 22 | else 23 | false 24 | end 25 | action = "Open" if !need_install 26 | action = `osascript 2> /dev/null <<-EOF 27 | tell application "System Events" 28 | try 29 | set question to display dialog "#{$QUESTION}" buttons {"Install","Open","Cancel"} default button 1 with title "Alfred Recent Downloads" with icon caution 30 | set answer to button returned of question 31 | on error number -128 32 | set answer to "Cancel" 33 | end try 34 | return answer 35 | end tell 36 | EOF`.strip if action == "Ask" 37 | case action 38 | when "Install" 39 | output = `/bin/bash installSoftware.sh -s #{config["auto_start"]} "#{ARGV[0]}"` 40 | puts output 41 | when "Open" 42 | `open "#{ARGV[0]}"` 43 | else 44 | # Do nothing 45 | end 46 | else 47 | `open "#{ARGV[0]}"` 48 | end 49 | -------------------------------------------------------------------------------- /download_progress.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # Helper functions for formatting bytes to how Finder displays them. 4 | module OSXBytes 5 | # Turns a float into an integer if it doesn't have a value after the demical 6 | # place (ex: 13.0 converts to 13 (an int), 13.5 doesn't change) 7 | # Yeah, I'm bad at naming things. 8 | def self.remove_trailing_zero(float) 9 | if float - float.to_i == 0 10 | float.to_i 11 | else 12 | float 13 | end 14 | end 15 | 16 | # Round number to a specified number of decimal places. 17 | def self.round_number(number, digits) 18 | (number * 10 ** digits).round.to_f / 10 ** digits 19 | end 20 | 21 | # Get floor of a number to a specified number of decimal places. 22 | def self.floor_number(number, digits) 23 | (number * 10 ** digits).floor.to_f / 10 ** digits 24 | end 25 | 26 | # Displays a readable representation of a file size, mimicking Finder's 27 | # representation. 28 | # 29 | # 1 KB = 1000 bytes, as in OS X 10.6 and up 30 | def self.format(bytes) 31 | return "0 bytes" if bytes < 1 32 | 33 | units = %w[B KB MB GB TB] 34 | 35 | max_exp = units.size - 1 36 | exp = (Math.log(bytes) / Math.log(1000)).to_i 37 | exp = max_exp if exp > max_exp 38 | 39 | number = (bytes / 1000.0 ** exp) 40 | 41 | # Display up to 2 decimal points for GBs and TBs, 1 for MBs, and round to 42 | # the nearest integer for B and KB. 43 | number = if exp > 2 44 | self.round_number(number, 2) 45 | else 46 | self.round_number(number, (exp == 0) ? exp : exp - 1) 47 | end 48 | 49 | number = self.remove_trailing_zero(number) 50 | 51 | "#{number} #{units[exp]}" 52 | end 53 | end 54 | 55 | def download_item(path) 56 | begin 57 | downloaded_file = "#{path}/#{path.split('/').last.chomp('.download')}" 58 | plist_file = "#{path}/Info.plist" 59 | total_size = `/usr/libexec/PlistBuddy -c Print:DownloadEntryProgressTotalToLoad "#{plist_file}"`.to_f 60 | current_size = File::stat(downloaded_file).size 61 | 62 | if total_size > 0 63 | percent = (current_size / total_size) * 100 64 | subtitle = "Download is %0.1f%% complete — %s of %s" % [ 65 | percent, 66 | OSXBytes::format(current_size), 67 | OSXBytes::format(total_size) 68 | ] 69 | else 70 | # Set a fake percentage to show a generic download icon 71 | percent = 100 72 | # We don't know the total size, so just display how much was downloaded 73 | subtitle = "#{OSXBytes::format(current_size)} downloaded" 74 | end 75 | 76 | icon = "/Applications/Safari.app/Contents/Resources/download#{(percent / 10).floor}.icns" 77 | 78 | { 79 | :title => File.basename(path).chomp('.download'), 80 | :subtitle => subtitle, 81 | :arg => path, 82 | :icon => {:name => icon} 83 | } 84 | rescue 85 | nil 86 | end 87 | end -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddjfreedom/recent-downloads-alfred-v2/a644f545836b95d0b1fa498fd9ee5b2883256fb4/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | recentdownloads.ddjfreedom 7 | connections 8 | 9 | 2F377015-2088-413D-8A95-33ED6CBD7A8B 10 | 11 | 12 | destinationuid 13 | C7A50D51-1AFA-4D7B-B31C-5D8CF9E5B0C2 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | 19 | 20 | E87F5100-52C4-4CFD-BD10-56A5F9FDB1BC 21 | 22 | 23 | destinationuid 24 | 2F377015-2088-413D-8A95-33ED6CBD7A8B 25 | modifiers 26 | 0 27 | modifiersubtext 28 | 29 | 30 | 31 | destinationuid 32 | A6509700-CBFC-4EB8-90C4-0613AF7C07FB 33 | modifiers 34 | 524288 35 | modifiersubtext 36 | Reveal in Finder 37 | 38 | 39 | destinationuid 40 | B4861326-BB43-4222-A7F4-D21ED1C0E3D8 41 | modifiers 42 | 262144 43 | modifiersubtext 44 | Delete 45 | 46 | 47 | destinationuid 48 | 5ED38895-9385-4E48-AA95-9276C6414F08 49 | modifiers 50 | 1048576 51 | modifiersubtext 52 | Move to Trash 53 | 54 | 55 | 56 | createdby 57 | Dajun Duan 58 | description 59 | Open or Reveal the recent downloads in Downloads folder 60 | disabled 61 | 62 | name 63 | Recent Downloads 64 | objects 65 | 66 | 67 | config 68 | 69 | escaping 70 | 38 71 | normalisation 72 | 0 73 | script 74 | output=`/usr/bin/ruby default_action.rb "{query}"` 75 | IFS=$'\n' 76 | for line in $output 77 | do 78 | echo $line 79 | done 80 | unset IFS 81 | 82 | type 83 | 0 84 | 85 | type 86 | alfred.workflow.action.script 87 | uid 88 | 2F377015-2088-413D-8A95-33ED6CBD7A8B 89 | 90 | 91 | config 92 | 93 | argumenttype 94 | 1 95 | escaping 96 | 0 97 | keyword 98 | recent 99 | normalisation 100 | 0 101 | script 102 | /usr/bin/ruby recent_downloads.rb "{query}" 103 | subtext 104 | Open or Reveal Recent Downloads 105 | title 106 | Recent Downloads 107 | type 108 | 0 109 | withspace 110 | 111 | 112 | type 113 | alfred.workflow.input.scriptfilter 114 | uid 115 | E87F5100-52C4-4CFD-BD10-56A5F9FDB1BC 116 | 117 | 118 | type 119 | alfred.workflow.action.revealfile 120 | uid 121 | A6509700-CBFC-4EB8-90C4-0613AF7C07FB 122 | 123 | 124 | config 125 | 126 | escaping 127 | 63 128 | normalisation 129 | 0 130 | script 131 | rm -rf {query} 132 | type 133 | 0 134 | 135 | type 136 | alfred.workflow.action.script 137 | uid 138 | B4861326-BB43-4222-A7F4-D21ED1C0E3D8 139 | 140 | 141 | config 142 | 143 | escaping 144 | 38 145 | normalisation 146 | 0 147 | script 148 | filepath="{query}" 149 | # replace " with \" to prevent closing the quotes in applescript 150 | # too early 151 | filepath=${filepath//\"/'\"'} 152 | osascript -e 'tell application "Finder" to delete POSIX file "'"$filepath"'"' 153 | 154 | type 155 | 0 156 | 157 | type 158 | alfred.workflow.action.script 159 | uid 160 | 5ED38895-9385-4E48-AA95-9276C6414F08 161 | 162 | 163 | config 164 | 165 | lastpathcomponent 166 | 167 | onlyshowifquerypopulated 168 | 169 | output 170 | 0 171 | removeextension 172 | 173 | sticky 174 | 175 | text 176 | {query} 177 | title 178 | Recent Downloads 179 | 180 | type 181 | alfred.workflow.output.notification 182 | uid 183 | C7A50D51-1AFA-4D7B-B31C-5D8CF9E5B0C2 184 | 185 | 186 | uidata 187 | 188 | 2F377015-2088-413D-8A95-33ED6CBD7A8B 189 | 190 | ypos 191 | 10 192 | 193 | 5ED38895-9385-4E48-AA95-9276C6414F08 194 | 195 | ypos 196 | 380 197 | 198 | A6509700-CBFC-4EB8-90C4-0613AF7C07FB 199 | 200 | ypos 201 | 130 202 | 203 | B4861326-BB43-4222-A7F4-D21ED1C0E3D8 204 | 205 | ypos 206 | 250 207 | 208 | C7A50D51-1AFA-4D7B-B31C-5D8CF9E5B0C2 209 | 210 | ypos 211 | 10 212 | 213 | E87F5100-52C4-4CFD-BD10-56A5F9FDB1BC 214 | 215 | ypos 216 | 70 217 | 218 | 219 | webaddress 220 | https://github.com/ddjfreedom/recent-downloads-alfred-v2 221 | 222 | 223 | -------------------------------------------------------------------------------- /installSoftware.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function installApplication { 4 | APPLICATION=$1 5 | APP_NAME=`basename ${APPLICATION}` 6 | APP_EXTENSION=${APPLICATION##*.} 7 | 8 | case $APP_EXTENSION in 9 | app) 10 | if [ "${APPLICATION}" = "/Applications/${APP_NAME}" ];then 11 | echo "'${APP_NAME}' has already been installed. No need to continue." 12 | else 13 | osascript >& /dev/null <<-EOF 14 | tell application "Finder" 15 | try 16 | move POSIX file "${APPLICATION}" to folder "Applications" of startup disk with replacing 17 | on error errMsg number errNr 18 | if errNr is -8087 then 19 | tell application "System Events" to display dialog "Unable to install application ${APP_NAME}.\n\nIt looks like you're trying to update an application that is still running." buttons "OK" with title "Alfred Install Action" with icon caution 20 | else 21 | tell application "System Events" to display dialog "Unable to install application.\n\n" & errMsg buttons "OK" with title "Alfred Install Action" with icon caution 22 | end if 23 | error errMsg number errNr 24 | end try 25 | end tell 26 | EOF 27 | 28 | if [ $? -ne 0 ];then 29 | echo "Application '${APP_NAME}' not installed." 30 | else 31 | echo "Application '${APP_NAME}' sucessfully installed." 32 | fi 33 | fi 34 | ;; 35 | pkg|mpkg) 36 | osascript >& /dev/null <<-EOF 37 | tell application "Finder" 38 | open POSIX file "${APPLICATION}" 39 | end tell 40 | EOF 41 | 42 | # Find the latest installer process in case others are running already 43 | PID=`ps -A -o pid,etime,command | sort -k2 | grep -i "/Installer.app" | grep -v "grep" | head -n1 | awk '{print $1}'` 44 | 45 | # Wait for the process to end. Can't do this with wait since it's not a sub process of this shell 46 | if [ "$PID" ]; then 47 | while kill -0 "$PID" 2> /dev/null; do 48 | sleep 2 49 | done 50 | fi 51 | 52 | echo "Installation process of '${APP_NAME}' completed." 53 | ;; 54 | alfredextension|prefPane) 55 | osascript >& /dev/null <<-EOF 56 | tell application "Finder" 57 | open POSIX file "${APPLICATION}" 58 | end tell 59 | EOF 60 | echo "Installation process of '${APP_NAME}' started. Complete manually." 61 | ;; 62 | *) 63 | echo "Unsupported applicationtype." 64 | ;; 65 | esac 66 | } 67 | 68 | function installSoftware { 69 | FILENAME="$1" 70 | EXTENSION=${FILENAME##*.} 71 | 72 | #LATEST_CTIME=`find /Applications -type d -name "*.app" -prune -ctime -15 -exec stat -f "%c" {} \; | sort -rn | head -n 4 | tail -n 1` 73 | LATEST_CTIME=`find /Applications -type d -name "*.app" -prune -ctime -15 -exec stat -f "%c" {} \; | sort -rn | head -n 1` 74 | 75 | case $EXTENSION in 76 | dmg) 77 | MOUNTPOINT=`echo "Y" | hdiutil attach -noautoopen -nobrowse -puppetstrings "${FILENAME}" | grep "/Volumes/" | sed -e 's#.*\(/Volumes/.*\)#\1#'` 78 | APPLICATIONS=`find "${MOUNTPOINT}" -not -path "${MOUNTPOINT}" 2> /dev/null | grep -e "\.app$" -e "\.mpkg$" -e "\.pkg$" -e "\.prefPane$"| grep -v -e "${MOUNTPOINT}.*\.mpkg/.*\.pkg$" -e "${MOUNTPOINT}.*\.app/.*\.app$" -e "${MOUNTPOINT}.*\.prefPane/.*\.app$" -e "${MOUNTPOINT}.*\.prefPane/.*\.pkg$" -e "${MOUNTPOINT}.*\.prefPane/.*\.mpkg$"` 79 | 80 | if [ -z "$APPLICATIONS" ];then 81 | APP_NAME=`basename ${FILENAME}` 82 | echo "${APP_NAME} does not contain any applications to install." 83 | fi 84 | 85 | APP_COUNT=`echo "$APPLICATIONS" | wc -l` 86 | if [ $APP_COUNT -gt 1 ];then 87 | APPLICATIONS=`echo "$APPLICATIONS" | grep -i -v -e "uninstall.*\.mpkg$" -e "uninstall.*\.pkg$" -e "remove.*\.mpkg$" -e "remove.*\.pkg"` 88 | fi 89 | 90 | IFS=$'\n' 91 | for i in $APPLICATIONS;do 92 | installApplication "$i" 93 | done 94 | unset IFS 95 | hdiutil detach "${MOUNTPOINT}" > /dev/null 96 | ;; 97 | zip) 98 | APPLICATIONS=`zipinfo -1 "${FILENAME}" | grep -e "\.app/$" -e "\.pkg$" -e "\.mpkg/$" -e "\.dmg$" -e "\.prefPane/$" | grep -v -e ".*\.mpkg/.*\.pkg$" -e ".*\.app/.*\.app/$" -e ".*\.prefPane/.*\.app/$" -e ".*\.prefPane/.*\.mpkg/$" -e ".*\.prefPane/.*\.pkg$" | grep -i -v "__MACOSX" | sed -e 's#/$##'` 99 | 100 | if [ -z "$APPLICATIONS" ];then 101 | APP_NAME=`basename ${FILENAME}` 102 | echo "${APP_NAME} does not contain any applications to install." 103 | fi 104 | 105 | APP_COUNT=`echo "$APPLICATIONS" | wc -l` 106 | if [ $APP_COUNT -gt 1 ];then 107 | APPLICATIONS=`echo "$APPLICATIONS" | grep -i -v -e "uninstall.*\.mpkg$" -e "uninstall.*\.pkg$" -e "remove.*\.mpkg$" -e "remove.*\.pkg"` 108 | fi 109 | 110 | UNZIPDIR=`dirname ${FILENAME}` 111 | 112 | unzip -o -qq -d "${UNZIPDIR}" "${FILENAME}" -x '__MACOSX/*' 2> /dev/null 113 | IFS=$'\n' 114 | for i in $APPLICATIONS;do 115 | if [ ! -z "$i" ];then 116 | installApplication "${UNZIPDIR}/${i}" 117 | EXTENSION=${i##*.} 118 | case $EXTENSION in 119 | pkg|mpkg) 120 | rm -rf "${UNZIPDIR}/${i}" 121 | ;; 122 | esac 123 | fi 124 | done 125 | unset IFS 126 | ;; 127 | app|pkg|mpkg|alfredextension|prefPane) 128 | installApplication "${FILENAME}" 129 | ;; 130 | *) 131 | APP_NAME=`basename "${FILENAME}"` 132 | echo "Can't install '${APP_NAME}'. Unsupported filetype." 133 | ;; 134 | esac 135 | 136 | # Check which applications were installed 137 | APP_COUNTER=0 138 | IFS=$'\n' 139 | for f in `find /Applications -type d -name "*.app" -prune -ctime -1 -exec stat -f "%c%t%N" {} \; | sort -rn`;do 140 | CTIME=`echo "$f" | cut -f 1` 141 | NEW_APPLICATION=`echo "$f" | cut -f 2` 142 | if [ $CTIME -gt $LATEST_CTIME ];then 143 | APP_COUNTER=$(( $APP_COUNTER + 1 )) 144 | INSTALLED_APPLICATIONS[${APP_COUNTER}]="$NEW_APPLICATION" 145 | fi 146 | done 147 | unset IFS 148 | 149 | 150 | if [ ${#INSTALLED_APPLICATIONS[@]} -gt 0 ];then 151 | case $AUTO_START in 152 | ask) 153 | if [ ${#INSTALLED_APPLICATIONS[@]} -eq 1 ];then 154 | SHORT_NAME=`basename "${INSTALLED_APPLICATIONS[1]}" | sed -e 's/\.app$//'` 155 | ACTION=`osascript 2> /dev/null <<-EOF 156 | tell application "System Events" 157 | set question to display dialog "You just installed application ${SHORT_NAME}.\n\nDo you want to start it?" buttons {"Yes, start application","No"} default button 1 with title "Alfred Install" with icon caution 158 | set answer to button returned of question 159 | return answer 160 | end tell 161 | EOF` 162 | 163 | if [ $? -eq 0 ] && [ "$ACTION" = "Yes, start application" ];then 164 | xattr -d com.apple.quarantine "${INSTALLED_APPLICATIONS[1]}" 2>/dev/null 165 | open -a "${INSTALLED_APPLICATIONS[1]}" 166 | echo "Application '${SHORT_NAME}' started." 167 | fi 168 | else 169 | IFS=$'\n' 170 | for a in ${INSTALLED_APPLICATIONS[@]};do 171 | LIST="${LIST}\"`basename "${a}" | sed -e 's/\.app$//'`\"" 172 | done 173 | unset IFS 174 | LIST=`echo "$LIST" | sed -e 's/""/","/g'` 175 | ACTION=`osascript 2> /dev/null <<-EOF 176 | tell application "System Events" 177 | set theList to { ${LIST} } 178 | choose from list theList with title "Alfred Install" with prompt "Several new installed applications were found.\n\nDo you want to start one?" OK button name "Start selected application" cancel button name "Cancel" 179 | return result 180 | end tell 181 | EOF` 182 | 183 | if [ $? -eq 0 ];then 184 | find /Applications -type d -name "${ACTION}.app" -exec xattr -d com.apple.quarantine {} \; 2> /dev/null 185 | open -a "${ACTION}.app" 186 | echo "Application '${ACTION}' started." 187 | fi 188 | fi 189 | ;; 190 | always) 191 | if [ ${#INSTALLED_APPLICATIONS[@]} -eq 1 ];then 192 | xattr -d com.apple.quarantine "${INSTALLED_APPLICATIONS[1]}" 2>/dev/null 193 | open -a "${INSTALLED_APPLICATIONS[1]}" 194 | echo "Application '`basename "${INSTALLED_APPLICATIONS[1]}" | sed -e 's/\.app$//'`' started." 195 | else 196 | IFS=$'\n' 197 | for a in ${INSTALLED_APPLICATIONS[@]};do 198 | LIST="${LIST}\"`basename "${a}" | sed -e 's/\.app$//'`\"" 199 | done 200 | unset IFS 201 | LIST=`echo "$LIST" | sed -e 's/""/","/g'` 202 | ACTION=`osascript 2> /dev/null <<-EOF 203 | tell application "System Events" 204 | set theList to { ${LIST} } 205 | choose from list theList with title "Alfred Extension" with prompt "Several new installed applications were found.\n\nSelect which one you want to start:" OK button name "Select" cancel button name "Cancel" 206 | return result 207 | end tell 208 | EOF` 209 | 210 | if [ $? -eq 0 ];then 211 | find /Applications -type d -name "${ACTION}.app" -exec xattr -d com.apple.quarantine {} \; 2> /dev/null 212 | open -a "${ACTION}.app" 213 | echo "Application '${ACTION}' started." 214 | fi 215 | fi 216 | ;; 217 | never) 218 | # do nothing 219 | ;; 220 | esac 221 | fi 222 | } 223 | 224 | while getopts ":rd:s:" opt; do 225 | case $opt in 226 | r) 227 | INSTALL_MOST_RECENT="Y" 228 | ;; 229 | d) 230 | INSTALL_DIR=$OPTARG 231 | ;; 232 | s) 233 | AUTO_START=$OPTARG # ask, always or never 234 | ;; 235 | \?) 236 | echo "Invalid option: -$OPTARG" >&2 237 | ;; 238 | esac 239 | done 240 | shift $((OPTIND-1)) 241 | 242 | INSTALL_DIR=${INSTALL_DIR:-~/Downloads} 243 | AUTO_START=${AUTO_START:-"ask"} 244 | 245 | if [ -z "$1" ];then 246 | if [ "${INSTALL_MOST_RECENT}" = "Y" ];then 247 | FILENAMES=`bash recent.sh -d "${INSTALL_DIR}" -- "list 15" | grep -e "\.pkg$" -e "\.zip$" -e "\.dmg$" -e "\.app$" -e "\.mpkg$" -e "\.alfredextension$" -e "\.prefPane$" | head -n 1` 248 | fi 249 | else 250 | set -- $1 251 | shopt -s nocasematch 252 | case $1 in 253 | l|last) 254 | NUMBER_OF_FILES=${2:-1} 255 | FILENAMES=`bash recent.sh -d "${INSTALL_DIR}" -- "list 15" | grep -e "\.pkg$" -e "\.zip$" -e "\.dmg$" -e "\.app$" -e "\.mpkg$" -e "\.alfredextension$" -e "\.prefPane$" | head -n ${NUMBER_OF_FILES}` 256 | ;; 257 | [0-9]|[0-9][0-9]) 258 | FILENAMES=`bash recent.sh -d "${INSTALL_DIR}" -- "list 25" | head -n $1 | tail -n 1` 259 | shift 260 | if [ ! -z "$@" ];then 261 | for i in "$@";do 262 | FILENAMES="${FILENAMES}"$'\n'`bash recent.sh -d "${INSTALL_DIR}" -- "list 25" | head -n $1 | tail -n 1` 263 | done 264 | fi 265 | ;; 266 | *) 267 | case ${1:0:1} in 268 | /|\~) 269 | FILENAMES=${FILENAMES:-`cd ${INSTALL_DIR} && eval echo "$@"`} 270 | ;; 271 | *) 272 | if [ ${#1} -lt 3 ];then 273 | echo "Minimum length for software search is 3. Aborting." 274 | exit 1 275 | else 276 | FILENAMES=${FILENAMES:-`find "${INSTALL_DIR}" \( \( -type f -ipath "*${1}*" \) -o \( -type d -name "*.app" -prune -o -name "*.mpkg" -prune -o -name "*.prefPane" -prune \) \) -exec stat -f "%c%t%N" {} \; | sort -rn | grep -e "\.pkg$" -e "\.zip$" -e "\.dmg$" -e "\.app$" -e "\.mpkg$" -e "\.alfredextension$" -e "\.prefPane$" | head -n 1 | cut -f 2`} 277 | fi 278 | ;; 279 | esac 280 | ;; 281 | esac 282 | fi 283 | 284 | if [ -z "${FILENAMES}" ];then 285 | echo "Nothing to install. Aborting." 286 | exit 0 287 | fi 288 | 289 | IFS=$'\n' 290 | for i in $FILENAMES;do 291 | 292 | EXTENSION=${i##*.} 293 | 294 | case $EXTENSION in 295 | app|mpkg) 296 | if [ ! -d "${i}" ];then 297 | echo "Unable to find folder '${i}'. Aborting." 298 | exit 1 299 | fi 300 | ;; 301 | *) 302 | if [ ! -f "${i}" ];then 303 | echo "Unable to find file '${i}'. Aborting." 304 | exit 1 305 | fi 306 | ;; 307 | esac 308 | #echo "Installing ${i}." 309 | installSoftware "${i}" 310 | done 311 | unset IFS 312 | -------------------------------------------------------------------------------- /recent_downloads.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'find' 3 | require 'shellwords' 4 | load "alfred_feedback.rb" 5 | load "config.rb" 6 | load "download_progress.rb" 7 | 8 | Encoding.default_external = Encoding::UTF_8 9 | Encoding.default_internal = Encoding::UTF_8 10 | 11 | $config = RDW::Config.new 12 | 13 | Data_File = File.expand_path "recent_downloads.txt", RDW::Config::VOLATILE_DIR 14 | 15 | DIR = File.expand_path "~/Downloads" 16 | 17 | Dir.chdir DIR 18 | 19 | def get_entries(dir, max_depth) 20 | xs = [] 21 | dir = File.expand_path dir 22 | max_depth += dir.split(File::SEPARATOR).count 23 | Find.find(dir) do |path| 24 | if RDW.directory? path 25 | if path.split(File::SEPARATOR).count < max_depth 26 | xs <<= path 27 | next 28 | elsif path.split(File::SEPARATOR).count == max_depth 29 | xs <<= path 30 | end 31 | elsif !RDW.hidden? path 32 | xs <<= path 33 | end 34 | Find.prune 35 | end 36 | xs 37 | end 38 | 39 | results = [] 40 | File.open(Data_File, "r") do |file| 41 | content = file.gets(nil) 42 | results += content.split "\n" if content 43 | end if File.exist?(Data_File) && File.mtime($config.config_file_path) < File.mtime(Data_File) 44 | 45 | entries = get_entries ".", 1 46 | excludes = [File.expand_path(".")] 47 | $config.subfolders.each do |x| 48 | next if !File.exist? x["folder"] 49 | excludes <<= File.expand_path(x["folder"]) if x["exclude"] 50 | entries += get_entries x["folder"], x["depth"] 51 | end 52 | entries -= excludes 53 | entries.uniq! 54 | results = results & entries # remove entries that are no longer in the Downloads folder 55 | entries = entries - results # process only newer ones 56 | 57 | if entries.length > 0 58 | escaped_entries = entries.map do |entry| 59 | Shellwords.escape(entry) 60 | end 61 | 62 | time_values = `mdls -name kMDItemDateAdded -raw #{escaped_entries.join(' ')}`.split("\0") 63 | 64 | entries = entries.each_with_index.map do |entry, i| 65 | if time_values[i] == "(null)" || time_values[i].nil? 66 | time = File.mtime entry 67 | else 68 | time = Time.parse time_values[i] 69 | end 70 | {:name => entry, :time_added => time} 71 | end 72 | 73 | entries.sort! {|x, y| y[:time_added] <=> x[:time_added]} 74 | end 75 | 76 | results = entries.collect! {|x| x[:name]} + results 77 | File.open(Data_File, "w") do |file| 78 | results.each {|x| file.puts x} 79 | end 80 | 81 | query = ARGV[0].gsub(/\s+/, "") 82 | pattern = Regexp.compile("#{[*query.chars.to_a.map! {|c| Regexp.escape c}] * ".*"}", true) 83 | results.delete_if {|x| (File.basename(x) =~ pattern) == nil} 84 | 85 | # construct feedback 86 | feedback = Feedback.new 87 | if results.length > 0 88 | results = results.first($config['max-entries']) if $config['max-entries'] != :all 89 | results.each do |path| 90 | fullpath = File.expand_path path 91 | if path[path.length - 9, 9] == '.download' && dl_item = download_item(fullpath) 92 | feedback.add_item(dl_item) 93 | else 94 | feedback.add_item({:title => File.basename(path), :subtitle => path, :arg => fullpath, 95 | :icon => {:type => "fileicon", :name => fullpath}}) 96 | end 97 | end 98 | else 99 | feedback.add_item({:title => "No Match", :valid => "no"}) 100 | end 101 | 102 | puts feedback.to_xml 103 | --------------------------------------------------------------------------------