├── .gitignore
├── spec
├── spec.opts
├── spec_helper.rb
├── application_spec.rb
├── actions
│ ├── skype_status_spec.rb
│ ├── terminal_spec.rb
│ ├── quit_applications_spec.rb
│ ├── launch_applications_spec.rb
│ └── quit_and_block_application_spec.rb
└── caprese_action_spec.rb
├── geminstaller.yml
├── actions
├── launch_applications.rb
├── quit_applications.rb
├── quit_and_block_applications.rb
├── ichat_status.rb
├── terminal.rb
├── adium_status.rb
├── skype_status.rb
├── block_domains.rb
└── campfire.rb
├── lib
├── config_helpers.rb
├── application_action.rb
├── growl_helper.rb
├── caprese_configurator.rb
├── env.rb
├── application.rb
├── caprese.rb
└── caprese_action.rb
├── caprese
├── config.rb.sample
└── README.textile
/.gitignore:
--------------------------------------------------------------------------------
1 | /config.rb
2 |
--------------------------------------------------------------------------------
/spec/spec.opts:
--------------------------------------------------------------------------------
1 | --format
2 | specdoc
3 | --loadby
4 | mtime
5 | --diff
6 |
7 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'spec'
3 |
4 | require File.expand_path("../lib/env.rb", File.dirname(__FILE__))
5 |
6 | alias running lambda
7 |
--------------------------------------------------------------------------------
/geminstaller.yml:
--------------------------------------------------------------------------------
1 | defaults:
2 | install_options: "--no-ri --no-rdoc"
3 | gems:
4 | - { name: 'tinder', version: '= 1.8.0' }
5 | - { name: 'rb-appscript', version: '>= 0.5.3' }
6 | - { name: 'activesupport', version: '>= 2.3.4' }
7 |
--------------------------------------------------------------------------------
/actions/launch_applications.rb:
--------------------------------------------------------------------------------
1 | class LaunchApplications < CapreseAction
2 | include ApplicationAction
3 |
4 | def start
5 | Application.hide_all
6 | target_apps.each { |app| app.activate! }
7 | end
8 |
9 | def stop
10 | end
11 | end
--------------------------------------------------------------------------------
/actions/quit_applications.rb:
--------------------------------------------------------------------------------
1 | class QuitApplications < CapreseAction
2 | include ApplicationAction
3 |
4 | def start
5 | target_apps.each do |application|
6 | application.quit! if application.running?
7 | end
8 | end
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/lib/config_helpers.rb:
--------------------------------------------------------------------------------
1 | module ConfigHelpers
2 | def start_time
3 | Time.now
4 | end
5 |
6 | def stop_time
7 | (Time.now + (ENV['DURATION'] || ENV['POMODORO_DURATION'] || 25).to_i * 60)
8 | end
9 |
10 | attr_writer :description
11 | def description
12 | @description ||= (ENV["DESCRIPTION"] || ENV["POMODORO_DESCRIPTION"]).to_s.dup
13 | end
14 | end
--------------------------------------------------------------------------------
/actions/quit_and_block_applications.rb:
--------------------------------------------------------------------------------
1 | class QuitAndBlockApplications < CapreseAction
2 | include ApplicationAction
3 |
4 | def start
5 | target_apps.each do |application|
6 | application.quit! if application.running?
7 | application.block!
8 | end
9 | end
10 |
11 | def stop
12 | target_apps.each do |application|
13 | application.unblock!
14 | end
15 | end
16 | end
17 |
18 |
--------------------------------------------------------------------------------
/spec/application_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("spec_helper", File.dirname(__FILE__))
2 |
3 | describe Application do
4 | describe "#running?, #activate!, and #quit!" do
5 | it "detects if an application is running" do
6 | app = Application.new("Calculator")
7 | app.activate!
8 | sleep 0.5
9 | app.running?.should be_true
10 | app.quit!
11 | sleep 0.5
12 | app.running?.should be_false
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/caprese:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require File.dirname(__FILE__) + '/lib/env.rb'
3 |
4 | ArgParser.extract_env_variables(ARGV)
5 | action = ARGV.shift
6 |
7 | Actions = {
8 | "start" => lambda { Caprese.start },
9 | "stop" => lambda { Caprese.stop },
10 | "check" => lambda { Caprese.check_config && puts("Configuration is valid") }
11 | }
12 |
13 | if action && Actions[action]
14 | Actions[action].call
15 | else
16 | puts "Usage: #{$0} [#{Actions.keys.sort.join '|'}] [ENV_VARIABLE=VALUE ...]"
17 | end
18 |
--------------------------------------------------------------------------------
/actions/ichat_status.rb:
--------------------------------------------------------------------------------
1 | class IchatStatus < CapreseAction
2 | include Appscript
3 |
4 | config_schema({:away => String, :available => String})
5 |
6 | def start
7 | ichat_accounts.status.set(:away)
8 | ichat_accounts.status_message.set(config[:away])
9 | end
10 |
11 | def stop
12 | ichat_accounts.status.set(:available)
13 | ichat_accounts.status_message.set(config[:available])
14 | end
15 |
16 | def ichat_accounts
17 | @ichat_accounts ||= app("iChat").services
18 | end
19 | end
--------------------------------------------------------------------------------
/actions/terminal.rb:
--------------------------------------------------------------------------------
1 | class Terminal < CapreseAction
2 | include Appscript
3 | config_schema [String]
4 |
5 | def start
6 | unless Terminal.caprese_application.running?
7 | Terminal.caprese_application.activate!
8 | app = Terminal.caprese_application.handle
9 | app.do_script(config, :in => app.windows.first)
10 | end
11 | end
12 |
13 | def stop
14 | end
15 |
16 | class << self
17 | def caprese_application
18 | @@terminal ||= Application.new("Terminal")
19 | end
20 | end
21 |
22 | end
--------------------------------------------------------------------------------
/lib/application_action.rb:
--------------------------------------------------------------------------------
1 | module ApplicationAction
2 | def self.included(klass)
3 | klass.config_schema [String]
4 | end
5 |
6 | def target_apps
7 | @target_apps ||= config.map do |app_config|
8 | Application.new(app_config)
9 | end
10 | end
11 |
12 | def validate
13 | return unless super
14 |
15 | target_apps.each do |application|
16 | add_error("I can't find application #{application.name} installed in #{application.path_candidates.inspect}") unless application.exists?
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/growl_helper.rb:
--------------------------------------------------------------------------------
1 | module GrowlHelper
2 | include Appscript
3 | extend self
4 |
5 | def growl(title, message)
6 | return unless ::Application.new("GrowlHelperApp").running?
7 | return if message.nil? || title.nil?
8 | app("GrowlHelperApp").register(:all_notifications => [title], :as_application => APP_NAME, :icon_of_application => "Pomodoro", :default_notifications => [title])
9 | app("GrowlHelperApp").notify(:title => title, :application_name => APP_NAME, :with_name => title, :description => message)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/actions/skype_status_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe SkypeStatus do
4 |
5 | before :each do
6 | @action = SkypeStatus.new(
7 | :away => "Pomodoro! Ends",
8 | :available => "Available"
9 | )
10 | end
11 |
12 | # spces are commented as Skype needs to be running
13 | describe "status update" do
14 | it "can set status to Do Not Disturb when pomodoro starts" do
15 | # @action.start
16 | end
17 |
18 | it "can set status to online when pomodoro ends" do
19 | # @action.stop
20 | end
21 | end
22 |
23 | end
--------------------------------------------------------------------------------
/spec/actions/terminal_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2 |
3 | describe Terminal do
4 |
5 | before :each do
6 | @action = Terminal.new(["echo 'hello world!'"])
7 | end
8 |
9 | describe "#start" do
10 | it "launch a Terminal only if it isn't running already" do
11 | @action.start
12 | Terminal.caprese_application.should be_running
13 | end
14 |
15 | it "should run the script in the first windows" do
16 | # I don't know how to write a test for this!
17 | end
18 |
19 | after :each do
20 | Terminal.caprese_application.quit!
21 | end
22 |
23 | end
24 |
25 | end
--------------------------------------------------------------------------------
/spec/actions/quit_applications_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../spec_helper", File.dirname(__FILE__))
2 |
3 | describe QuitApplications do
4 | describe "#validate" do
5 | it "is invalid if a non-existing application is provided" do
6 | action = QuitApplications.new(["System Preferences", "Jooky - Party in a Can"])
7 | action.should_not be_valid
8 | action.errors.first.should include("I can't find application Jooky - Party in a Can installed in")
9 | end
10 |
11 | it "is valid if provided applications exist" do
12 | action = QuitApplications.new(["System Preferences"])
13 | action.should be_valid
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/actions/launch_applications_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../spec_helper", File.dirname(__FILE__))
2 |
3 | describe LaunchApplications do
4 | describe "#validate" do
5 | it "is invalid if a non-existing application is provided" do
6 | action = LaunchApplications.new(["System Preferences", "Jooky - Party in a Can"])
7 | action.should_not be_valid
8 | action.errors.first.should include("I can't find application Jooky - Party in a Can installed in")
9 | end
10 |
11 | it "is valid if provided applications exist" do
12 | action = LaunchApplications.new(["System Preferences"])
13 | action.should be_valid
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/actions/adium_status.rb:
--------------------------------------------------------------------------------
1 | class AdiumStatus < CapreseAction
2 | include Appscript
3 |
4 | config_schema({:away => String, :available => String})
5 |
6 | def start
7 | return unless running?
8 | adium_accounts.status_type.set(:away)
9 | adium_accounts.status_message.set(config[:away])
10 | end
11 |
12 | def stop
13 | return unless running?
14 | adium_accounts.status_type.set(:available)
15 | adium_accounts.status_message.set(config[:available])
16 | end
17 |
18 | def adium_accounts
19 | @adium_accounts ||= app("Adium").accounts
20 | end
21 |
22 | def running?
23 | app("System Events").processes["Adium"].exists
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/actions/quit_and_block_application_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../spec_helper", File.dirname(__FILE__))
2 |
3 | describe QuitAndBlockApplications do
4 | describe "#validate" do
5 | it "is invalid if a non-existing application is provided" do
6 | action = QuitAndBlockApplications.new(["System Preferences", "Jooky - Party in a Can"])
7 | action.should_not be_valid
8 | action.errors.first.should include("I can't find application Jooky - Party in a Can installed")
9 | end
10 |
11 | it "is valid if provided applications exist" do
12 | action = QuitAndBlockApplications.new(["System Preferences"])
13 | action.should be_valid
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/actions/skype_status.rb:
--------------------------------------------------------------------------------
1 | class SkypeStatus < CapreseAction
2 | include Appscript
3 |
4 | config_schema({:away => String, :available => String})
5 |
6 | def start
7 | set_status("dnd")
8 | set_status_message(config[:away])
9 | end
10 |
11 | def stop
12 | set_status("online")
13 | set_status_message(config[:available])
14 | end
15 |
16 | def skype
17 | @skype ||= app("Skype")
18 | end
19 |
20 | private
21 |
22 | def set_status(status)
23 | skype.send_(:command => "set userstatus #{status}", :script_name => "caprese")
24 | end
25 |
26 | def set_status_message(message)
27 | skype.send_(:command => "set profile mood_text #{message}", :script_name => "caprese")
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/lib/caprese_configurator.rb:
--------------------------------------------------------------------------------
1 | class CapreseConfigurator
2 | include ConfigHelpers
3 | attr_reader :config
4 |
5 | def initialize
6 | load_config
7 | end
8 |
9 | def load_config
10 | @config = []
11 | load('config.rb')
12 | @config
13 | end
14 |
15 | def actions
16 | config.map do |action_args|
17 | action, args = action_args
18 | Object.const_get(action).new(args)
19 | end
20 | end
21 |
22 | def load(file)
23 | file = File.expand_path(file, APP_PATH)
24 | eval(File.read(file), binding, file, 1)
25 | end
26 |
27 | alias require load
28 |
29 | def method_missing(method, *args)
30 | if /[A-Z]/.match(method.to_s) && Object.const_defined?(method) && Object.const_get(method).ancestors.include?(CapreseAction)
31 | @config << [method, args]
32 | else
33 | super
34 | end
35 | end
36 | end
--------------------------------------------------------------------------------
/lib/env.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'appscript'
3 | require 'yaml'
4 | require 'pp'
5 |
6 | require 'pathname'
7 | require "fileutils"
8 |
9 | APP_PATH = Pathname.new(File.expand_path("..", File.dirname(__FILE__)))
10 | APP_NAME = "Caprese"
11 | LIB_PATH = APP_PATH + "lib"
12 | ACTIONS_PATH = APP_PATH + "actions"
13 | CONFIG_PATH = APP_PATH + "config"
14 |
15 | class String
16 | def classify
17 | gsub(/^[a-z]/) { |l| l.upcase}.gsub(/_[a-z]/) { |l| l[1..1].upcase}.gsub(/\b[a-z]/) {|l| l.upcase}.gsub("/", "::")
18 | end
19 | end
20 |
21 | class Time
22 | def to_short_time
23 | strftime('%I:%M %p').gsub(/^0/, '')
24 | end
25 | end
26 |
27 | (Dir[ACTIONS_PATH + "*"] + Dir[LIB_PATH + "*"]).each do |path|
28 | Object.autoload(File.basename(path, ".rb").classify.to_sym, path)
29 | end
30 |
31 | class ArgParser
32 | def self.extract_env_variables(args = ARGV)
33 | i = 0
34 | while ARGV[i]
35 | if ARGV[i].match(/^([A-Z_]+)=(.+)$/)
36 | ENV[$1] = $2
37 | ARGV.delete_at(i)
38 | ARGV.length
39 | else
40 | i += 1
41 | end
42 | end
43 | args
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/actions/block_domains.rb:
--------------------------------------------------------------------------------
1 | class BlockDomains < CapreseAction
2 | config_schema({String => Object})
3 |
4 | def start
5 | config.each do |target_domain, ips|
6 | Domain.new(target_domain, ips).block
7 | end
8 | end
9 |
10 | def stop
11 | IPFW.clear
12 | end
13 | end
14 |
15 | class Domain
16 | def initialize(name, ips = nil)
17 | @name = name
18 | @ips = ips
19 | end
20 |
21 | def ips
22 | @ips ||= resolve_ips
23 | end
24 |
25 | def resolve_ips
26 | ips = []
27 | ips.concat Dig.resolve_ips(@name)
28 | ips.concat Dig.resolve_ips("www." + @name) unless /^www/.match(@name)
29 | ips
30 | end
31 |
32 | def block
33 | ips.each do |ip|
34 | IPFW.block_ip(ip)
35 | end
36 | end
37 | end
38 |
39 | class Dig
40 | def self.resolve_ips(domain)
41 | %x{dig +short #{domain}}.split("\n").select {|ip| ip.match(/^[0-9.]+/)}
42 | end
43 | end
44 |
45 | class IPFW
46 | RULESET=6164
47 | IPFW_BIN="/sbin/ipfw"
48 |
49 | def self.run(*args)
50 | system(IPFW_BIN, *args.map{|a| a.to_s })
51 | end
52 |
53 | def self.block_ip(ip)
54 | run(*%W[add #{RULESET} deny tcp from any to #{ip}])
55 | end
56 |
57 | def self.clear
58 | run "delete", RULESET
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/actions/campfire.rb:
--------------------------------------------------------------------------------
1 | gem 'tinder', "= 1.8.0"
2 | gem "activesupport", ">= 2.3.4"
3 |
4 | require 'tinder'
5 | require "active_support"
6 |
7 | class Campfire < CapreseAction
8 | config_schema({:start_message => String, :stop_message => String, :campfire => {:ssl => Boolean, :domain => String, :room => String, :token => String}})
9 |
10 | def campfire_config
11 | config[:campfire]
12 | end
13 |
14 | def campfire
15 | @campfire ||= (
16 | campfire = Tinder::Campfire.new(campfire_config[:domain], campfire_config[:token])
17 | # Use old-style username and password
18 | # campfire = Tinder::Campfire.new(campfire_config[:domain],
19 | # :username => campfire_config[:username],
20 | # :password => campfire_config[:password])
21 | )
22 | end
23 |
24 | def room
25 | @room ||= (campfire.find_room_by_name(campfire_config[:room])) || raise(RuntimeError, "room '#{campfire_config[:room]}' not found.")
26 | end
27 |
28 | def speak(message)
29 | message.respond_to?(html_safe!) && message.html_safe! || message
30 | room.speak(message)
31 | end
32 |
33 | def start
34 | speak config[:start_message]
35 | end
36 |
37 | def stop
38 | speak config[:stop_message]
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/application.rb:
--------------------------------------------------------------------------------
1 | class Application
2 | STANDARD_APP_PATHS = ENV["APPS_PATH"].to_s.split(":") + ["/Applications", "/Applications/Utilities", File.join(ENV["HOME"], "applications")]
3 |
4 | include Appscript
5 | extend Appscript
6 |
7 | attr_reader :name
8 | def initialize(name)
9 | @name = name
10 | end
11 |
12 | def running?
13 | ap = app("System Events").application_processes
14 | (ap[its.short_name.eq(@name)].count(:each => :item) + ap[its.name.eq(@name)].count(:each => :item)) > 0
15 | end
16 |
17 | def quit!
18 | handle.quit
19 | end
20 |
21 | def activate!
22 | handle.activate
23 | end
24 |
25 | def handle
26 | @handle ||= app(@name)
27 | end
28 |
29 | def path_candidates
30 | STANDARD_APP_PATHS.map { |app_path| File.join(app_path, "#{@name}.app") }
31 | end
32 |
33 | def path
34 | @path ||=
35 | path_candidates.
36 | select {|p| File.exist?(p)}.
37 | first
38 | end
39 |
40 | def execpath
41 | "#{path}/Contents/MacOS"
42 | end
43 |
44 | def block!
45 | FileUtils.chmod(000, execpath)
46 | end
47 |
48 | def unblock!
49 | FileUtils.chmod(0755, execpath)
50 | end
51 |
52 | def self.hide_all
53 | app("System Events").processes[(its.visible.eq(true))].visible.set(false)
54 | end
55 |
56 | def exists?
57 | ! path.nil?
58 | end
59 |
60 | def to_s
61 | name
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/caprese.rb:
--------------------------------------------------------------------------------
1 | class Caprese
2 | def self.config
3 | @config ||= CapreseConfigurator.new
4 | end
5 |
6 | def self.actions
7 | config.actions
8 | end
9 |
10 | def self.start
11 | unless actions_with_errors.empty?
12 | errors = actions_with_errors.map {|a| "#{a.class}:\n" + format_errors(a.errors) }.join("\n")
13 | GrowlHelper.growl("#{APP_NAME} config has errors", "Run #{$0} check to see all.\n\n#{errors}")
14 | return false
15 | end
16 |
17 | with_growl_notification("start") do
18 | actions.each { |action| action.start }
19 | end
20 | end
21 |
22 | def self.stop
23 | with_growl_notification("stop") do
24 | actions.each { |action| action.stop }
25 | end
26 | end
27 |
28 | def self.check_config
29 | return true if actions_with_errors.empty?
30 |
31 | actions_with_errors.each do |action|
32 | puts "#{action.class} has errors:\n#{format_errors(action.errors)}"
33 | end
34 | false
35 | end
36 |
37 | private
38 | def self.with_growl_notification(kind, &block)
39 | begin
40 | yield
41 | GrowlHelper.growl("#{APP_NAME} success!", "All #{kind} actions were executed successfully")
42 | rescue Exception => e
43 | GrowlHelper.growl("#{APP_NAME} error", "Not all #{kind} actions were completed because this happened:\n#{e.message}")
44 | raise(e)
45 | end
46 | end
47 |
48 | def self.format_errors(errors)
49 | "- " + (errors * "\n- ")
50 | end
51 |
52 | def self.actions_with_errors
53 | @actions_with_errors ||= actions.select { |a| not a.valid? }
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/config.rb.sample:
--------------------------------------------------------------------------------
1 | BlockDomains(
2 | 'youtube.com' => %w[
3 | 72.14.213.100
4 | 72.14.213.101
5 | 72.14.213.102
6 | 72.14.213.113
7 | 72.14.213.138
8 | 72.14.213.139
9 | 74.125.127.100
10 | 74.125.127.101
11 | 74.125.127.102
12 | 74.125.127.113
13 | 74.125.127.138
14 | 74.125.127.139
15 | 74.125.45.100
16 | 74.125.67.100
17 | 72.14.213.138
18 | ],
19 | "facebook.com" => %w[
20 | 69.63.181.0/24
21 | 69.63.184.0/24
22 | 69.63.186.0/24
23 | 69.63.187.0/24
24 | ],
25 | "myspace.com" => nil,
26 | "twitter.com" => %w[
27 | 128.121.146.0/24
28 | 168.143.162.0/24
29 | ],
30 | "vimeo.com" => nil,
31 | "veoh.com" => nil,
32 | "linkedin.com" => nil,
33 | "plurk.com" => nil,
34 | "bebo.com" => nil,
35 | "yelp.com" => nil
36 | )
37 |
38 | LaunchApplications "Campfire", "Pivotal Tracker", "Ruby Docs (and others)", "TextMate"
39 | QuitApplications "Google Calendar", "Remember the Milk", "Preview"
40 | QuitAndBlockApplications "GMail", "Google Reader", "Google Voice", "Colloquy", "Mail", "TweetDeck"
41 | Terminal "cd Code;git pull"
42 |
43 | AdiumStatus(
44 | :away => "Pomodoro! Ends at #{stop_time.to_short_time}",
45 | :available => "Available"
46 | )
47 |
48 | IchatStatus(
49 | :away => "Pomodoro! Ends at #{stop_time.to_short_time}",
50 | :available => "Available"
51 | )
52 |
53 | SkypeStatus(
54 | :away => "Pomodoro! Ends at #{stop_time.to_short_time}",
55 | :available => "Available"
56 | )
57 |
58 | Campfire(
59 | :start_message => %(< #{MY_NAME} started "#{description}": #{start_time.to_short_time} - #{stop_time.to_short_time} >),
60 | :stop_message => "< #{MY_NAME} ended >",
61 | :campfire => {
62 | :domain => 'domain.campfirenow.com',
63 | :room => 'Room Name',
64 | :token => 'YOUR_CAMPFIRE_TOKEN'
65 | #:username => 'LOGIN@domain.com',
66 | #:password => 'SECRET_PASSWORD'
67 | }
68 | )
69 |
--------------------------------------------------------------------------------
/lib/caprese_action.rb:
--------------------------------------------------------------------------------
1 | class Boolean; end
2 |
3 | class CapreseAction
4 | attr_reader :config
5 |
6 | def initialize(config)
7 | self.config = config
8 | end
9 |
10 | def self.config_schema(schema)
11 | @schema = schema
12 | end
13 |
14 | def self.schema
15 | @schema
16 | end
17 |
18 | def schema
19 | self.class.schema
20 | end
21 |
22 | def errors
23 | @errors ||= []
24 | end
25 |
26 | def valid?
27 | return @valid if defined?(@valid)
28 | validate
29 | @valid
30 | end
31 |
32 | def start
33 | end
34 |
35 | def stop
36 | end
37 |
38 | def validate
39 | errors.clear
40 | @valid = true
41 | validate_schema
42 | @valid
43 | end
44 |
45 | def config=(config)
46 | if schema == [String]
47 | @config = unfold_splat(config) { |first_item| first_item.is_a?(Array) }
48 | else
49 | @config = unfold_splat(config)
50 | end
51 | validate
52 | end
53 |
54 | private
55 | def validate_schema
56 | case
57 | when schema == [String]
58 | add_error("config expects an array of strings, got #{@config.inspect}") unless @config.all? { |element| element.is_a?(String) }
59 | when schema.is_a?(Hash)
60 | return(add_error("config expects a Hash, got a(n) #{@config.class} instead")) unless @config.is_a?(Hash)
61 |
62 | if schema.keys.first.is_a?(Class)
63 | key_expectation, value_expectation = schema.keys.first, schema.values.first
64 | @config.each do |key, value|
65 | add_error("key #{key.inspect} expected to be of type #{key_expectation}, but is of type #{key.class}") unless key.is_a?(key_expectation)
66 | validate_value(key, value, value_expectation)
67 | end
68 | else
69 | validate_hash(@config, schema)
70 | end
71 | end
72 | end
73 |
74 | def add_error(message)
75 | @valid = false
76 | errors << message
77 | end
78 |
79 | def unfold_splat(config)
80 | if config.is_a?(Array) && config.length == 1
81 | return config if block_given? && (not yield(config.first))
82 | config.first
83 | else
84 | config
85 | end
86 | end
87 |
88 | def validate_value(key, value, expectation)
89 | case expectation.name
90 | when "Boolean"
91 | add_error("key #{key.inspect} expects a value of type #{expectation}, but you provided #{value.inspect} of type #{value.class}") unless [nil, true, false].include?(value)
92 | else
93 | add_error("key #{key.inspect} expects a value of type #{expectation}, but you provided #{value.inspect} of type #{value.class}") unless value.is_a?(expectation)
94 | end
95 | end
96 |
97 | def validate_hash(input, expectation)
98 | input.each do |key, value|
99 | next add_error("Unexpected key #{key.inspect} provided. Valid keys here are [#{expectation.keys.map {|k| k.inspect }.sort * ', '}]") unless expectation.has_key?(key)
100 | case
101 | when expectation[key].is_a?(Hash)
102 | next add_error("key #{key.inspect} expects a nested Hash of values, but you provided #{value.inspect}") unless value.is_a?(Hash)
103 | validate_hash(value, expectation[key])
104 | when expectation[key].is_a?(Class)
105 | validate_value(key, value, expectation[key])
106 | else
107 | puts "Invalid schema: #{expectation[key]}"
108 | end
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/spec/caprese_action_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("spec_helper", File.dirname(__FILE__))
2 |
3 | describe CapreseAction do
4 | describe ".config_schema" do
5 | context "[String]" do
6 | before(:each) do
7 | @klass = Class.new(CapreseAction) do
8 | config_schema [String]
9 | end
10 | end
11 |
12 | it "causes splatted args to be unfolded" do
13 | @klass.new([["a", "b"]]).config.should == ["a", "b"]
14 | end
15 |
16 | it "takes an array" do
17 | @klass.new(["a", "b"]).config.should == ["a", "b"]
18 | end
19 | end
20 |
21 | context "{...}" do
22 | before(:each) do
23 | @klass = Class.new(CapreseAction) do
24 | config_schema({String => Object})
25 | end
26 | end
27 |
28 | it "takes a hash" do
29 | @klass.new({:param => 1}).config.should == {:param => 1}
30 | end
31 |
32 | it "unfolds a splat" do
33 | @klass.new([{:param => 1}]).config.should == {:param => 1}
34 | end
35 | end
36 | end
37 |
38 | describe "#validate" do
39 | context "schema is [String]" do
40 | before(:each) do
41 | @klass = Class.new(CapreseAction) do
42 | config_schema [String]
43 | end
44 | end
45 |
46 | it "fails if all items aren't strings" do
47 | action = @klass.new([1, 2])
48 | action.should_not be_valid
49 | action.errors.should include("config expects an array of strings, got [1, 2]")
50 | end
51 | end
52 |
53 | context "schema is {String => String}" do
54 | before(:each) do
55 | @klass = Class.new(CapreseAction) do
56 | config_schema({String => String})
57 | end
58 | end
59 |
60 | it "fails if you provide an array instead of a hash" do
61 | action = @klass.new([1, 2])
62 | action.should_not be_valid
63 | action.errors.should include("config expects a Hash, got a(n) Array instead")
64 | end
65 |
66 | it "fails if all keys aren't strings" do
67 | action = @klass.new({:a => "1", "b" => "2"})
68 | action.should_not be_valid
69 | action.errors.should include("key :a expected to be of type String, but is of type Symbol")
70 | end
71 |
72 | it "fails if all values aren't strings" do
73 | action = @klass.new({"a" => :value, "b" => "2"})
74 | action.should_not be_valid
75 | action.errors.should include("key \"a\" expects a value of type String, but you provided :value of type Symbol")
76 | end
77 | end
78 |
79 | context "schema is {:message => String, :credentials => {:name => String, :pass => String}}" do
80 | before(:each) do
81 | @klass = Class.new(CapreseAction) do
82 | config_schema({:message => String, :credentials => {:name => String, :pass => String}})
83 | end
84 | end
85 |
86 | it "fails if you provide an array instead of a hash" do
87 | action = @klass.new([1, 2])
88 | action.should_not be_valid
89 | action.errors.should include("config expects a Hash, got a(n) Array instead")
90 | end
91 |
92 | it "is valid if the keys are missing" do
93 | action = @klass.new({})
94 | action.should be_valid
95 | end
96 |
97 | it "fails if an invalid key is specified in the outer hash" do
98 | action = @klass.new({:boo => "hi"})
99 | action.should_not be_valid
100 | action.errors.should include("Unexpected key :boo provided. Valid keys here are [:credentials, :message]")
101 | end
102 |
103 | it "fails if an invalid key is specified in the inner hash" do
104 | action = @klass.new({:credentials => {:boo => "hi"}})
105 | action.should_not be_valid
106 | action.errors.should include("Unexpected key :boo provided. Valid keys here are [:name, :pass]")
107 | end
108 |
109 | it "fails if a value doesn't match the specification" do
110 | action = @klass.new({:message => :hi})
111 | action.should_not be_valid
112 | action.errors.should include("key :message expects a value of type String, but you provided :hi of type Symbol")
113 | end
114 |
115 | it "passes if the value does match the specification" do
116 | action = @klass.new({:message => "hello world", :credentials => {:name => "Tim", :pass => "secret"}})
117 | action.should be_valid
118 | end
119 |
120 | it "fails if you provide a value where a nested hash is expected" do
121 | action = @klass.new({:credentials => :hi})
122 | action.should_not be_valid
123 | action.errors.should include("key :credentials expects a nested Hash of values, but you provided :hi")
124 | end
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. Caprese
2 |
3 | "Caprese":http://github.com/timcharper/caprese (pronounced ca-PRAY-zay) is a helper app for the "Mac OS X Pomodoro App":http://pomodoro.ugolandini.com/Mac by Ugo Landini. It provides an easy-to-configure suite of actions that can be executed and unexecuted when your Pomodoro starts and stops. Actions include:
4 |
5 | * Set iChat status for all accounts
6 | * Set adium status for all accounts
7 | * Set skype status
8 | * Announce pomodoro status to campfire
9 | * Launch applications, hiding all other windows
10 | * Quit applications
11 | * Block applications
12 | * Block domains (blocks by IP using the Mac OS X firewall)
13 |
14 | h2. Installation
15 |
16 | h3. Prerequisites
17 |
18 | * A Ruby interpreter, 1.8.6 or greater
19 | * XCode Mac developer tools (required to install Ruby/AppleScript interop support)
20 |
21 | h3. Step 1: Download and install caprese
22 |
23 | Caprese requires root access and should be installed in /opt/caprese, owned as root.
24 |
25 | *Option 1*: download a release file from github (see http://github.com/timcharper/caprese/downloads), and unzip it to /opt/caprese. If the folder /opt/caprese/lib exists, you've done it right.
26 |
27 | *Option 2*: clone the git repository and run edge. updates are as easy a 'git pull'.
28 |
29 |
30 | sudo su - 31 | # type password 32 | cd /opt 33 | git clone git://github.com/timcharper/caprese.git 34 |35 | 36 | If you prefer not to run caprese with root privileges, many features are still available without it. Install caprese to any folder and skip the "sudo access section". Domain blocking and application blocking are likely not work. 37 | 38 | h3. Step 2: grant password free sudo access to /opt/caprese/caprese 39 | 40 | Make it so you can run
sudo /opt/caprese/caprese without supplying a password. I can't guarantee that this will be 100% safe to do, but if you make sure that only root has access to write or create any file in /opt/caprese, I don't foresee any security threat.
41 |
42 | Edit your sudoers file by running visudo as root, and put these lines at the bottom of the file:
43 |
44 | 45 | %admin ALL=PASSWD: ALL, NOPASSWD: /opt/caprese/caprese 46 |47 | 48 | (hint: copy above line, then in
visudo, press shift-G, o, command-v, , :wq)
49 |
50 | h3. Step 3: Gem dependencies
51 |
52 | Use sudo geminstaller to make sure you have the needed gems. Also, you will want to make sure that you are using your System-supplied version of ruby, and not your custom built version, as Mac OS X will likely ignore your path settings and pick the System-supplied.
53 |
54 | I recommend:
55 |
56 | 57 | cd path_to_caprese 58 | sudo /usr/bin/gem install geminstaller 59 | sudo /usr/bin/geminstaller 60 |61 | 62 | h3. Step 4: Configure 63 | 64 | copy
/opt/caprese/config.rb.sample to /opt/caprese/config.rb, and then edit it with your favorite text editor. The config file should be self-explanatory.
65 |
66 | h3. Step 5: Test
67 |
68 | As your user (NOT ROOT!), run:
69 |
70 | 71 | sudo -k 72 |73 | 74 | This clears sudo timestamp so sudo will begin prompting for a password again. 75 | 76 | ------- 77 | 78 |
79 | sudo /opt/caprese/caprese start DESCRIPTION="test description" 80 |81 | 82 | *You should have seen no password prompt*. If you did, something went wrong in Step 2. To diagnose: 83 | 84 | * Check to make sure your user is apart of the admin group 85 | * make sure no other config lines conflict. 86 | 87 | *If you saw a configuration error*, follow the instructions shown. If it was a ruby parse error, you probably forgot to close a parenthesis pair, forgot a command, put in an extra command, etc. 88 | 89 | ------- 90 | 91 |
92 | sudo /opt/caprese/caprese stop 93 |94 | 95 | This command should have undid everything the start command did 96 | 97 | ------- 98 | 99 |
100 | sudo ls -l /root 101 |102 | 103 | This should've prompted you for a password. If it doesn't, your sudoers change was to permissive, or you'd given free rain to 'ls' previously. 104 | 105 | h3. Step 6: Integrating with Pomodoro.app 106 | 107 | Go to the config -> applescript. 108 | 109 | Check the checkboxes next to "*start*", "*reset*", and "*end*" to tell
Pomodoro.app to actually execute those scripts.
110 |
111 | Put this in "*start*" (copy, delete all text in box, then right click and choose paste) :
112 |
113 | 114 | do shell script "sudo /opt/caprese/caprese start DESCRIPTION=\"$pomodoroName\" DURATION=$duration" 115 |116 | 117 | Put this in "*reset*" and "*end*" similarly : 118 | 119 |
120 | do shell script "sudo /opt/caprese/caprese stop" 121 |122 | 123 | h3. Step 7: Profit! 124 | 125 | The setup has a lot of steps, sorry about that. Caprese will growl an error message at you if one of the commands errored out. Please report any bugs "here":http://github.com/timcharper/caprese/issues. 126 | 127 | h2. Credits 128 | 129 | * Developed by Tim Harper of "Lead Media Partners":http://www.leadmediapartners.com/. 130 | * HAS for rb-appscript and ASTranslate, invaluable tools that made this possible 131 | * Alan Whitaker for the name 132 | --------------------------------------------------------------------------------