['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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------