habits 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README +8 -0
- data/bin/habits +4 -0
- data/lib/habits/command_line.rb +129 -0
- data/lib/habits/event.rb +11 -0
- data/lib/habits/events/activity.rb +20 -0
- data/lib/habits/habit.rb +162 -0
- data/lib/habits/mock_data_store.rb +25 -0
- data/lib/habits/status.rb +60 -0
- data/lib/habits/subcommand.rb +96 -0
- data/lib/habits/whip.rb +39 -0
- data/lib/habits.rb +3 -0
- data/whip_config.rb +24 -0
- metadata +78 -0
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
Copyright (c) 2010 Antti Hakala
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions
|
7
|
+
are met:
|
8
|
+
1. Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
3. The name of the author may not be used to endorse or promote products
|
14
|
+
derived from this software without specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
17
|
+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
21
|
+
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
22
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
23
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
25
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
|
data/README
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Habits
|
2
|
+
======
|
3
|
+
|
4
|
+
An experimental habit tracker.
|
5
|
+
|
6
|
+
Tracks habits in weekly cycles (i.e. status of each habit is cleared weekly).
|
7
|
+
Each habit has a day or days associated with it. Habits expects activity on the habit on those days. If no activity is registered, habit goes first into yellow zone (e.g. 20 hours before deadline), then into red zone (e.g. 6 hours before), and finally into missed state. Transfer into each zone/state can be used to trigger commands.
|
8
|
+
|
data/bin/habits
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
require 'habits'
|
4
|
+
require 'habits/subcommand'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
if !File.exists?(Habits::Habit::HABITS_DIR)
|
8
|
+
FileUtils.mkdir Habits::Habit::HABITS_DIR
|
9
|
+
FileUtils.cp File.join(File.dirname(__FILE__), '..', '..', 'whip_config.rb'),
|
10
|
+
Habits::Habit::HABITS_DIR+"/"
|
11
|
+
puts "\n*** Copied default whip_config.rb to #{Habits::Habit::HABITS_DIR}"
|
12
|
+
puts "*** Please edit it to suit you."
|
13
|
+
puts "*** Also, consider adding line '0 * * * * /usr/bin/habits whip'"
|
14
|
+
puts "*** to your crontab.\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
sub = Subcommand.new
|
18
|
+
|
19
|
+
sub.register('whip', [], 'Use the whip.') do
|
20
|
+
Habits::Whip.use
|
21
|
+
end
|
22
|
+
|
23
|
+
sub.register('delete', ['TITLE'], 'Delete habit.') do |title|
|
24
|
+
h = Habits::Habit.find(title)
|
25
|
+
h.destroy
|
26
|
+
puts "#{h.title} deleted."
|
27
|
+
end
|
28
|
+
|
29
|
+
sub.register('create', ['TITLE','DAYS'], 'Create a new habit.') do |title, days|
|
30
|
+
Habits::Habit.new(title, days.split(',')).save
|
31
|
+
puts "Habit \"#{title}\" created."
|
32
|
+
end
|
33
|
+
|
34
|
+
sub.register('list', [], 'List habits.') do
|
35
|
+
puts "\nHABIT DAYS STATUS YELLOW/RED"
|
36
|
+
puts "===============================================================================\n"
|
37
|
+
Habits::Habit.all_not_on_hold.each do |habit|
|
38
|
+
printf("%-25s %-30s %-10s [%dh / %dh]\n",
|
39
|
+
habit.title, habit.days.join(','),
|
40
|
+
habit.status.value.to_s.capitalize,
|
41
|
+
(habit.yellow_zone / (60*60)),
|
42
|
+
(habit.red_zone / (60*60)))
|
43
|
+
end
|
44
|
+
puts "===============================================================================\n"
|
45
|
+
on_hold = Habits::Habit.all_on_hold
|
46
|
+
|
47
|
+
if on_hold.size > 0
|
48
|
+
puts "\nOn Hold: #{on_hold.map{|h| h.title}.join(', ')}.\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
puts "\nWeek #{Date.today.cweek} | Total #{Habits::Habit.all.size} habits.\n\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
sub.register('zones', ['TITLE','YELLOW_ZONE','RED_ZONE'],
|
55
|
+
"Set habit's yellow and red zones.") do |title, yellow, red|
|
56
|
+
h = Habits::Habit.find(title)
|
57
|
+
|
58
|
+
h.yellow_zone = yellow.to_i * 60 * 60
|
59
|
+
h.red_zone = red.to_i * 60 * 60
|
60
|
+
h.save
|
61
|
+
puts "Zones set."
|
62
|
+
end
|
63
|
+
|
64
|
+
sub.register('do', ['TITLE', '[HOURS]'],
|
65
|
+
'Add activity to habit, hours optional.') do |title, hours|
|
66
|
+
h = Habits::Habit.find(title)
|
67
|
+
h.add_event(Habits::Events::Activity.new(hours ? hours.to_f : nil))
|
68
|
+
puts "Activity added."
|
69
|
+
end
|
70
|
+
|
71
|
+
sub.register('rename', ['TITLE', 'NEW_TITLE'], 'Rename habit.') do |title, new_title|
|
72
|
+
h = Habits::Habit.find(title)
|
73
|
+
h.set_title(new_title)
|
74
|
+
puts "Renamed #{title} -> #{new_title}"
|
75
|
+
h.save
|
76
|
+
end
|
77
|
+
|
78
|
+
sub.register('show', ['TITLE'], 'Show activity of habit.') do |title|
|
79
|
+
h = Habits::Habit.find(title)
|
80
|
+
|
81
|
+
if h.events.empty?
|
82
|
+
puts "\nNo activity.\n\n"
|
83
|
+
else
|
84
|
+
puts "\n#{title} Activity:\n"+ ('='*(title.size)) + "==========\n"
|
85
|
+
|
86
|
+
h.events.each do |event|
|
87
|
+
str = " #{event.applied_at.strftime("%a %b %d %Y")}"
|
88
|
+
str += " #{event.duration} hours" if event.duration
|
89
|
+
puts str
|
90
|
+
end
|
91
|
+
puts
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
sub.register('split', ['TITLE'], 'Split a habit into days.') do |title|
|
96
|
+
h = Habits::Habit.find(title)
|
97
|
+
h.split!
|
98
|
+
puts "Habit \"#{title}\" split."
|
99
|
+
end
|
100
|
+
|
101
|
+
sub.register('join', ['TITLE', '[OTHER_TITLE]'],
|
102
|
+
'Join habits. First one is kept.') do |title, other_title|
|
103
|
+
if other_title.nil?
|
104
|
+
Habits::Habit.join_all(title)
|
105
|
+
puts "#{title} joined."
|
106
|
+
else
|
107
|
+
habit = Habits::Habit.find(title)
|
108
|
+
other = Habits::Habit.find(other_title)
|
109
|
+
habit.join!(other)
|
110
|
+
puts "Habits \"#{title}\" and \"#{other_title}\" joined."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
sub.register('hold', ['TITLE'], 'Put a habit on hold.') do |title|
|
115
|
+
h = Habits::Habit.find(title)
|
116
|
+
h.hold
|
117
|
+
puts "Habit \"#{title}\" is now on hold."
|
118
|
+
end
|
119
|
+
|
120
|
+
sub.register('unhold', ['TITLE'], 'Unhold a habit.') do |title|
|
121
|
+
h = Habits::Habit.find(title)
|
122
|
+
h.unhold
|
123
|
+
puts "Habit \"#{title}\" is not on hold anymore."
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
sub.default = 'list'
|
128
|
+
|
129
|
+
sub.parse
|
data/lib/habits/event.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'habits/event'
|
2
|
+
|
3
|
+
module Habits::Events
|
4
|
+
|
5
|
+
class Activity
|
6
|
+
include Habits::Event
|
7
|
+
|
8
|
+
attr_reader :duration
|
9
|
+
|
10
|
+
def initialize(duration=nil)
|
11
|
+
@duration = duration
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply(habit, time)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/lib/habits/habit.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'habits/status'
|
5
|
+
require 'habits/events/activity'
|
6
|
+
|
7
|
+
module Habits
|
8
|
+
|
9
|
+
# The habit class. Attached events describe activities etc.
|
10
|
+
class Habit
|
11
|
+
HABITS_DIR = File.join(ENV['HOME'], '.habits')
|
12
|
+
YELLOW_ZONE = 16*60*60 # 16 hours before deadline,
|
13
|
+
RED_ZONE = 6*60*60 # 6 -"-
|
14
|
+
|
15
|
+
def self.all
|
16
|
+
@@all ||= begin
|
17
|
+
habits = []
|
18
|
+
Dir.glob(File.join(HABITS_DIR, '*.habit')).each do |habit_file|
|
19
|
+
habits << YAML.load(File.read(habit_file))
|
20
|
+
end
|
21
|
+
habits
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.all_not_on_hold
|
26
|
+
all.select{|habit| habit.status != Status.on_hold}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.all_on_hold
|
30
|
+
all.select{|habit| habit.status == Status.on_hold}
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find(title)
|
34
|
+
h = Habit.all.detect {|h| h.title == title.strip}
|
35
|
+
raise "No such habit found." unless h
|
36
|
+
h
|
37
|
+
end
|
38
|
+
|
39
|
+
# Join all habits with title 'title':something.
|
40
|
+
def self.join_all(title)
|
41
|
+
joinables = Habit.all.select {|habit| habit.title =~ /^#{title}:.+/}
|
42
|
+
raise "Habits not found." if joinables.size == 0
|
43
|
+
habit = Habit.find(title) rescue nil
|
44
|
+
habit ||= Habit.new(title, [])
|
45
|
+
joinables.each{|joinable| habit.join!(joinable)}
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :title, :days, :status, :events
|
49
|
+
attr_accessor :yellow_zone, :red_zone
|
50
|
+
|
51
|
+
def initialize(title, days = ['Mon'],
|
52
|
+
yellow_zone = YELLOW_ZONE,
|
53
|
+
red_zone = RED_ZONE,
|
54
|
+
events = [])
|
55
|
+
set_title(title)
|
56
|
+
set_days(days)
|
57
|
+
@yellow_zone, @red_zone = yellow_zone, red_zone
|
58
|
+
@status = Status.green
|
59
|
+
@created_at = Time.now
|
60
|
+
@events = events
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_event(event, time=Time.now)
|
64
|
+
event.apply(self, time)
|
65
|
+
@events << event
|
66
|
+
save
|
67
|
+
end
|
68
|
+
|
69
|
+
def file_path(title=@title)
|
70
|
+
File.join(HABITS_DIR, title.downcase + '.habit')
|
71
|
+
end
|
72
|
+
|
73
|
+
def save
|
74
|
+
@@all = nil
|
75
|
+
FileUtils.rm_f(file_path(@old_title)) if @old_title
|
76
|
+
FileUtils.mkdir_p HABITS_DIR
|
77
|
+
File.open(file_path, 'w') do |file|
|
78
|
+
file.write self.to_yaml
|
79
|
+
end
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def update_status
|
84
|
+
old_status = @status
|
85
|
+
@status = Status.resolve(self)
|
86
|
+
|
87
|
+
if old_status != @status
|
88
|
+
yield if block_given?
|
89
|
+
save
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def activities_on_week(week, day=nil)
|
94
|
+
activities = @events.select do |e|
|
95
|
+
e.is_a?(Events::Activity) and Date.new(e.applied_at.year,
|
96
|
+
e.applied_at.month,
|
97
|
+
e.applied_at.day).cweek == week
|
98
|
+
end
|
99
|
+
activities = activities.select{|a| a.applied_at.strftime('%a') == day} if day
|
100
|
+
activities
|
101
|
+
end
|
102
|
+
|
103
|
+
def destroy
|
104
|
+
FileUtils.rm_f file_path
|
105
|
+
@@all = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def set_title(title)
|
109
|
+
raise "No spaces or commas allowed in habit title" if title =~ /[\s,,]+/
|
110
|
+
@old_title = @title
|
111
|
+
@title = title
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_days(days)
|
115
|
+
if days.detect{|d| Time::RFC2822_DAY_NAME.index(d).nil?}
|
116
|
+
raise "Valid days are #{Time::RFC2822_DAY_NAME.join(',')}"
|
117
|
+
else
|
118
|
+
@days = days
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def split
|
123
|
+
@days.map do |day|
|
124
|
+
events = @events.select{|event| event.applied_at.strftime('%a') == day}
|
125
|
+
Habit.new("#{@title}:#{day}", [day], YELLOW_ZONE, RED_ZONE, events)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def split!
|
130
|
+
habits = split
|
131
|
+
habits.each{|habit| habit.save}
|
132
|
+
destroy
|
133
|
+
habits
|
134
|
+
end
|
135
|
+
|
136
|
+
def join(habit)
|
137
|
+
@events += habit.events
|
138
|
+
@days += habit.days
|
139
|
+
@days.uniq!
|
140
|
+
@days.sort!{|a,b| Time::RFC2822_DAY_NAME.index(a) <=> Time::RFC2822_DAY_NAME.index(b)}
|
141
|
+
end
|
142
|
+
|
143
|
+
def join!(habit)
|
144
|
+
join(habit)
|
145
|
+
save
|
146
|
+
habit.destroy
|
147
|
+
end
|
148
|
+
|
149
|
+
def hold
|
150
|
+
@status = Status.on_hold
|
151
|
+
save
|
152
|
+
end
|
153
|
+
|
154
|
+
def unhold
|
155
|
+
@status = Status.green
|
156
|
+
@status = Status.resolve(self)
|
157
|
+
save
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module Habits
|
3
|
+
|
4
|
+
# a module to in Habit to mock data storing
|
5
|
+
class Habit
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
@@all
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.all=(habits)
|
12
|
+
@@all = habits
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
# do nothing
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroy
|
20
|
+
# do nothing
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Habits
|
5
|
+
class Status
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
VALUES = [:green, :yellow, :red, :missed, :on_hold]
|
9
|
+
VALUES.each do |val|
|
10
|
+
eval %Q(def self.#{val}; Status.new(#{val.inspect}) end)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :value
|
14
|
+
|
15
|
+
def initialize(val)
|
16
|
+
raise "Invalid status value" unless VALUES.include?(val)
|
17
|
+
@value = val
|
18
|
+
end
|
19
|
+
|
20
|
+
def <=>(other)
|
21
|
+
VALUES.index(self.value) <=> VALUES.index(other.value)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Resolves the status of a habit.
|
25
|
+
# Status starts fresh every week.
|
26
|
+
def self.resolve(habit, time=Time.now)
|
27
|
+
return Status.on_hold if habit.status == Status.on_hold
|
28
|
+
|
29
|
+
statuses = []
|
30
|
+
date = Date.new(time.year, time.month, time.day)
|
31
|
+
|
32
|
+
habit.days.each do |day|
|
33
|
+
activities = habit.activities_on_week(date.cweek, day)
|
34
|
+
day_diff = Time::RFC2822_DAY_NAME.index(day) - date.wday
|
35
|
+
|
36
|
+
if !activities.empty? or (day_diff > 0)
|
37
|
+
statuses << Status.green
|
38
|
+
else
|
39
|
+
dl_date = date + day_diff
|
40
|
+
deadline = Time.mktime(dl_date.year, dl_date.month,
|
41
|
+
dl_date.day, 23, 59)
|
42
|
+
|
43
|
+
if Date.new(deadline.year, deadline.month, deadline.day).cweek != date.cweek
|
44
|
+
statuses << Status.green
|
45
|
+
elsif time > deadline
|
46
|
+
statuses << Status.missed
|
47
|
+
elsif time > (deadline - habit.red_zone)
|
48
|
+
statuses << Status.red
|
49
|
+
elsif time > (deadline - habit.yellow_zone)
|
50
|
+
statuses << Status.yellow
|
51
|
+
else
|
52
|
+
statuses << Status.green
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
statuses.max
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
#
|
2
|
+
# A minimal command line parser for subcommands.
|
3
|
+
# CMD SUBCOMMAND ARG1 ARG2 ...
|
4
|
+
#
|
5
|
+
class Subcommand
|
6
|
+
attr_reader :subs
|
7
|
+
|
8
|
+
class Cmd
|
9
|
+
attr_reader :args, :desc, :blk
|
10
|
+
|
11
|
+
def initialize(args, desc, blk)
|
12
|
+
@args, @desc, @blk = args, desc, blk
|
13
|
+
@optional = @args.pop if @args.last =~ /^\[.*\]$/
|
14
|
+
end
|
15
|
+
|
16
|
+
def args_str
|
17
|
+
(@args + [@optional]).compact.join(' ')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse @call_args for this command
|
21
|
+
def parse(args)
|
22
|
+
if (args.size == @args.size) or (@optional and args.size == @args.size+1)
|
23
|
+
@call_args = args
|
24
|
+
else
|
25
|
+
raise "Invalid arguments."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Trigger this command
|
30
|
+
def call
|
31
|
+
if @optional # provide nil for the optional arg in block if needed
|
32
|
+
args = @call_args
|
33
|
+
args << nil if @call_args.size == @args.size
|
34
|
+
blk.call *args
|
35
|
+
else
|
36
|
+
blk.call *@call_args
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@default = nil
|
43
|
+
@subs = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Register a command with name, arguments, description, and block (blk).
|
47
|
+
# The block is called with as many input args as you defined in 'args'
|
48
|
+
def register(cmd_name, args, desc, &blk)
|
49
|
+
@subs ||= {}
|
50
|
+
@subs[cmd_name] = Cmd.new(args, desc, blk)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the default command to be called when just the executable is run.
|
54
|
+
def default(&blk)
|
55
|
+
raise "No block given" unless blk
|
56
|
+
@default = blk
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the default command to be one of the registered subcommands
|
60
|
+
def default=(cmd_name)
|
61
|
+
raise "No subcommand #{cmd_name} registerd" if @subs[cmd_name].nil?
|
62
|
+
@default = @subs[cmd_name].blk
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parse command line arguments according to registered commands.
|
66
|
+
def parse(args=ARGV)
|
67
|
+
if args.size == 0
|
68
|
+
@default.call if @default
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
sub = @subs[args.shift]
|
73
|
+
|
74
|
+
help and return if sub.nil?
|
75
|
+
|
76
|
+
begin
|
77
|
+
sub.parse(args)
|
78
|
+
sub.call
|
79
|
+
rescue Exception => e
|
80
|
+
puts e.message
|
81
|
+
help
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Print help of available commands.
|
86
|
+
def help
|
87
|
+
puts "\nUsage: #{File.basename($0)} {command} arg1 arg2 ...\n\nCommand Args"+
|
88
|
+
' '*32+"Description"
|
89
|
+
@subs.keys.sort.each do |name|
|
90
|
+
printf(" %-12s %-35s %s\n", name, @subs[name].args_str, @subs[name].desc)
|
91
|
+
end
|
92
|
+
puts
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/lib/habits/whip.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'habits/habit'
|
2
|
+
|
3
|
+
module Habits
|
4
|
+
|
5
|
+
module Whip
|
6
|
+
extend self
|
7
|
+
@@transitions = {}
|
8
|
+
@@ticks = []
|
9
|
+
|
10
|
+
# Add block to be executed when status changes to given status.
|
11
|
+
# A habit which changes to this status is passed to the block.
|
12
|
+
def on(status, &blk)
|
13
|
+
raise "No block given" if blk.nil?
|
14
|
+
@@transitions[status] = blk
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a block to be called each time before whip is used.
|
18
|
+
def on_tick(&blk)
|
19
|
+
raise "No block given" if blk.nil?
|
20
|
+
@@ticks << blk
|
21
|
+
end
|
22
|
+
|
23
|
+
# Use whip, i.e. update status of each habit and call
|
24
|
+
# blocks associated with state changes.
|
25
|
+
def use
|
26
|
+
@@ticks.each {|tick| tick.call}
|
27
|
+
|
28
|
+
Habit.all.each do |habit|
|
29
|
+
habit.update_status do
|
30
|
+
st = habit.status.value
|
31
|
+
@@transitions[st].call(habit) if @@transitions[st]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
config = File.join(Habits::Habit::HABITS_DIR, 'whip_config.rb')
|
39
|
+
require config if File.exists?(config)
|
data/lib/habits.rb
ADDED
data/whip_config.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#
|
2
|
+
# a habits whip config file
|
3
|
+
#
|
4
|
+
# Store in $HOME/.habits/whip_config.rb
|
5
|
+
#
|
6
|
+
|
7
|
+
def dialog(title, txt)
|
8
|
+
system %Q(say '#{title}')
|
9
|
+
system %Q(osascript -e 'tell app "System Events" to display dialog "#{txt}"')
|
10
|
+
# or if you have 'ruby-growl' gem installed
|
11
|
+
# system "growl -h localhost -m '#{txt}'"
|
12
|
+
end
|
13
|
+
|
14
|
+
Habits::Whip.on(:yellow) do |habit|
|
15
|
+
dialog habit.title, "[HABITS] #{habit.title} is now in yellow."
|
16
|
+
end
|
17
|
+
|
18
|
+
Habits::Whip.on(:red) do |habit|
|
19
|
+
dialog habit.title, "[HABITS] #{habit.title} is now in RED!"
|
20
|
+
end
|
21
|
+
|
22
|
+
Habits::Whip.on(:missed) do |habit|
|
23
|
+
dialog habit.title, "[HABITS] #{habit.title} MISSED!"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: habits
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Antti Hakala
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-26 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: A habit tracker. Tracks habits in weekly cycles.Each habit has a day or days associated with it.Habits expects activity on the habit on those days. If no activity is registered, habit goes first into yellow zone (e.g. 20 hours before deadline) and then into red zone (e.g. 6 hours). And finally into missed state.Transfer into each state can be used to trigger commands.
|
22
|
+
email: antti.hakala@gmail.com
|
23
|
+
executables:
|
24
|
+
- habits
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- README
|
31
|
+
- LICENSE
|
32
|
+
- bin/habits
|
33
|
+
- whip_config.rb
|
34
|
+
- lib/habits/command_line.rb
|
35
|
+
- lib/habits/event.rb
|
36
|
+
- lib/habits/events/activity.rb
|
37
|
+
- lib/habits/habit.rb
|
38
|
+
- lib/habits/mock_data_store.rb
|
39
|
+
- lib/habits/status.rb
|
40
|
+
- lib/habits/subcommand.rb
|
41
|
+
- lib/habits/whip.rb
|
42
|
+
- lib/habits.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://github.com/ander/habits
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 3
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.3.7
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: A habit tracker.
|
77
|
+
test_files: []
|
78
|
+
|