tracking 1.0.0 → 1.1.0
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.
- data/.travis.yml +5 -2
- data/CHANGELOG.md +9 -0
- data/Gemfile +14 -7
- data/README.md +24 -9
- data/Rakefile +10 -10
- data/VERSION +1 -1
- data/bin/tracking +1 -1
- data/lib/tracking.rb +9 -20
- data/lib/tracking/cli.rb +117 -109
- data/lib/tracking/config.rb +34 -15
- data/lib/tracking/list.rb +77 -57
- data/lib/tracking/task.rb +99 -0
- data/spec/cli_spec.rb +36 -0
- data/spec/helper.rb +21 -0
- data/tracking.gemspec +81 -0
- metadata +103 -22
- data/test/helper.rb +0 -39
- data/test/test_cli.rb +0 -47
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
data/Gemfile
CHANGED
@@ -1,14 +1,21 @@
|
|
1
|
-
source
|
1
|
+
source :rubygems
|
2
2
|
|
3
3
|
# Add dependencies required to use your gem here.
|
4
|
+
gem 'colorize', '~> 0.5'
|
4
5
|
|
5
6
|
# Add dependencies to develop your gem here.
|
6
7
|
# Include everything needed to run rake, tests, features, etc.
|
7
8
|
group :development do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
gem 'rdoc', '~> 3.12'
|
10
|
+
gem 'bundler', '>= 1.0.0'
|
11
|
+
gem 'jeweler', '~> 1.8.4'
|
12
|
+
gem 'simplecov'
|
13
|
+
gem 'yard'
|
14
|
+
gem 'rspec'
|
15
|
+
platforms :ruby do
|
16
|
+
gem 'redcarpet'
|
17
|
+
end
|
18
|
+
platforms :jruby do
|
19
|
+
gem 'kramdown'
|
20
|
+
end
|
14
21
|
end
|
data/README.md
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
#tracking [](http://travis-ci.org/thenickperson/tracking)
|
1
|
+
#tracking [](http://travis-ci.org/thenickperson/tracking) [](https://gemnasium.com/thenickperson/tracking) [](https://codeclimate.com/github/thenickperson/tracking)
|
2
2
|
A simple and configurable command line time tracker.
|
3
3
|
|
4
4
|
##Installation
|
5
5
|
|
6
6
|
`gem install tracking`
|
7
7
|
|
8
|
-
If you're on Windows, you should set up
|
8
|
+
If you're on Windows, you should set up
|
9
|
+
[Ruby Installer](http://rubyinstaller.org/downloads/) and
|
10
|
+
[DevKit](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit) first.
|
9
11
|
|
10
12
|
Also, tracking does not work on Ruby 1.8 (yet). Please upgrade to Ruby 1.9.3.
|
11
13
|
|
@@ -73,6 +75,8 @@ The default settings are listed below, along with a description of each setting.
|
|
73
75
|
:task_width: 40
|
74
76
|
# format to use for elapsed time display (:colons or :letters)
|
75
77
|
:elapsed_format: :colons
|
78
|
+
# toggle colored display of the current (last) task
|
79
|
+
:color_current_task: true
|
76
80
|
# toggle header describing tracking's display columns (true or false)
|
77
81
|
:show_header: true
|
78
82
|
# toggle display of seconds in elapsed time (true of false)
|
@@ -80,7 +84,8 @@ The default settings are listed below, along with a description of each setting.
|
|
80
84
|
```
|
81
85
|
|
82
86
|
##Elapsed Time Formats
|
83
|
-
Elapsed times are displayed in this order: days, hours, minutes, seconds (if
|
87
|
+
Elapsed times are displayed in this order: days, hours, minutes, seconds (if
|
88
|
+
enabled)
|
84
89
|
- hide elapsed seconds
|
85
90
|
- colons: `01:02:03` (default)
|
86
91
|
- letters: `01d 02h 03m`
|
@@ -89,18 +94,28 @@ Elapsed times are displayed in this order: days, hours, minutes, seconds (if ena
|
|
89
94
|
- letters: `01d 02h 03m 04s`
|
90
95
|
|
91
96
|
##Contributing to tracking
|
92
|
-
- Check out the latest master to make sure the feature hasn't been implemented
|
93
|
-
|
97
|
+
- Check out the latest master to make sure the feature hasn't been implemented
|
98
|
+
or the bug hasn't been fixed yet.
|
99
|
+
- Check out the issue tracker to make sure someone already hasn't requested it
|
100
|
+
and/or contributed it.
|
94
101
|
- Fork the project.
|
95
102
|
- Start a feature/bugfix branch.
|
96
103
|
- Commit and push until you are happy with your contribution.
|
97
|
-
- Make sure to add tests for it. This is important so I don't break it in a
|
98
|
-
|
104
|
+
- Make sure to add tests for it. This is important so I don't break it in a
|
105
|
+
future version unintentionally.
|
106
|
+
- Please try not to mess with the Rakefile, version, or history. If you want to
|
107
|
+
have your own version, or is otherwise necessary, that is fine, but please
|
108
|
+
isolate to its own commit so I can cherry-pick around it.
|
99
109
|
|
100
110
|
##Similar Projects
|
101
111
|
- [timetrap](https://github.com/samg/timetrap)
|
102
112
|
- [d-time-tracker](https://github.com/DanielVF/d-time-tracker)
|
113
|
+
- [to-do](http://github.com/kristenmills/to-do) if you want a good command line
|
114
|
+
todo manager to complement tracking
|
115
|
+
|
116
|
+
##Special Thanks
|
117
|
+
- [to-do](http://github.com/kristenmills/to-do) and
|
118
|
+
[timetrap](https://github.com/samg/timetrap) for letting me borrow some code
|
103
119
|
|
104
120
|
##Copyright
|
105
|
-
Copyright (c) 2012 Nicolas McCurdy. See LICENSE.txt for
|
106
|
-
further details.
|
121
|
+
Copyright (c) 2012 Nicolas McCurdy. See LICENSE.txt for further details.
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ begin
|
|
6
6
|
Bundler.setup(:default, :development)
|
7
7
|
rescue Bundler::BundlerError => e
|
8
8
|
$stderr.puts e.message
|
9
|
-
$stderr.puts
|
9
|
+
$stderr.puts 'Run `bundle install` to install missing gems'
|
10
10
|
exit e.status_code
|
11
11
|
end
|
12
12
|
require 'rake'
|
@@ -14,13 +14,13 @@ require 'rake'
|
|
14
14
|
require 'jeweler'
|
15
15
|
Jeweler::Tasks.new do |gem|
|
16
16
|
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
-
gem.name =
|
18
|
-
gem.homepage =
|
19
|
-
gem.license =
|
20
|
-
gem.summary =
|
21
|
-
gem.description =
|
22
|
-
gem.email =
|
23
|
-
gem.authors = [
|
17
|
+
gem.name = 'tracking'
|
18
|
+
gem.homepage = 'http://github.com/thenickperson/tracking'
|
19
|
+
gem.license = 'MIT'
|
20
|
+
gem.summary = 'A simple and configurable command line time tracker.'
|
21
|
+
gem.description = 'See README for more information.'
|
22
|
+
gem.email = 'thenickperson@gmail.com'
|
23
|
+
gem.authors = ['Nicolas McCurdy']
|
24
24
|
# dependencies defined in Gemfile
|
25
25
|
end
|
26
26
|
Jeweler::RubygemsDotOrgTasks.new
|
@@ -32,9 +32,9 @@ Rake::TestTask.new(:test) do |test|
|
|
32
32
|
test.verbose = true
|
33
33
|
end
|
34
34
|
|
35
|
-
desc
|
35
|
+
desc 'Code coverage detail'
|
36
36
|
task :simplecov do
|
37
|
-
ENV['COVERAGE'] =
|
37
|
+
ENV['COVERAGE'] = 'true'
|
38
38
|
Rake::Task['spec'].execute
|
39
39
|
end
|
40
40
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.1.0
|
data/bin/tracking
CHANGED
data/lib/tracking.rb
CHANGED
@@ -1,21 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require File.join(File.dirname(__FILE__),
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
#create config file
|
11
|
-
if not File.exist? File.join(ENV["HOME"], ".tracking", "config.yml")
|
12
|
-
Tracking::Config.write
|
1
|
+
# Require all of tracking's stuff
|
2
|
+
require File.join(File.dirname(__FILE__), 'tracking', 'config')
|
3
|
+
require File.join(File.dirname(__FILE__), 'tracking', 'list')
|
4
|
+
require File.join(File.dirname(__FILE__), 'tracking', 'task')
|
5
|
+
require File.join(File.dirname(__FILE__), 'tracking', 'cli')
|
6
|
+
|
7
|
+
# Tracking is the main namespace that all of the other modules and classes are a
|
8
|
+
# part of
|
9
|
+
module Tracking
|
13
10
|
end
|
14
|
-
|
15
|
-
#create data file
|
16
|
-
if not File.exist? File.expand_path Tracking::Config[:data_file]
|
17
|
-
FileUtils.touch File.expand_path Tracking::Config[:data_file]
|
18
|
-
end
|
19
|
-
|
20
|
-
require File.join(File.dirname(__FILE__), "tracking", "list")
|
21
|
-
require File.join(File.dirname(__FILE__), "tracking", "cli")
|
data/lib/tracking/cli.rb
CHANGED
@@ -1,152 +1,153 @@
|
|
1
|
-
|
1
|
+
require 'optparse'
|
2
|
+
require 'colorize'
|
2
3
|
|
3
|
-
#imports
|
4
|
-
require "optparse"
|
5
|
-
|
6
|
-
#view module methods
|
7
4
|
module Tracking
|
5
|
+
# Contains methods for displaying the list in a command line and parsing
|
6
|
+
# command line arguments.
|
8
7
|
module CLI
|
9
|
-
|
10
8
|
extend self
|
11
9
|
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
data_file.each_with_index do |line, index|
|
37
|
-
if index+1 > file_length - Config[:lines]
|
38
|
-
data << line
|
39
|
-
end
|
40
|
-
end
|
41
|
-
#display data
|
42
|
-
for i in 0..data.length-1
|
43
|
-
if data[i].length == 2
|
44
|
-
begin
|
45
|
-
#grab and reformat data
|
46
|
-
time_string = Time.parse(data[i][0]).strftime("%H:%M")
|
47
|
-
task_string = data[i][1].chomp
|
48
|
-
start_time = Time.parse(data[i][0])
|
49
|
-
end_time = i<data.length-1 ? Time.parse(data[i+1][0]) : Time.now
|
50
|
-
elapsed_string = List.get_elapsed_time(start_time,end_time)
|
51
|
-
#format data into lines
|
52
|
-
lines = []
|
53
|
-
split_task(task_string).each_with_index do |task_line, i|
|
54
|
-
col_1 = pad(i==0 ? time_string : nil, 5)
|
55
|
-
col_2 = pad(task_line, Config[:task_width])
|
56
|
-
col_3 = pad(i==0 ? elapsed_string : nil, elapsed_time_length)
|
57
|
-
lines << "| #{col_1} | #{col_2} | #{col_3} |"
|
58
|
-
end
|
59
|
-
#display lines
|
60
|
-
if valid_lines == 0
|
61
|
-
puts horizontal_border
|
62
|
-
if Config[:show_header]
|
63
|
-
puts header
|
64
|
-
puts horizontal_border
|
65
|
-
end
|
10
|
+
# Width of the first column (start time)
|
11
|
+
@start_time_width = 5
|
12
|
+
|
13
|
+
# Width of the second column (name)
|
14
|
+
@name_width = Config[:task_width]
|
15
|
+
|
16
|
+
# Width of the third column (elapsed time)
|
17
|
+
@elapsed_time_width = Task.elapsed_time_length
|
18
|
+
|
19
|
+
# Displays part of the list in the command line
|
20
|
+
#
|
21
|
+
# @param [Integer] max the maximum number of items to display
|
22
|
+
def display max=Config[:lines]
|
23
|
+
display_object :top
|
24
|
+
tasks = List.get max
|
25
|
+
if tasks.length > 0
|
26
|
+
tasks.each_with_index do |task, task_index|
|
27
|
+
current_task = (task_index + 1 == tasks.length)
|
28
|
+
split_task(task.name).each_with_index do |name_line, line_index|
|
29
|
+
col_1 = pad(line_index==0 ? task.start_time : nil, 5)
|
30
|
+
col_2 = pad(name_line, @name_width)
|
31
|
+
col_3 = pad(line_index==0 ? task.elapsed_time : nil, @elapsed_time_width)
|
32
|
+
if current_task and Config[:color_current_task]
|
33
|
+
col_1,col_2,col_3 = col_1.yellow,col_2.yellow,col_3.yellow
|
66
34
|
end
|
67
|
-
|
68
|
-
valid_lines += 1
|
69
|
-
rescue
|
70
|
-
invalid_lines += 1
|
35
|
+
puts "| #{col_1} | #{col_2} | #{col_3} |"
|
71
36
|
end
|
72
|
-
else
|
73
|
-
invalid_lines += 1
|
74
37
|
end
|
75
|
-
end
|
76
|
-
#display intro, if needed
|
77
|
-
if valid_lines > 0
|
78
|
-
puts horizontal_border
|
79
38
|
else
|
80
|
-
|
39
|
+
display_object :intro
|
81
40
|
end
|
82
|
-
|
41
|
+
display_object :bottom
|
42
|
+
# Display a warning, if needed
|
43
|
+
=begin
|
83
44
|
if invalid_lines > 0
|
84
|
-
warn "Error: #{invalid_lines} invalid line#{
|
45
|
+
warn "Error: #{invalid_lines} invalid line#{'s' if invalid_lines > 1} found in data file."
|
85
46
|
end
|
47
|
+
=end
|
86
48
|
end
|
87
49
|
|
88
|
-
#
|
50
|
+
# Displays commonly used text objects in the command line
|
51
|
+
#
|
52
|
+
# @param type the type of text object to display (:top/:bottom/:intro)
|
53
|
+
def display_object type
|
54
|
+
horizontal_border = "+-------+-#{'-'*@name_width}-+-#{'-'*@elapsed_time_width}-+"
|
55
|
+
case type
|
56
|
+
when :top
|
57
|
+
puts horizontal_border
|
58
|
+
if Config[:show_header]
|
59
|
+
puts "| start | #{pad('task', @name_width, :center)} | #{pad('elapsed', @elapsed_time_width, :center)} |"
|
60
|
+
puts horizontal_border
|
61
|
+
end
|
62
|
+
when :bottom
|
63
|
+
puts horizontal_border
|
64
|
+
when :intro
|
65
|
+
intro_text = <<-EOF
|
66
|
+
You haven't started any tasks yet! :(
|
67
|
+
|
68
|
+
Run this to begin your first task:
|
69
|
+
tracking starting some work
|
70
|
+
EOF
|
71
|
+
intro_text.each_line do |line|
|
72
|
+
puts "| | #{pad(line.chomp, @name_width)} | #{pad(nil, @elapsed_time_width)} |"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Pads tasks with whitespace to align them for display
|
78
|
+
#
|
79
|
+
# @param [String] string the string to pad
|
80
|
+
# @param [Integer] length the length of the resultant string
|
81
|
+
# @param [Symbol] align the alignment of the start string within the end
|
82
|
+
# string (:left/:right/:center)
|
83
|
+
# @return [String] the padded string
|
89
84
|
def pad(string, length, align=:left)
|
90
85
|
if string == nil
|
91
|
-
return
|
86
|
+
return ' ' * length
|
92
87
|
elsif string.length >= length
|
93
88
|
return string
|
94
89
|
else
|
95
90
|
difference = (length - string.length).to_f
|
96
91
|
case align
|
97
92
|
when :left
|
98
|
-
return string +
|
93
|
+
return string + ' ' * difference
|
99
94
|
when :right
|
100
|
-
return
|
95
|
+
return ' ' * difference + string
|
101
96
|
when :center
|
102
|
-
return
|
97
|
+
return ' '*(difference/2).floor + string + ' '*(difference/2).ceil
|
103
98
|
else
|
104
99
|
return string
|
105
100
|
end
|
106
101
|
end
|
107
102
|
end
|
108
103
|
|
109
|
-
#
|
104
|
+
# Word wraps tasks into multiple lines for display (based on the user's task
|
105
|
+
# width setting)
|
106
|
+
#
|
107
|
+
# @param [String] task the task string to split up
|
108
|
+
# @return [Array] an array of strings, each containing an individual line of
|
109
|
+
# wrapped text
|
110
110
|
def split_task task
|
111
111
|
|
112
|
-
#
|
112
|
+
# If the task fits
|
113
113
|
if task.length <= Config[:task_width]
|
114
114
|
return [task]
|
115
115
|
|
116
|
-
#
|
116
|
+
# If the task needs to be split
|
117
117
|
else
|
118
|
-
words = task.split(
|
118
|
+
words = task.split(' ')
|
119
119
|
split = []
|
120
|
-
line =
|
120
|
+
line = ''
|
121
121
|
words.each do |word|
|
122
122
|
|
123
|
-
#
|
123
|
+
# If the word needs to be split
|
124
124
|
if word.length > Config[:task_width]
|
125
|
-
#
|
125
|
+
# Add the start of the word onto the first line (even if it has
|
126
|
+
# already started)
|
126
127
|
while line.length < Config[:task_width]
|
127
128
|
line += word[0]
|
128
129
|
word = word[1..-1]
|
129
130
|
end
|
130
131
|
split << line
|
131
|
-
#
|
132
|
+
# Split the rest of the word up onto new lines
|
132
133
|
split_word = word.scan(%r[.{1,#{Config[:task_width]}}])
|
133
134
|
split_word[0..-2].each do |word_section|
|
134
135
|
split << word_section
|
135
136
|
end
|
136
137
|
line = split_word.last
|
137
138
|
|
138
|
-
#
|
139
|
+
# If the word would fit on a new line
|
139
140
|
elsif (line + word).length > Config[:task_width]
|
140
141
|
split << line.chomp
|
141
142
|
line = word
|
142
143
|
|
143
|
-
#
|
144
|
+
# If the word can be added to this line
|
144
145
|
else
|
145
146
|
line += word
|
146
147
|
end
|
147
148
|
|
148
|
-
#
|
149
|
-
line +=
|
149
|
+
# Add a space to the end of the last word, if it would fit
|
150
|
+
line += ' ' if line.length != Config[:task_width]
|
150
151
|
|
151
152
|
end
|
152
153
|
split << line
|
@@ -154,47 +155,54 @@ EOF
|
|
154
155
|
end
|
155
156
|
end
|
156
157
|
|
157
|
-
#
|
158
|
+
# Use option parser to parse command line arguments and run the selected
|
159
|
+
# command with its selected options
|
158
160
|
def parse
|
159
161
|
#options = {}
|
160
162
|
done = false
|
161
163
|
|
162
164
|
OptionParser.new do |opts|
|
163
|
-
#
|
164
|
-
version_path = File.expand_path(
|
165
|
-
opts.version = File.exist?(version_path) ? File.read(version_path) :
|
166
|
-
#
|
167
|
-
opts.banner =
|
168
|
-
opts.separator
|
169
|
-
opts.separator
|
170
|
-
#
|
171
|
-
opts.on(
|
172
|
-
List.
|
173
|
-
|
165
|
+
# Setup
|
166
|
+
version_path = File.expand_path('../../VERSION', File.dirname(__FILE__))
|
167
|
+
opts.version = File.exist?(version_path) ? File.read(version_path) : ''
|
168
|
+
# Start of help text
|
169
|
+
opts.banner = 'Usage: tracking [mode]'
|
170
|
+
opts.separator ' display tasks'
|
171
|
+
opts.separator ' <task description> start a new task with the given text (spaces allowed)'
|
172
|
+
# Modes
|
173
|
+
opts.on('-r', '--rename', 'rename the last task') do
|
174
|
+
List.rename ARGV.join(' ').gsub("\t",'')
|
175
|
+
display
|
174
176
|
done = true
|
175
177
|
return
|
176
178
|
end
|
177
|
-
opts.on(
|
179
|
+
opts.on('-d', '--delete', 'delete the last task') do
|
178
180
|
List.delete
|
179
181
|
display
|
180
182
|
done = true
|
181
183
|
return
|
182
184
|
end
|
183
|
-
opts.on(
|
185
|
+
opts.on('-c', '--clear', 'delete all tasks') do
|
186
|
+
List.clear
|
187
|
+
puts 'List cleared.'
|
188
|
+
done = true
|
189
|
+
return
|
190
|
+
end
|
191
|
+
opts.on('-h', '--help', 'display this help information') do
|
184
192
|
puts opts
|
185
193
|
done = true
|
186
194
|
return
|
187
195
|
end
|
188
196
|
end.parse!
|
189
197
|
|
190
|
-
#
|
191
|
-
|
198
|
+
# Basic modes (display and add)
|
199
|
+
unless done
|
192
200
|
if ARGV.count == 0
|
193
|
-
#
|
201
|
+
# Display all tasks
|
194
202
|
display
|
195
203
|
else
|
196
|
-
#
|
197
|
-
List.add ARGV.join(
|
204
|
+
# Start a new task
|
205
|
+
List.add ARGV.join(' ').gsub("\t",'')
|
198
206
|
display
|
199
207
|
end
|
200
208
|
end
|