├── .document ├── .gitignore ├── LICENSE ├── README.markdown ├── Rakefile ├── VERSION ├── bin └── captured ├── captured.gemspec ├── etc ├── captured.yml-example └── launchd.plist.erb ├── lib ├── captured.rb └── captured │ ├── file_tracker.rb │ ├── file_uploader.rb │ ├── fs_events.rb │ ├── history.rb │ └── uploaders │ ├── eval_uploader.rb │ ├── imageshack_uploader.rb │ ├── imgur_uploader.rb │ └── scp_uploader.rb ├── resources ├── 2uparrow.png ├── action_run.png ├── captured-box.png ├── captured-empty-box.png ├── captured.png ├── green_check.png ├── growlnotify ├── red_star.png ├── red_x.png └── ruby.png └── spec ├── bin └── mockgrowlnotify ├── captured_spec.rb ├── file_uploader_spec.rb ├── fixtures ├── history └── scp_config.yml ├── history_spec.rb ├── spec_helper.rb └── uploader_specs ├── imageshack_uploader_spec.rb ├── imgur_uploader_spec.rb └── scp_uploader_spec.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | tmp 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is software is distributed under the same license as Ruby itself. See http://www.ruby-lang.org/en/LICENSE.txt. 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | captured 2 | ======== 3 | 4 | Quick screen capture and sharing for Mac OS X. 5 | 6 | _Note: I have switched my efforts to working on a native Mac version of Captured, which is avaliable on the [Mac App Store](http://itunes.apple.com/us/app/captured/id414675451?mt=12). I recomend it over the ruby version because it is faster, lighter on system resources, and has a slightly diffrent feature set. The native version is a paid app (only 99¢), and the ruby version will always be free and open source. Thanks, Chris_ 7 | 8 | Screen Capture Sharing Tool 9 | =========================== 10 | 11 | 12 | 13 | I made captured because I wanted to customize and extend screen capture sharing programs, it is really intended for the commandline savvy. 14 | 15 | So, I am making some assumptions about the environment that captured runs in. In particular it expects: 16 | 17 | * A decent understanding of installing ruby gems 18 | * That [Growl](http://growl.info/) is installed 19 | 20 | With that said, once things are installed and configured it really is handy. 21 | 22 | Install 23 | ======= 24 | 25 | To install captured: 26 | 27 | $ sudo gem install captured 28 | $ captured --install 29 | 30 | When you install an example config file to ~/.captured.yml, which has a few examples of possible configuration types. 31 | 32 | Using Captured 33 | ============== 34 | 35 | The main use is to upload a screen shot taken using OS X's built in screen capture. 36 | 37 | 1. Press ⌘-⇧-4 to capture 38 | 2. Paste the link 39 | 40 | Captured can also be used from the command line to easily share files. 41 | 42 | 1. Run `captured path/to/file` 43 | 2. Paste the link 44 | 45 | Configuration 46 | ============= 47 | 48 | By default captured uses Imgur as the default host, but you can configure it to upload and share images by other services. 49 | 50 | To edit the configuraiton: 51 | 52 | $ open -e ~/.captured.yml 53 | 54 | Type: Imgur 55 | ----------- 56 | The simple image sharer. The default option. 57 | 58 |
 59 | upload:
 60 |   type: imgur
 61 | 
62 | 63 | Type: Imageshack 64 | ---------------- 65 | This service is a little slower, but is free and easy. 66 | 67 |
 68 | upload:
 69 |   type: imageshack
 70 | 
71 | 72 | 73 | Type: scp 74 | --------- 75 | 76 | If you have you own web server scp is a very handy way to host your own captures. 77 | 78 | * user - optional if your remote user is the same as your local user 79 | * password - optional if you have setup key pair authentication 80 | * host - the remote host name 81 | * url - the public url to the remote host+path 82 | * path - the remote path to upload to 83 | 84 |
 85 | upload:
 86 |   type: scp
 87 |   user: user
 88 |   password: secret
 89 |   host: example.com
 90 |   path: path/to/captured/
 91 |   url: "http://example.com/captured/"
 92 | 
93 | 94 | Icons 95 | ===== 96 | 97 | Icons from the [Crystal Clear](http://www.everaldo.com/crystal/) icon set by [Everaldo Coelho](http://en.wikipedia.org/wiki/Everaldo_Coelho). – The icons are [licensed](http://www.everaldo.com/crystal/?action=license) under the [GNU Lesser General Public License (LGPL)](http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License). 98 | 99 | The Logo was made from the fantastic Vector Wood Signs by [DragonArt](http://dragonartz.wordpress.com) under the [Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License](http://creativecommons.org/licenses/by-nc-sa/3.0/us/). 100 | 101 | Copyright 102 | ========= 103 | 104 | Copyright (c) 2010 Christopher Sexton. See LICENSE for details. 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "captured" 8 | gem.summary = "Quick screenshot sharing for OS X" 9 | gem.description = "Because --4 is the single most useful shorcut in Macdom" 10 | gem.email = "csexton@gmail.com" 11 | gem.homepage = "http://github.com/csexton/captured" 12 | gem.authors = ["Christopher Sexton"] 13 | gem.add_dependency('imgur') 14 | gem.add_dependency('net-scp') 15 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 16 | gem.files = FileList["VERSION", "[A-Z]*.*", "{bin,etc,lib,features,resources,spec}/**/*"] 17 | gem.post_install_message = < ['gemcutter:release'] do 53 | puts "Released" 54 | end 55 | 56 | 57 | task :default => :spec 58 | 59 | require 'rake/rdoctask' 60 | Rake::RDocTask.new do |rdoc| 61 | if File.exist?('VERSION.yml') 62 | config = YAML.load(File.read('VERSION.yml')) 63 | version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}" 64 | else 65 | version = "" 66 | end 67 | 68 | rdoc.rdoc_dir = 'rdoc' 69 | rdoc.title = "captured #{version}" 70 | rdoc.rdoc_files.include('README*') 71 | rdoc.rdoc_files.include('lib/**/*.rb') 72 | end 73 | 74 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.2 2 | -------------------------------------------------------------------------------- /bin/captured: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'optparse' 4 | require 'fileutils' 5 | require "#{File.dirname(__FILE__)}/../lib/captured" 6 | 7 | options = {:config_file => "#{ENV['HOME']}/.captured.yml", 8 | :watch_path => "#{ENV['HOME']}/Desktop/", 9 | :watch_pattern => Captured.guess_watch_path} 10 | 11 | OptionParser.new do |opts| 12 | opts.summary_width = 25 13 | 14 | opts.banner = "captured: Quick screen capture and sharing on OS X" 15 | opts.banner = " Quick upload" 16 | 17 | opts.on('--help', "Print this message") do 18 | puts "#{opts}\n" 19 | exit 20 | end 21 | 22 | opts.on('--history', "Print History") do 23 | History.list 24 | exit 25 | end 26 | 27 | opts.on('--version', "Print the version") do 28 | puts "Captured #{File.open("#{File.dirname(__FILE__)}/../VERSION", "r").read}" 29 | exit 30 | end 31 | 32 | opts.on('--config-file=FILE', "Config file (Default: #{options[:config_file]})") do |file| 33 | options[:config_file] = file 34 | end 35 | 36 | opts.on('--watch-path=PATH', "Path to watch (Default: #{options[:watch_path]})") do |path| 37 | options[:watch_path] = path 38 | end 39 | 40 | opts.on('--watch-pattern=PATTERN', "Pattern to match (Default: #{options[:watch_pattern]})") do |path| 41 | options[:watch_path] = path 42 | end 43 | 44 | opts.on('--growlnotify=PATH', "Path to growlnotify (Default: #{options[:growl_path]})") do |file| 45 | options[:growl_path] = file 46 | end 47 | 48 | opts.on('--install', "Run at startup") do 49 | options[:install] = true 50 | end 51 | 52 | opts.on('--remove', "Remove captured from launchd") do 53 | puts "Uninstalling captured" 54 | File.delete "#{ENV['HOME']}/Library/LaunchAgents/com.codeography.captured" 55 | system "launchctl remove com.codeography.captured" 56 | exit 57 | end 58 | 59 | opts.on('--launchd', "Indicate that this was invoked via launchd") do 60 | options[:launchd] = true 61 | end 62 | 63 | opts.on('--watch', "Create a run loop that will watch for screenshots") do 64 | options[:launchd] = true 65 | end 66 | 67 | opts.on('--config', "Install a config file to #{Captured.config_file}") do 68 | puts "install config file!" 69 | if (!File.exists? Captured.config_file) 70 | FileUtils.copy("#{File.dirname(__FILE__)}/../etc/captured.yml-example", Captured.config_file) 71 | end 72 | exit 73 | end 74 | 75 | end.parse! 76 | 77 | if options[:install] == true 78 | puts "Installing captured" 79 | require 'erb' 80 | require 'pathname' 81 | if (File.exists? options[:config_file]) 82 | puts "Found existing config file at #{options[:config_file]}" 83 | else 84 | FileUtils.copy("#{File.dirname(__FILE__)}/../etc/captured.yml-example", options[:config_file]) 85 | puts "Copied example config file to #{options[:config_file]}" 86 | end 87 | program_path = Pathname.new($0).realpath 88 | watch_path = options[:watch_path] 89 | plist_path = "#{ENV['HOME']}/Library/LaunchAgents/" 90 | template = ERB.new(File.open("#{File.dirname(__FILE__)}/../etc/launchd.plist.erb", 'r').read) 91 | FileUtils.mkdir_p plist_path 92 | File.open("#{plist_path}/com.codeography.captured", 'w') do |f| 93 | f.write(template.result(binding)) 94 | end 95 | system "launchctl load ~/Library/LaunchAgents" 96 | exit 97 | end 98 | 99 | if options[:launchd] == true 100 | Captured::run_once! options 101 | exit 102 | end 103 | 104 | if options[:watch] == true 105 | Captured::run_and_watch! options 106 | exit 107 | end 108 | 109 | if ARGV[0] && File.exists?(ARGV[0]) 110 | FileUploader.upload(ARGV[0], options) 111 | exit 112 | end 113 | 114 | puts "Invalid option, run `captured --help` for usage" 115 | -------------------------------------------------------------------------------- /captured.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{captured} 8 | s.version = "0.4.1" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Christopher Sexton"] 12 | s.date = %q{2010-05-18} 13 | s.default_executable = %q{captured} 14 | s.description = %q{Because --4 is the single most useful shorcut in Macdom} 15 | s.email = %q{csexton@gmail.com} 16 | s.executables = ["captured"] 17 | s.extra_rdoc_files = [ 18 | "LICENSE", 19 | "README.markdown" 20 | ] 21 | s.files = [ 22 | "README.markdown", 23 | "VERSION", 24 | "bin/captured", 25 | "etc/captured.yml-example", 26 | "etc/launchd.plist.erb", 27 | "lib/captured.rb", 28 | "lib/captured/file_tracker.rb", 29 | "lib/captured/file_uploader.rb", 30 | "lib/captured/fs_events.rb", 31 | "lib/captured/history.rb", 32 | "lib/captured/uploaders/eval_uploader.rb", 33 | "lib/captured/uploaders/imageshack_uploader.rb", 34 | "lib/captured/uploaders/imgur_uploader.rb", 35 | "lib/captured/uploaders/scp_uploader.rb", 36 | "resources/2uparrow.png", 37 | "resources/action_run.png", 38 | "resources/captured-box.png", 39 | "resources/captured-empty-box.png", 40 | "resources/captured.png", 41 | "resources/green_check.png", 42 | "resources/growlnotify", 43 | "resources/red_star.png", 44 | "resources/red_x.png", 45 | "resources/ruby.png", 46 | "spec/bin/mockgrowlnotify", 47 | "spec/captured_spec.rb", 48 | "spec/file_uploader_spec.rb", 49 | "spec/fixtures/history", 50 | "spec/fixtures/scp_config.yml", 51 | "spec/history_spec.rb", 52 | "spec/spec_helper.rb", 53 | "spec/uploader_specs/imageshack_uploader_spec.rb", 54 | "spec/uploader_specs/imgur_uploader_spec.rb", 55 | "spec/uploader_specs/scp_uploader_spec.rb" 56 | ] 57 | s.homepage = %q{http://github.com/csexton/captured} 58 | s.post_install_message = %q{ 59 | ========================================================================= 60 | 61 | Thanks for installing Captured! You can now run: 62 | 63 | captured --install to setup launchd to run captured in the background 64 | 65 | When you install an example config file to ~/.captured.yml, which has a 66 | few examples of possible configuration types. 67 | 68 | ========================================================================= 69 | 70 | } 71 | s.rdoc_options = ["--charset=UTF-8"] 72 | s.require_paths = ["lib"] 73 | s.rubyforge_project = %q{captured} 74 | s.rubygems_version = %q{1.3.5} 75 | s.summary = %q{Quick screenshot sharing for OS X} 76 | s.test_files = [ 77 | "spec/captured_spec.rb", 78 | "spec/file_uploader_spec.rb", 79 | "spec/history_spec.rb", 80 | "spec/spec_helper.rb", 81 | "spec/uploader_specs/imageshack_uploader_spec.rb", 82 | "spec/uploader_specs/imgur_uploader_spec.rb", 83 | "spec/uploader_specs/scp_uploader_spec.rb" 84 | ] 85 | 86 | if s.respond_to? :specification_version then 87 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 88 | s.specification_version = 3 89 | 90 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 91 | s.add_runtime_dependency(%q, [">= 0"]) 92 | s.add_runtime_dependency(%q, [">= 0"]) 93 | else 94 | s.add_dependency(%q, [">= 0"]) 95 | s.add_dependency(%q, [">= 0"]) 96 | end 97 | else 98 | s.add_dependency(%q, [">= 0"]) 99 | s.add_dependency(%q, [">= 0"]) 100 | end 101 | end 102 | 103 | -------------------------------------------------------------------------------- /etc/captured.yml-example: -------------------------------------------------------------------------------- 1 | # Example captured configuration file 2 | # 3 | 4 | # Upload type: Imgur 5 | # ================================= 6 | # 7 | # The simple image sharer 8 | 9 | upload: 10 | type: imgur 11 | 12 | # Upload type: Image Shack 13 | # ================================= 14 | 15 | #upload: 16 | # type: imageshack 17 | 18 | # 19 | # Powerful upload type: scp 20 | # ========================= 21 | # 22 | # Standard scp, using the ruby net/ssh library. 23 | # 24 | # * user - optinal if your remote user is the same as your local user 25 | # * password - optional if you have setup key pair authentication 26 | # * host - the remote host name 27 | # * url - the public url to the remote host+path 28 | # * path - the remote path to upload to 29 | 30 | #upload: 31 | # type: scp 32 | # user: user 33 | # host: example.com 34 | # path: example.com/captured/ 35 | # url: "http://example.com/captured/" 36 | 37 | # 38 | # Advanced upload type: Eval 39 | # ========================== 40 | # 41 | # Complete control for the complete nerd. This allows you to execute arbtrary 42 | # ruby code when a matching file is found. Normally this would be used to 43 | # invoke a command line upload (such as scp, curl, etc). 44 | # 45 | # One advantage with calling scp this way is it will be aware of all the custom 46 | # settings made in ~/.ssh/config. 47 | # 48 | # You have access to two local varibles: 49 | # * file - the local file to upload 50 | # * remote_name - the hashed name to use on the server 51 | # 52 | # Simple scp via eval command 53 | 54 | #upload: 55 | # type: eval 56 | # command: system "scp '#{file}' 'user@example.com:example.com/captured/#{remote_name}'" 57 | # url: "http://example.com/captured/" 58 | 59 | # Curl post to twitpic.com 60 | 61 | #upload: 62 | # type: eval 63 | # url: "http://twitter.com/user/" 64 | # command: | 65 | # remote_path="http://twitter.com/user" 66 | # system "curl -F media='@#{file}' -F username=user -F password=secret -F message=Captured http://twitpic.com/api/uploadAndPost" 67 | # 68 | -------------------------------------------------------------------------------- /etc/launchd.plist.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.codeography.captured 7 | LowPriorityIO 8 | 9 | Program 10 | <%= program_path %> 11 | ProgramArguments 12 | 13 | captured 14 | --launchd 15 | 16 | WatchPaths 17 | 18 | <%= watch_path %> 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/captured.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/captured/history" 2 | require "#{File.dirname(__FILE__)}/captured/file_tracker" 3 | require "#{File.dirname(__FILE__)}/captured/file_uploader" 4 | 5 | class Captured 6 | def self.config_file 7 | "#{ENV['HOME']}/.captured.yml" 8 | end 9 | 10 | def self.guess_watch_path 11 | # This should work for 10.5, and 10.6 12 | "{Picture,Screen}*.png" 13 | end 14 | 15 | def self.run_once!(options) 16 | watch_path = options[:watch_path] || "#{ENV['HOME']}/Desktop/" 17 | Dir["#{watch_path}#{options[:watch_pattern]}"].each do |file| 18 | if (File.mtime(file).to_i > (Time.now.to_i-10)) 19 | puts "#{file} is new" 20 | FileUploader.upload(file, options) 21 | end 22 | end 23 | end 24 | 25 | # Depricated this is now handeled by launchd 26 | def self.run_and_watch!(options) 27 | require 'captured/fs_events' 28 | watch_path = options[:watch_path] || "#{ENV['HOME']}/Desktop/" 29 | tracker = FileTracker.new(options) 30 | tracker.scan([desktop_dir], :existing) 31 | e = FSEvents.new(watch_path) 32 | e.run do |paths| 33 | tracker.scan paths, :pending 34 | tracker.each_pending do |file| 35 | FileUploader.upload(file, options) 36 | tracker.mark_processed(file) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/captured/file_tracker.rb: -------------------------------------------------------------------------------- 1 | class FileTracker 2 | attr_accessor :tracked_files 3 | 4 | def initialize(options) 5 | @options = options 6 | @tracked_files = {} 7 | end 8 | 9 | def scan(paths, state) 10 | puts "Scanning #{paths}" 11 | paths.each do |path| 12 | Dir["#{path}#{@options[:watch_pattern]}"].each do |file| 13 | self.add file, state 14 | end 15 | end 16 | end 17 | 18 | def add(file, state) 19 | unless @tracked_files[file] 20 | puts "Adding #{file} to tracked files as #{state}" 21 | @tracked_files[file] = state 22 | end 23 | end 24 | 25 | def mark_processed(file) 26 | puts "Marking #{file} processed" 27 | @tracked_files[file] = :processed 28 | end 29 | 30 | def each_pending 31 | @tracked_files.each_pair do |key, value| 32 | if(value == :pending) 33 | yield(key) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/captured/file_uploader.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require 'erb' 3 | require 'yaml' 4 | 5 | class FileUploader 6 | def self.upload(file, options) 7 | self.new(options).process_upload(file) 8 | end 9 | 10 | def initialize(options) 11 | @growl_path = options[:growl_path] || "#{File.dirname(File.expand_path(__FILE__))}/../../resources/growlnotify" 12 | @config = YAML.load_file(options[:config_file]) 13 | case @config['upload']['type'] 14 | when "eval" 15 | require File.expand_path(File.dirname(__FILE__) + '/uploaders/eval_uploader') 16 | @uploader = EvalUploader.new(@config) 17 | when "scp" 18 | require File.expand_path(File.dirname(__FILE__) + '/uploaders/scp_uploader') 19 | @uploader = ScpUploader.new(@config) 20 | when "imgur" 21 | require File.expand_path(File.dirname(__FILE__) + '/uploaders/imgur_uploader') 22 | @uploader = ImgurUploader.new 23 | when "imageshack" 24 | require File.expand_path(File.dirname(__FILE__) + '/uploaders/imageshack_uploader') 25 | @uploader = ImageshackUploader.new(@config) 26 | else 27 | raise "Invalid Type" 28 | end 29 | rescue 30 | growl "Unable to load config file" 31 | raise "Unable to load config file" 32 | end 33 | 34 | def pbcopy(str) 35 | system "ruby -e \"print '#{str}'\" | pbcopy" 36 | # I prefer the following method but it was being intermitant about actually 37 | # coping to the clipboard. 38 | #IO.popen('pbcopy','w+') do |pbc| 39 | # pbc.print str 40 | #end 41 | rescue 42 | raise "Copy to clipboard failed" 43 | end 44 | 45 | def process_upload(file) 46 | remote_name = Digest::MD5.hexdigest(file+Time.now.to_i.to_s) + File.extname(file) 47 | growl("Processing Upload", "#{File.dirname(File.expand_path(__FILE__))}/../../resources/action_run.png") 48 | @uploader.upload(file) 49 | remote_path = @uploader.url 50 | puts "Uploaded '#{file}' to '#{remote_path}'" 51 | pbcopy remote_path 52 | growl("Upload Succeeded", "#{File.dirname(File.expand_path(__FILE__))}/../../resources/green_check.png") 53 | History.append(file, remote_path) 54 | rescue => e 55 | puts e 56 | puts e.backtrace 57 | growl(e) 58 | end 59 | 60 | def growl(msg, image = "#{File.dirname(File.expand_path(__FILE__))}/../../resources/red_x.png") 61 | puts "grr: #{msg}" 62 | if File.exists? @growl_path 63 | raise "Growl Failed" unless system("#{@growl_path} -t 'Captured' -m '#{msg}' --image '#{image}'") 64 | end 65 | rescue 66 | puts "Growl Notify Error" 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/captured/fs_events.rb: -------------------------------------------------------------------------------- 1 | require 'osx/foundation' 2 | OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework' 3 | 4 | class FSEvents 5 | def initialize(dir = "#{HOME}/Desktop") 6 | @watch_dir = dir 7 | end 8 | 9 | def run 10 | callback = proc do |stream, ctx, numEvents, paths, marks, eventIDs| 11 | 12 | #p stream 13 | #p ctx 14 | #p numEvents 15 | #p paths.methods 16 | #p marks.methods 17 | #p eventIDs 18 | 19 | paths.regard_as('*') 20 | rpaths = [] 21 | numEvents.times { |i| rpaths << paths[i] } 22 | 23 | yield(*rpaths) 24 | end 25 | 26 | allocator = OSX::KCFAllocatorDefault 27 | context = nil 28 | path = [@watch_dir] #[Dir.pwd] 29 | sinceWhen = OSX::KFSEventStreamEventIdSinceNow 30 | latency = 1.0 31 | flags = 0 32 | 33 | stream = OSX::FSEventStreamCreate(allocator, callback, context, path, sinceWhen, latency, flags) 34 | unless stream 35 | puts "Failed to create stream" 36 | exit 37 | end 38 | 39 | OSX::FSEventStreamScheduleWithRunLoop(stream, OSX::CFRunLoopGetCurrent(), OSX::KCFRunLoopDefaultMode) 40 | unless OSX::FSEventStreamStart(stream) 41 | puts "Failed to start stream" 42 | exit 43 | end 44 | 45 | puts "Watching #{path}" 46 | 47 | OSX::CFRunLoopRun() 48 | rescue Interrupt 49 | OSX::FSEventStreamStop(stream) 50 | OSX::FSEventStreamInvalidate(stream) 51 | OSX::FSEventStreamRelease(stream) 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/captured/history.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | class History 4 | 5 | def self.file_path 6 | "#{ENV['HOME']}/.captured_history" 7 | end 8 | 9 | def self.time_stamp 10 | DateTime.now.strftime("%m/%d/%Y-%I:%M%p") 11 | end 12 | 13 | def self.append(original_name, remote_path) 14 | File.open(History.file_path, 'a') do |f| 15 | f.puts(History.format_line(original_name, remote_path)) 16 | end 17 | end 18 | 19 | def self.format_line(original_name, remote_path) 20 | "#{History.time_stamp} \"#{original_name}\" #{remote_path}" 21 | end 22 | 23 | def self.list 24 | if File.exists? History.file_path 25 | File.open(History.file_path).each do |line| 26 | puts line 27 | end 28 | else 29 | puts "You ain't got no history yet" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/captured/uploaders/eval_uploader.rb: -------------------------------------------------------------------------------- 1 | class EvalUploader 2 | attr_accessor :url 3 | 4 | def initialize(config = {}) 5 | @config = config 6 | end 7 | 8 | def gen_remote_name(file) 9 | Digest::MD5.hexdigest(file+Time.now.to_i.to_s) + File.extname(file) 10 | end 11 | 12 | def upload(file) 13 | remote_path = nil 14 | remote_name = gen_remote_name(file) 15 | unless eval @config['upload']['command'] 16 | raise "Upload failed: Bad Eval in config file" 17 | end 18 | # if the eval defines remote_path we will copy that to the clipboard 19 | # otherwise we compute it ouselves 20 | @url = remote_path || "#{@config['upload']['url']}#{remote_name}" 21 | @url 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/captured/uploaders/imageshack_uploader.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'cgi' 4 | 5 | # Adapted from http://codesnippets.joyent.com/posts/show/1156 6 | class ImageshackUploader 7 | attr_reader :url 8 | USER_AGENT = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3" 9 | BOUNDARY = '----------PuSHerInDaBUSH_$' 10 | 11 | def initialize(config = {}) 12 | @config = config 13 | @shack_id = config['upload']['shackid'] || "captured" 14 | end 15 | 16 | def upload(file_name) 17 | unless file_name =~ /jpe?g|png|gif|bmp|tif|tiff|swf$/ 18 | raise(NonImageTypeError, 'Expected image file.') 19 | end 20 | @img = file_name 21 | @posted_url, @hosturi, @res = "","","" 22 | @header, @params = {}, {} 23 | @header['Cookie'] = "myimages=#{@shack_id}" 24 | @header['User-Agent'] = USER_AGENT 25 | @params['uploadtype'] = 'on' 26 | @params['brand'] = '' 27 | @params['refer'] = '' 28 | @params['MAX_FILE_SIZE'] = '13145728' 29 | @params['optimage'] = '0' 30 | @params['rembar'] = '1' 31 | transfer 32 | getdirect 33 | @url = @posted_url.gsub("content.php?page=done&l=", "") 34 | end 35 | 36 | def prepare_multipart ( params ) 37 | fp = [] 38 | params.each do |k,v| 39 | if v.respond_to?(:read) 40 | fp.push(FileParam.new(k,v.path,v.read)) 41 | else fp.push(Param.new(k,v)) 42 | end 43 | end 44 | query = fp.collect {|p| "--" + BOUNDARY + "\r\n" + p.to_multipart }.join("") + "--" + BOUNDARY + "--" 45 | return query 46 | end 47 | 48 | def prepFile(path_to_file) 49 | file = File.new(path_to_file) 50 | @header['Content-Type'] = "multipart/form-data, boundary=" + BOUNDARY + " " 51 | @params['url'] = 'paste image url here' 52 | @params['fileupload'] = file 53 | $query = prepare_multipart(@params) 54 | file.close 55 | end 56 | 57 | def locate(path) 58 | path !~ /^http/ ? "local" : "remote" 59 | end 60 | 61 | def process_upload( query, headers={} ) 62 | Net::HTTP.start(@hosturi.host) do | http | 63 | http.post(@hosturi.path, query, headers); 64 | end 65 | end 66 | 67 | def transload(url) 68 | @header['Content-Type'] = 'form-data' 69 | @params['url'] = url 70 | @params['fileupload'] = '' 71 | postreq = Net::HTTP::Post.new(@hosturi.path, @header) 72 | postreq.set_form_data(@params) 73 | return Net::HTTP.new(@hosturi.host, @hosturi.port).start { |http| http.request(postreq) } 74 | end 75 | 76 | def transfer 77 | case locate(@img) 78 | when "local" 79 | @hosturi = URI.parse('http://load.imageshack.us/index.php') 80 | prepFile(@img) 81 | @res = process_upload($query,@header) 82 | when "remote" 83 | @hosturi = URI.parse('http://imageshack.us/transload.php') 84 | @res = transload(@img) 85 | end 86 | end 87 | 88 | def getdirect 89 | puts @res.header 90 | puts @res.body 91 | @posted_url = @res.header['location'] 92 | end 93 | 94 | end 95 | 96 | class Param 97 | attr_accessor :k, :v 98 | 99 | def initialize(k,v) 100 | @k = k 101 | @v = v 102 | end 103 | 104 | def to_multipart 105 | return "Content-Disposition: form-data; name=\"#{CGI::escape(k)}\"\r\n\r\n#{v}\r\n" 106 | end 107 | end 108 | 109 | class FileParam 110 | attr_accessor :k, :filename, :content 111 | 112 | def initialize(k, filename, content) 113 | @k = k 114 | @filename = filename 115 | @content = content 116 | @extension_index = { 117 | 'jpg' => "image/jpeg", 118 | 'jpeg' => "image/jpeg", 119 | 'png' => "image/png", 120 | 'bmp' => "image/bmpimage/x-bmp", 121 | 'tiff' => "image/tiff", 122 | 'tif' => "image/tiff"} 123 | end 124 | 125 | def type_for(filename) 126 | ext = filename.chomp.downcase.gsub(/.*\./o, '') 127 | @extension_index[ext] 128 | end 129 | 130 | 131 | def to_multipart 132 | return "Content-Disposition: form-data; name=\"#{CGI::escape(k)}\"; filename=\"#{filename}\"\r\n" + 133 | "Content-Type: #{type_for(@filename)}\r\n\r\n" + content + "\r\n" 134 | end 135 | end 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /lib/captured/uploaders/imgur_uploader.rb: -------------------------------------------------------------------------------- 1 | require 'imgur' 2 | 3 | class ImgurUploader 4 | attr_accessor :url 5 | API_KEY = "f4fa5e1e9974405c62117a8a84fbde46" 6 | 7 | def upload(file) 8 | puts "Uploading #{file}" 9 | @url = Imgur::API.new('f4fa5e1e9974405c62117a8a84fbde46').upload_file(file)["imgur_page"] 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/captured/uploaders/scp_uploader.rb: -------------------------------------------------------------------------------- 1 | class ScpUploader 2 | attr_accessor :url 3 | 4 | def initialize(config = {}) 5 | @config = config 6 | end 7 | 8 | def gen_remote_name(file) 9 | Digest::MD5.hexdigest(file+Time.now.to_i.to_s) + File.extname(file) 10 | end 11 | 12 | def upload(file) 13 | puts "Uploading #{file}" 14 | # TODO: This needs to be called from file upload 15 | # and this calss needs to be completed 16 | # maybe some tests 17 | require 'net/scp' 18 | require 'etc' 19 | settings = @config['upload'] 20 | remote_name = gen_remote_name(file) 21 | puts Net::SCP.upload!(settings['host'], 22 | settings['user'] || Etc.getlogin, 23 | file, 24 | settings['path']+remote_name, 25 | :password => settings['password']) 26 | 27 | @url = "#{@config['upload']['url']}#{remote_name}" 28 | @url 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /resources/2uparrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/2uparrow.png -------------------------------------------------------------------------------- /resources/action_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/action_run.png -------------------------------------------------------------------------------- /resources/captured-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/captured-box.png -------------------------------------------------------------------------------- /resources/captured-empty-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/captured-empty-box.png -------------------------------------------------------------------------------- /resources/captured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/captured.png -------------------------------------------------------------------------------- /resources/green_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/green_check.png -------------------------------------------------------------------------------- /resources/growlnotify: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/growlnotify -------------------------------------------------------------------------------- /resources/red_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/red_star.png -------------------------------------------------------------------------------- /resources/red_x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/red_x.png -------------------------------------------------------------------------------- /resources/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csexton/captured-ruby/4badb3386eaf6a9ebfba471f2f09dda00632e3c6/resources/ruby.png -------------------------------------------------------------------------------- /spec/bin/mockgrowlnotify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'optparse' 3 | 4 | def red(s); colorize(s, "\e[0m\e[31m"); end 5 | def green(s); colorize(s, "\e[0m\e[32m"); end 6 | def dark_green(s); colorize(s, "\e[32m"); end 7 | def yellow(s); colorize(s, "\e[0m\e[33m"); end 8 | def blue(s); colorize(s, "\e[0m\e[34m"); end 9 | def dark_blue(s); colorize(s, "\e[34m"); end 10 | def pur(s); colorize(s, "\e[0m\e[35m"); end 11 | def colorize(text, color_code) "#{color_code}#{text}\e[0m" end 12 | 13 | def log(msg) 14 | puts yellow " #{msg}" 15 | end 16 | 17 | options = {} 18 | OptionParser.new do |opts| 19 | # -h,--help Display this help 20 | # -v,--version Display version number 21 | # -n,--name Set the name of the application that sends the notification 22 | # [Default: growlnotify] 23 | # -s,--sticky Make the notification sticky 24 | # -a,--appIcon Specify an application name to take the icon from 25 | # -i,--icon Specify a file type or extension to look up for the 26 | # notification icon 27 | # -I,--iconpath Specify a file whose icon will be the notification icon 28 | # --image Specify an image file to be used for the notification icon 29 | 30 | opts.on('--image IMAGE', "Specify an image file to be used for the notification icon") do |img| 31 | options[:image] = true 32 | if !File.exists? img 33 | log "GROWL ERROR: Image file does not exist" 34 | exit 1 35 | end 36 | end 37 | 38 | # -m,--message Sets the message to be used instead of using stdin 39 | opts.on('-m', '--message MSG', "Specify an image file to be used for the notification icon") do |msg| 40 | options[:message] = true 41 | if !msg 42 | log "GROWL ERROR: No message" 43 | exit 1 44 | else 45 | log "growl: #{msg}" 46 | end 47 | end 48 | # Passing - as the argument means read from stdin 49 | # -p,--priority Specify an int or named key (default is 0) 50 | # -d,--identifier Specify a notification identifier (used for coalescing) 51 | # -H,--host Specify a hostname to which to send a remote notification. 52 | # -P,--password Password used for remote notifications. 53 | # -u,--udp Use UDP instead of DO to send a remote notification. 54 | # --port Port number for UDP notifications. 55 | # -A,--auth Specify digest algorithm for UDP authentication. 56 | # Either MD5 [Default], SHA256 or NONE. 57 | # -c,--crypt Encrypt UDP notifications. 58 | # -w,--wait Wait until the notification has been dismissed. 59 | # --progress Set a progress value for this notification. 60 | 61 | # -t,--title Does nothing. Any text following will be treated as the 62 | # title because that's the default argument behaviour 63 | opts.on('-t', "Does nothing") do |msg| 64 | options[:title] = true 65 | if !msg 66 | log "GROWL ERROR: No title" 67 | exit 1 68 | end 69 | log "growl: #{msg}" 70 | end 71 | 72 | #puts " Grrrr! #{ARGV.inspect}" 73 | end.parse! 74 | 75 | -------------------------------------------------------------------------------- /spec/captured_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "File Uploader" do 4 | before(:all) do 5 | # This is run once and only once, before all of the examples 6 | 7 | #Make a backup of the clipboard 8 | $pb_backup = `pbpaste` 9 | 10 | system "mkdir -p #{File.dirname(__FILE__) + '/../tmp/watch_path'}" 11 | @options = {:config_file => File.dirname(__FILE__) + '/fixtures/scp_config.yml', 12 | :watch_path => File.dirname(__FILE__) + '/../tmp/watch_path', 13 | :watch_pattern => Captured.guess_watch_path, 14 | :growl_path => "#{File.dirname(File.expand_path(__FILE__))}/../resources/growlnotify" } 15 | end 16 | 17 | after(:all) do 18 | # This is run once and only once, after all of the examples 19 | # Restore the clipboard contents 20 | FileUploader.new(@options).pbcopy $pb_backup 21 | end 22 | 23 | 24 | it "should copy text to the system clipboard" do 25 | fu = FileUploader.new @options 26 | str = "Testing captured is fun for you" 27 | fu.pbcopy str 28 | `pbpaste`.should == str 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/file_uploader_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "File Uploader" do 4 | before(:all) do 5 | # This is run once and only once, before all of the examples 6 | 7 | #Make a backup of the clipboard 8 | $pb_backup = `pbpaste` 9 | 10 | system "mkdir -p #{File.dirname(__FILE__) + '/../tmp/watch_path'}" 11 | @options = {:config_file => File.dirname(__FILE__) + '/fixtures/scp_config.yml', 12 | :watch_path => File.dirname(__FILE__) + '/../tmp/watch_path', 13 | :watch_pattern => Captured.guess_watch_path, 14 | :growl_path => File.dirname(__FILE__) + '/bin/mockgrowlnotify'} 15 | end 16 | 17 | after(:all) do 18 | # This is run once and only once, after all of the examples 19 | # Restore the clipboard contents 20 | FileUploader.new(@options).pbcopy $pb_backup 21 | end 22 | 23 | 24 | it "should copy text to the system clipboard" do 25 | fu = FileUploader.new @options 26 | str = "Testing captured is fun for you" 27 | fu.pbcopy str 28 | `pbpaste`.should == str 29 | end 30 | it "should call growl" do 31 | fu = FileUploader.new @options 32 | str = "Testing captured is fun for you" 33 | fu.growl str 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/history: -------------------------------------------------------------------------------- 1 | 01/11/2010 02:47PM original_file remote_path 2 | 01/11/2010 02:48PM original_file remote_path 3 | 01/11/2010 02:49PM original_file remote_path 4 | -------------------------------------------------------------------------------- /spec/fixtures/scp_config.yml: -------------------------------------------------------------------------------- 1 | upload: 2 | type: scp 3 | user: user 4 | host: example.com 5 | path: example.com/captured/ 6 | url: "http://example.com/captured/" 7 | 8 | -------------------------------------------------------------------------------- /spec/history_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "History" do 4 | before(:each) do 5 | @date_time = mock(DateTime) 6 | @date_time.stub!(:strftime).and_return("01/11/2010 02:48PM") 7 | DateTime.stub!(:now).and_return(@date_time) 8 | History.stub!(:file_path).and_return("#{File.dirname(__FILE__)}/fixtures/history") 9 | end 10 | 11 | it "should format a line" do 12 | line = History.format_line("original_name", "remote_path") 13 | line.should == "01/11/2010 02:48PM original_name remote_path" 14 | end 15 | 16 | it "should list the history" do 17 | History.should respond_to(:list) 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'spec' 3 | 4 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 5 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 6 | require 'captured' 7 | 8 | CONFIG = YAML.load_file("#{ENV['HOME']}/.captured.yml") 9 | 10 | Spec::Runner.configure do |config| 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/uploader_specs/imageshack_uploader_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../../lib/captured/uploaders/imageshack_uploader') 3 | 4 | if CONFIG['imageshack_spec'] 5 | describe "Imageshack File Uploader" do 6 | it "should upload to the server" do 7 | config = {"upload"=>{"url"=>"http://fuzzymonk.com/captured/", 8 | "type"=>"imageshack", 9 | "shackid"=>"capturedspec"}} 10 | 11 | @uploader = ImageshackUploader.new(config) 12 | @uploader.upload(File.expand_path(File.dirname(__FILE__) + '/../../resources/captured.png')) 13 | system "open #{@uploader.url}" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/uploader_specs/imgur_uploader_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../../lib/captured/uploaders/imgur_uploader') 3 | 4 | if CONFIG['imgur_spec'] 5 | describe "Imgur File Uploader" do 6 | it "should upload to the server" do 7 | # This spec requires a scp_spec section in the config file with the 8 | # scp settings 9 | config = CONFIG['imgur_spec'] 10 | @uploader = ImgurUploader.new 11 | @uploader.upload(File.expand_path(File.dirname(__FILE__) + '/../../resources/captured.png')) 12 | system "open #{@uploader.url}" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/uploader_specs/scp_uploader_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/../../lib/captured/uploaders/scp_uploader') 3 | 4 | if CONFIG['scp_spec'] 5 | describe "SCP File Uploader" do 6 | it "should upload to the server" do 7 | # This spec requires a scp_spec section in the config file with the 8 | # scp settings 9 | config = YAML.load_file("#{ENV['HOME']}/.captured.yml")['scp_spec'] 10 | @uploader = ScpUploader.new(config) 11 | @uploader.upload(File.expand_path(File.dirname(__FILE__) + '/../../resources/captured.png')) 12 | system "open #{@uploader.url}" 13 | end 14 | end 15 | end 16 | --------------------------------------------------------------------------------