quirk 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []