quirk 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE.md +23 -0
  2. data/README.md +82 -0
  3. data/quirk +24 -0
  4. data/quirk.rb +239 -0
  5. data/quirk_test.rb +109 -0
  6. metadata +84 -0
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2012, Hugh Bien
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ Redistributions of source code must retain the above copyright notice, this list
8
+ of conditions and the following disclaimer.
9
+
10
+ Redistributions in binary form must reproduce the above copyright notice, this
11
+ list of conditions and the following disclaimer in the documentation and/or
12
+ other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,82 @@
1
+ Description
2
+ ===========
3
+
4
+ Quirk is a command line utility for tracking good and bad habits.
5
+
6
+ Installation
7
+ ============
8
+
9
+ % gem install quirk
10
+
11
+ Then configure your habits in a plaintext file:
12
+
13
+ % quirk -e
14
+ mile-run: monday, wednesday, thursday
15
+ walk-dog: everyday
16
+ ^quit-tv: friday
17
+
18
+ If a habit is prefixed with `^`, it means you're trying to break that habit.
19
+ In this case you're trying to quit TV on Fridays.
20
+
21
+ By default, all this does is edit the `~/.quirk` file. You can configure
22
+ which file to use by setting the environment variable `QUIRKFILE`.
23
+
24
+ Usage
25
+ =====
26
+
27
+ When you've done something, mark it with:
28
+
29
+ % quirk -m mile-run
30
+
31
+ To see a single habit (green days are good, red is bad):
32
+
33
+ % quirk -c mile-run
34
+ Jan 2012
35
+ Su Mo Tu We Th Fr Sa
36
+ 1 2 3 4 5 6 7
37
+ 8 9 10 11 12 13 14
38
+ 15 16 17 18 19 20 21
39
+ 22 23 24 25 26 27 28
40
+ 29 30 31
41
+
42
+ Looking for a specific year?
43
+
44
+ % quirk mile-run -y 2011
45
+
46
+ See all of your current streaks:
47
+
48
+ % quirk -s
49
+ 17 mile-run
50
+ 3 walk-dog
51
+ -3 quit-tv
52
+
53
+ Habits are stored in plaintext in `~/.quirk`. You can use `quirk -e` to
54
+ add/remove entries. Note that habits start on the day of the first mark
55
+ by default. You can also specify the first day using `^`:
56
+
57
+ 2012/01/01 walk-dog
58
+ 2012/01/01 ^quit-tv
59
+
60
+ The first line means you walked the dog on `1/1`. The second line means you
61
+ started the habit of quitting TV. This is especailly handy for starting
62
+ quitting habits on a green day.
63
+
64
+ Zsh Tab Completion
65
+ ==================
66
+
67
+ Here's an example zsh completion function:
68
+
69
+ #compdef quirk
70
+ compadd `quirk -l`
71
+
72
+ Put this into your `site-functions` directory (wherever `$fpath` points to):
73
+
74
+ % echo $fpath
75
+ /usr/share/zsh/site-functions /usr/share/zsh/4.3.11/functions
76
+ % sudo vim /usr/share/zsh/site-functions/_quirk
77
+
78
+ License
79
+ =======
80
+
81
+ Copyright 2012 Hugh Bien - http://hughbien.com.
82
+ Released under BSD License, see LICENSE.md for more info.
data/quirk ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require File.expand_path('quirk', File.dirname(__FILE__))
4
+
5
+ ARGV.options do |o|
6
+ begin
7
+ app = Quirk::App.new
8
+ action = nil
9
+ o.set_summary_indent(' ')
10
+ o.banner = "Usage: #{File.basename($0)} [OPTION]"
11
+ o.define_head "Track good and bad habits (v#{Quirk::VERSION})"
12
+ o.on('-c', '--calendar habit', 'show calendar') { |h| action = [:calendar, h] }
13
+ o.on('-e', '--edit', 'edit habits') { action = [:edit] }
14
+ o.on('-l', '--list', 'list habits') { action = [:list] }
15
+ o.on('-m', '--mark habit', 'mark for today') { |h| action = [:mark, h] }
16
+ o.on('-s', '--streaks', 'list streaks') { action = [:streaks] }
17
+ o.on('-y', '--year year', 'set calendar year') { |y| app.year = y }
18
+ o.on('-h', '--help', 'show this help message') { puts o; exit }
19
+ o.parse!
20
+ action.nil? ? puts(o) : app.send(*action)
21
+ rescue RuntimeError => e
22
+ puts e.to_s
23
+ end
24
+ end
@@ -0,0 +1,239 @@
1
+ require 'date'
2
+ require 'colorize'
3
+
4
+ module Quirk
5
+ VERSION = '0.0.2'
6
+ QUIRKFILE = ENV['QUIRKFILE'] || "#{ENV['HOME']}/.quirk"
7
+ EDITOR = ENV['EDITOR'] || 'vi'
8
+
9
+ def self.today
10
+ @today || Date.today
11
+ end
12
+
13
+ def self.today=(date) # for testing
14
+ @today = date
15
+ end
16
+
17
+ class App
18
+ def initialize(quirkfile = QUIRKFILE)
19
+ @quirkfile = quirkfile
20
+ end
21
+
22
+ def calendar(habit_id)
23
+ puts cal.output(habit_id)
24
+ end
25
+
26
+ def edit
27
+ `#{EDITOR} #{QUIRKFILE} < \`tty\` > \`tty\``
28
+ end
29
+
30
+ def list
31
+ puts cal.list
32
+ end
33
+
34
+ def mark(habit_id)
35
+ File.open(@quirkfile, 'a') do |file|
36
+ if cal.has_habit?(habit_id)
37
+ file.puts("#{Quirk.today.strftime('%Y/%m/%d')} #{habit_id}")
38
+ end
39
+ end
40
+ end
41
+
42
+ def streaks
43
+ puts cal.streaks
44
+ end
45
+
46
+ def year=(year)
47
+ raise "Invalid year: #{year}" if year !~ /\d\d\d\d/
48
+ cal.year = year.to_i
49
+ end
50
+
51
+ private
52
+ def cal
53
+ @cal ||= Calendar.parse(File.read(@quirkfile))
54
+ end
55
+ end
56
+
57
+ class Habit
58
+ attr_reader :id, :days, :marks
59
+
60
+ def initialize(id, days, quitting)
61
+ @id, @days, @quitting, @marks = id, days, quitting, []
62
+ end
63
+
64
+ def quitting?
65
+ !!@quitting
66
+ end
67
+
68
+ def mark!(date)
69
+ @marks << date
70
+ @marks.sort
71
+ end
72
+
73
+ def mark_first!(date)
74
+ @first_date = date
75
+ end
76
+
77
+ def first_date
78
+ @first_date || @marks.first
79
+ end
80
+
81
+ def color_on(date)
82
+ hit_color = quitting? ? :light_red : :light_green
83
+ miss_color = quitting? ? :light_green : :light_red
84
+ last_date = quitting? ? (Quirk.today + 1) : Quirk.today
85
+ if @marks.empty? ||
86
+ date < first_date ||
87
+ date > last_date ||
88
+ !days.include?(date.wday)
89
+ :white
90
+ elsif @marks.include?(date)
91
+ hit_color
92
+ else
93
+ date == last_date && !quitting? ? :white : miss_color
94
+ end
95
+ end
96
+
97
+ def streak
98
+ return 0 if @marks.empty?
99
+
100
+ count = 0
101
+ deltas = {:light_red => -1, :light_green => 1, :white => 0}
102
+ date = Quirk.today
103
+ while date >= first_date && (color = color_on(date)) == :white
104
+ date -= 1
105
+ end
106
+
107
+ init_color = color
108
+ while date >= first_date
109
+ color = color_on(date)
110
+ break if color != init_color && days.include?(date.wday)
111
+ date -= 1
112
+ count += deltas[color]
113
+ end
114
+ count
115
+ end
116
+
117
+ def self.parse(line)
118
+ line.strip!
119
+ line.gsub!(/\s+/, ' ')
120
+
121
+ quitting = line =~ /^\^/
122
+ line = line[1..-1] if quitting
123
+ id, days = line.split(':', 2).map(&:strip)
124
+ self.new(id, parse_days(days), quitting)
125
+ end
126
+
127
+ def self.parse_days(text)
128
+ originals = text.split(',').map(&:strip)
129
+ days = []
130
+ if originals.include?('everyday')
131
+ (0..6).to_a
132
+ else
133
+ %w(sunday monday tuesday wednesday
134
+ thursday friday saturday).each_with_index do |day, index|
135
+ days << (index) if originals.include?(day)
136
+ end
137
+ days
138
+ end
139
+ end
140
+ end
141
+
142
+ class Calendar
143
+ attr_writer :year
144
+
145
+ def initialize(habits)
146
+ @habits = habits.reduce({}) {|hash,habit| hash[habit.id] = habit; hash}
147
+ end
148
+
149
+ def year
150
+ @year || Quirk.today.year
151
+ end
152
+
153
+ def has_habit?(habit_id)
154
+ raise "No habit found: #{habit_id}" if !@habits.has_key?(habit_id)
155
+ true
156
+ end
157
+
158
+ def output(habit_id)
159
+ return if !has_habit?(habit_id)
160
+ habit = @habits[habit_id]
161
+ months = (1..12).map do |month|
162
+ first = Date.new(year, month, 1)
163
+ out = "#{first.strftime(' %b ')}\n"
164
+ out += "Su Mo Tu We Th Fr Sa\n"
165
+ out += (" " * first.wday)
166
+ while first.month == month
167
+ out += (first..(first + (6 - first.wday))).map do |date|
168
+ if date.year != year || date.month != month
169
+ ' '
170
+ else
171
+ len = date.day.to_s.length
172
+ "#{' ' * (2 - len)}#{date.day.to_s.colorize(habit.color_on(date))}"
173
+ end
174
+ end.join(" ")
175
+ out += "\n"
176
+ first += 7 - first.wday
177
+ end
178
+ out.split("\n")
179
+ end
180
+ out = "#{(' ' * 32)}#{year}\n"
181
+ index = 0
182
+ while index < 12
183
+ line = 0
184
+ max_line = [months[index], months[index+1], months[index+2]].
185
+ map(&:length).max
186
+ while line <= max_line
187
+ out += "#{months[index][line] || (' ' * 20)} "
188
+ out += "#{months[index+1][line] || ( ' ' * 20)} "
189
+ out += "#{months[index+2][line]}\n"
190
+ line += 1
191
+ end
192
+ index += 3
193
+ end
194
+ out
195
+ end
196
+
197
+ def list
198
+ @habits.values.map(&:id).sort.join("\n")
199
+ end
200
+
201
+ def mark!(line)
202
+ date = Date.parse(line.strip.split(/\s+/)[0])
203
+ originals = line.strip.split(/\s+/, 2)[1].split(',').map(&:strip)
204
+ originals.each do |original|
205
+ if original =~ /^\^\s*/
206
+ original = original.sub(/^\^\s*/, '')
207
+ habit = @habits[original] if has_habit?(original)
208
+ habit.mark_first!(date)
209
+ elsif has_habit?(original)
210
+ @habits[original].mark!(date)
211
+ end
212
+ end
213
+ end
214
+
215
+ def streaks
216
+ pairs = @habits.values.map do |habit|
217
+ [habit.streak, habit.id]
218
+ end.sort_by(&:first)
219
+ len = pairs.map {|p| p.first.to_s.length}.max
220
+ pairs.map do |count, id|
221
+ "#{' ' * (len - count.to_s.length)}#{count} #{id}"
222
+ end.join("\n")
223
+ end
224
+
225
+ def self.parse(text)
226
+ marks, habits = [], []
227
+ text.strip.each_line do |line|
228
+ if line =~ /^\d\d\d\d\/\d\d?\/\d\d?\s+/
229
+ marks << line
230
+ elsif line !~ /^\s*$/
231
+ habits << Habit.parse(line)
232
+ end
233
+ end
234
+ cal = Calendar.new(habits)
235
+ marks.each { |mark| cal.mark!(mark) }
236
+ cal
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,109 @@
1
+ require 'rubygems'
2
+ require "#{File.dirname(__FILE__)}/quirk"
3
+ require 'minitest/autorun'
4
+
5
+ Quirk.today = Date.new(2012, 1, 5) # 1 Su, 2 Mo, 3 Tu, 4 We, 5 Th
6
+
7
+ class QuirkAppTest < MiniTest::Unit::TestCase
8
+ def setup
9
+ @app = Quirk::App.new(File.join(File.dirname(__FILE__), 'quirkfile'))
10
+ end
11
+ end
12
+
13
+ class QuirkHabitTest < MiniTest::Unit::TestCase
14
+ def test_parse
15
+ habit = Quirk::Habit.parse(' running: monday , tuesday, wednesday')
16
+ assert_equal('running', habit.id)
17
+ assert_equal([1, 2, 3], habit.days)
18
+ refute(habit.quitting?)
19
+
20
+ habit2 = Quirk::Habit.parse('walk-dog: everyday')
21
+ assert_equal('walk-dog', habit2.id)
22
+ assert_equal([0, 1, 2, 3, 4, 5, 6], habit2.days)
23
+ refute(habit2.quitting?)
24
+
25
+ habit3 = Quirk::Habit.parse('^quit-tv: everyday')
26
+ assert_equal('quit-tv', habit3.id)
27
+ assert_equal([0, 1, 2, 3, 4, 5, 6], habit3.days)
28
+ assert(habit3.quitting?)
29
+ end
30
+
31
+ def test_color_on
32
+ habit = Quirk::Habit.parse('running: sunday, monday, wednesday, thursday')
33
+ habit.mark!(Date.new(2012, 1, 2))
34
+ habit.mark!(Date.new(2012, 1, 3))
35
+ assert_equal(:white, habit.color_on(Date.new(2012, 1, 1)))
36
+ assert_equal(:green, habit.color_on(Date.new(2012, 1, 2)))
37
+ assert_equal(:white, habit.color_on(Date.new(2012, 1, 3)))
38
+ assert_equal(:red, habit.color_on(Date.new(2012, 1, 4)))
39
+ assert_equal(:white, habit.color_on(Date.new(2012, 1, 5)))
40
+ end
41
+
42
+ def test_color_on_quit
43
+ quit = Quirk::Habit.parse('^quit-tv: sunday, monday, wednesday, thursday')
44
+ quit.mark!(Date.new(2012, 1, 2))
45
+ assert_equal(:white, quit.color_on(Date.new(2012, 1, 1)))
46
+ assert_equal(:red, quit.color_on(Date.new(2012, 1, 2)))
47
+ assert_equal(:white, quit.color_on(Date.new(2012, 1, 3)))
48
+ assert_equal(:green, quit.color_on(Date.new(2012, 1, 4)))
49
+ assert_equal(:green, quit.color_on(Date.new(2012, 1, 5)))
50
+
51
+ quit.mark_first!(Date.new(2012, 1, 1))
52
+ assert_equal(:green, quit.color_on(Date.new(2012, 1, 1)))
53
+ end
54
+
55
+ def test_streak
56
+ missing = Quirk::Habit.parse('running: everyday')
57
+ assert_equal(0, missing.streak)
58
+
59
+ perfect = Quirk::Habit.parse('running: monday, wednesday')
60
+ perfect.mark!(Date.new(2012, 1, 2))
61
+ perfect.mark!(Date.new(2012, 1, 4))
62
+ assert_equal(2, perfect.streak)
63
+
64
+ bad = Quirk::Habit.parse('running: monday, tuesday, wednesday')
65
+ bad.mark!(Date.new(2012, 1, 1))
66
+ bad.mark!(Date.new(2012, 1, 2))
67
+ assert_equal(-2, bad.streak)
68
+ end
69
+
70
+ def test_streak_quit
71
+ missing = Quirk::Habit.parse('^quit-tv: everyday')
72
+ assert_equal(0, missing.streak)
73
+
74
+ perfect = Quirk::Habit.parse('^quit-tv: monday, tuesday')
75
+ perfect.mark!(Date.new(2011, 12, 31))
76
+ perfect.mark!(Date.new(2012, 1, 4))
77
+ assert_equal(2, perfect.streak)
78
+
79
+ bad = Quirk::Habit.parse('^quit-tv: sunday, thursday')
80
+ bad.mark!(Date.new(2012, 1, 1)) # sunday
81
+ bad.mark!(Date.new(2012, 1, 5)) # thursday
82
+ assert_equal(-2, bad.streak)
83
+ end
84
+ end
85
+
86
+ class QuirkCalendarTest < MiniTest::Unit::TestCase
87
+ def setup
88
+ @running = Quirk::Habit.parse('running: everyday')
89
+ @walking = Quirk::Habit.parse('walk-dog: sunday, saturday')
90
+ @smoking = Quirk::Habit.parse('^smoking: everyday')
91
+ @cal = Quirk::Calendar.new([@running, @walking, @smoking])
92
+ @cal.mark!('2012/01/01 ^smoking')
93
+ @cal.mark!('2012/01/01 running, walk-dog')
94
+ @cal.mark!('2012/01/02 running, walk-dog')
95
+ end
96
+
97
+ def test_mark
98
+ assert_equal([Date.new(2012,1,1), Date.new(2012,1,2)], @running.marks)
99
+ assert_equal([Date.new(2012,1,1), Date.new(2012,1,2)], @walking.marks)
100
+ end
101
+
102
+ def test_mark_first
103
+ assert_equal(Date.new(2012, 1, 1), @smoking.first_date)
104
+ end
105
+
106
+ def test_streaks
107
+ assert_equal("-2 running\n 0 smoking\n 1 walk-dog", @cal.streaks)
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quirk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Hugh Bien
9
+ autorequire:
10
+ bindir: .
11
+ cert_chain: []
12
+ date: 2012-05-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: colorize
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Command line tool for tracking good/bad habits, data stored in plaintext.
47
+ email:
48
+ - hugh@hughbien.com
49
+ executables:
50
+ - quirk
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - quirk.rb
55
+ - quirk_test.rb
56
+ - README.md
57
+ - LICENSE.md
58
+ - quirk
59
+ - ./quirk
60
+ homepage: https://github.com/hughbien/quirk
61
+ licenses: []
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.6
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 1.8.23
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Track good and bad habits
84
+ test_files: []