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