Skip to content

Instantly share code, notes, and snippets.

@natebunnyfield
Created February 21, 2010 03:47
Show Gist options
  • Save natebunnyfield/310104 to your computer and use it in GitHub Desktop.
Save natebunnyfield/310104 to your computer and use it in GitHub Desktop.
#!/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? || !@yml.is_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