├── .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 |
--------------------------------------------------------------------------------