Created
February 21, 2010 03:47
-
-
Save natebunnyfield/310104 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
require 'optparse' | |
require 'appscript' | |
require 'growl' | |
include Appscript | |
include Growl | |
class Pomodoro | |
attr_accessor :playlist, :time, :message, :lock, :done | |
def initialize( | |
playlist, | |
time, | |
message, | |
lock | |
) | |
@playlist = playlist | |
@time = time | |
@message = message | |
@lock = lock | |
@done = false | |
end | |
def to_s | |
%Q{#{time}: msg:"#{message}" lock:#{lock} done:#{done} playlist:#{playlist}} | |
end | |
def <=>(other) | |
self.time <=> other.time | |
end | |
end | |
class Pomodori | |
CIRILLO_DURATIONS = [25,5,25,5,25,5,25,15] | |
POLLING_FREQUENCY = 2 | |
MAX_WINDOW = 16*60 | |
LOCK_WAIT = 7 | |
def initialize( | |
duration_pattern, | |
options = {} | |
) | |
@options = options | |
if itunes? | |
@itunes = app("iTunes") | |
odd_playlist = get_playlist(@options[:odd_playlist]) | |
even_playlist = get_playlist(@options[:even_playlist]) | |
end | |
if taskpaper? | |
@taskpaper = app("TaskPaper").documents[@options[:taskpaper_name]] | |
@options[:work_message] = :taskpaper | |
end | |
@pomodori = [] | |
times = times_from_durations(Time.now, [0]+durations_from_pattern(duration_pattern)) | |
times.each_index do |i| | |
playlist = i.even? ? even_playlist : odd_playlist if itunes? | |
message = i.even? ? @options[:break_message] : @options[:work_message] | |
lock = i.odd? | |
@pomodori.push(Pomodoro.new(playlist, times[i], message, lock)) | |
end | |
end | |
def active | |
@pomodori.select do |pomodoro| | |
not pomodoro.done and pomodoro.time <= Time.now | |
end | |
end | |
def rotate_out | |
active.each do |pomodoro| | |
pomodoro.done = true | |
end | |
end | |
def current | |
active.sort.last | |
end | |
def future | |
@pomodori.select do |pomodoro| | |
not pomodoro.done and pomodoro.time > Time.now | |
end | |
end | |
def next1 | |
future.sort.first | |
end | |
def finished | |
@pomodori.all? { |pomodoro| pomodoro.done } | |
end | |
def run | |
until finished do | |
unless current.nil? | |
play current.playlist | |
unless next1.nil? | |
if taskpaper? and next1.message == :taskpaper | |
next1.message = next_task | |
end | |
puts "#{next1.time}: #{next1.message}" | |
notify(next1.message, :icon => :TaskPaper) if growl? | |
end | |
if lock? and current.lock | |
stop | |
sleep LOCK_WAIT | |
`/System/Library/CoreServices/Menu\\ Extras/User.menu/Contents/Resources/CGSession -suspend` | |
end | |
end | |
rotate_out | |
sleep POLLING_FREQUENCY | |
end | |
stop | |
end | |
def lock? ; @options[:lock] ; end | |
def growl? ; @options[:growl] ; end | |
def taskpaper? ; @options[:taskpaper] ; end | |
def itunes? ; @options[:itunes] ; end | |
protected | |
def durations_from_pattern(pattern) | |
durations = pattern.dup | |
until MAX_WINDOW <= durations.inject(0) { |x,y| x + y } | |
durations *= 2 | |
end | |
durations | |
end | |
def play(item) | |
return unless @options[:itunes] | |
@itunes.play item | |
if not @itunes.current_track.podcast.get | |
@itunes.next_track | |
end | |
end | |
def stop | |
return unless @options[:itunes] | |
@itunes.stop | |
end | |
def next_task | |
return unless @options[:taskpaper] | |
@taskpaper.projects.first.tasks.get.select do |task| | |
not task.tags.name.get.include? "done" | |
end.first.text_content.get | |
end | |
def times_from_durations(offset, durations) | |
times = durations.map do |duration| | |
offset = offset + duration * 60 | |
end | |
end | |
def get_playlist(name) | |
@itunes.playlists.get.select do |playlist| | |
playlist.name.get == name | |
end[0] | |
end | |
end | |
class ProfileLoader | |
class YmlLoadError < StandardError; end | |
class ProfilesNotDefinedError < YmlLoadError; end | |
class ProfileNotFound < StandardError; end | |
def initialize(yml_filename) | |
@yml_filename = yml_filename | |
@yml = nil | |
end | |
def args_from(profile) | |
raise(ProfileNotFound, "#{profile}") unless yml.has_key?(profile) | |
args_from_yml = yml[profile] || '' | |
case(args_from_yml) | |
when String | |
raise YmlLoadError, "The '#{profile}' profile in #{yml_file} was blank. Please define the command line arguments for the '#{profile}' profile in #{yml_file}.\n" if args_from_yml =~ /^\s*$/ | |
require 'shellwords' | |
args_from_yml = args_from_yml.shellsplit | |
when Array | |
raise YmlLoadError, "The '#{profile}' profile in #{yml_file} was empty. Please define the command line arguments for the '#{profile}' profile in #{yml_file}.\n" if args_from_yml.empty? | |
else | |
raise YmlLoadError, "The '#{profile}' profile in #{yml_file} was a #{args_from_yml.class}. It must be a String or an Array." | |
end | |
args_from_yml | |
end | |
def has_profile?(profile) | |
yml.has_key?(profile) | |
end | |
def yml_defined? | |
yml_file and File.exist?(yml_file) | |
end | |
private | |
def yml | |
return @yml if @yml | |
unless yml_defined? | |
raise(ProfilesNotDefinedError, "#{@yml_filename} was not found. You must define a 'default' profile to use without any arguments.\nType '--help' for a list of arguments.\n") | |
end | |
require 'erb' | |
require 'yaml' | |
begin | |
@erb = ERB.new(IO.read(yml_file)).result | |
rescue Exception => e | |
raise(YmlLoadError,"#{@yml_filename} was found, but could not be parsed with ERB.\n#{$!.inspect}") | |
end | |
begin | |
@yml = YAML::load(@erb) | |
rescue StandardError => e | |
raise(YmlLoadError,"#{yml_file} was found, but could not be parsed.\n") | |
end | |
if @yml.nil? || [email protected]_a?(Hash) | |
raise(YmlLoadError,"#{yml_file} was found, but was blank or malformed.\n") | |
end | |
return @yml | |
end | |
def yml_file | |
@yml_file ||= Dir.glob("{,config/,#{ENV['HOME']}/}{,.}#{@yml_filename}{.yml,.yaml,rc}").first | |
end | |
end | |
options = { | |
:lock => false, | |
:growl => false, | |
:itunes => false, | |
:odd_playlist => "#break", | |
:even_playlist => "#work", | |
:taskpaper => false, | |
:taskpaper_name => "pomodori.taskpaper", | |
:break_message => "#break", | |
:work_message => "#work" | |
} | |
args = ARGV.dup | |
if args.empty? | |
args = ProfileLoader.new('pomo').args_from('default') | |
end | |
OptionParser.new do |opts| | |
opts.banner = "Usage: pomo [options] [duration_pattern]" | |
opts.on("-l", "--[no-]lock-screen") { |x| options[:lock] = x } | |
opts.on("-g", "--[no-]growl") { |x| options[:growl] = x } | |
opts.on("-i", "--[no-]itunes") { |x| options[:itunes] = x } | |
opts.on("-t", "--[no-]taskpaper") { |x| options[:taskpaper] = x } | |
opts.on("--work-playlist NAME") { |x| options[:even_playlist] = x } | |
opts.on("--break-playlist NAME") { |x| options[:odd_playlist] = x } | |
opts.on("--taskpaper-name NAME") { |x| options[:taskpaper_name] = x } | |
opts.on("--break_message TEXT") { |x| options[:break_message] = x } | |
opts.on("--work_message TEXT") { |x| options[:break_message] = x } | |
end.parse!(args) | |
duration_pattern = args | |
duration_pattern = duration_pattern.map { |x| x.split(/[ ;,|]/)}.flatten | |
duration_pattern = duration_pattern.map { |x| x.to_f } | |
raise OptionParser::InvalidOption, "bad duration" if duration_pattern.include? 0.0 | |
duration_pattern = Pomodori::CIRILLO_DURATIONS if duration_pattern.empty? | |
# options.each { |k, v| puts "#{k} => #{v}" } | |
Pomodori.new(duration_pattern, options).run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment