t_time_tracker 0.0.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/bin/t_time_tracker +161 -0
- data/lib/t_time_tracker.rb +188 -0
- metadata +71 -0
data/bin/t_time_tracker
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Christian Genco (@cgenco)
|
3
|
+
|
4
|
+
require 'time'
|
5
|
+
require 'optparse'
|
6
|
+
require 't_time_tracker'
|
7
|
+
|
8
|
+
@tracker = TTimeTracker.new
|
9
|
+
@options = {:from => nil, :to => nil, :filter => '', :done => false}
|
10
|
+
parser = OptionParser.new do |opt|
|
11
|
+
opt.banner = "" +
|
12
|
+
"t-time-tracker: simple command line time tracking\n" +
|
13
|
+
"github.com/christiangenco/t-time-tracker\n\n" +
|
14
|
+
"Usage:\n" +
|
15
|
+
" $ t-time-tracker TASK_DESCRIPTION [OPTIONS]\n" +
|
16
|
+
" $ t-time-tracker making lunch\n" +
|
17
|
+
" $ t-time-tracker homework --at \"five minutes ago\"\n" +
|
18
|
+
" $ t-time-tracker --done\n" +
|
19
|
+
" $ t-time-tracker took a nap --from \"1 hour ago\" --to \"five minutes ago\"\n" +
|
20
|
+
" $ t-time-tracker --list --from \"one week ago\" --to \"yesterday\"\n"
|
21
|
+
opt.summary_indent = ' '
|
22
|
+
opt.separator "\nOptions:\n"
|
23
|
+
|
24
|
+
opt.on('-h', '--help', 'Shows this help message') do
|
25
|
+
puts parser
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
|
29
|
+
opt.on('-v', '--version', 'Shows the current version') do
|
30
|
+
puts '0.1'
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
|
34
|
+
opt.on('-u', '--update', 'Check github for updates') do
|
35
|
+
puts "Please check manually at https://github.com/christiangenco/t-time-tracker"
|
36
|
+
throw "Not yet implemented"
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
opt.on('-f', '--from [FROM]', 'The starting time in words (uses Chronic gem). Include any task that starts after it (inclusive)') do |f|
|
41
|
+
require 'chronic'
|
42
|
+
@options[:from] = Chronic.parse(f, :context => :past, :guess => false).first
|
43
|
+
end
|
44
|
+
|
45
|
+
opt.on('-a', '--at [AT]', 'A synonym for --from') do |f|
|
46
|
+
require 'chronic'
|
47
|
+
@options[:from] = Chronic.parse(f, :context => :past, :guess => false).first
|
48
|
+
end
|
49
|
+
|
50
|
+
opt.on('-t', '--to [TO]', 'The ending time in words (uses Chronic gem). Include any task that starts before it (inclusive)') do |t|
|
51
|
+
require 'chronic'
|
52
|
+
@options[:to] = (Chronic.parse(t, :context => :past, :guess => false)-1).last
|
53
|
+
end
|
54
|
+
|
55
|
+
opt.on('-d', '--done', 'End the current task without starting a new one') do
|
56
|
+
@options[:done] = true
|
57
|
+
end
|
58
|
+
|
59
|
+
opt.on('-s', '--stop', 'Synonym for --done') do
|
60
|
+
@options[:done] = true
|
61
|
+
end
|
62
|
+
|
63
|
+
opt.on('-r', '--resume', 'Resume the previous task') do
|
64
|
+
@options[:resume] = true
|
65
|
+
end
|
66
|
+
|
67
|
+
opt.on('-l', '--list', 'Print the tallied activities from --from to --to') do
|
68
|
+
@options[:list] = true
|
69
|
+
end
|
70
|
+
|
71
|
+
opt.on('-e', '--edit', 'Edit saved daily task files with your $EDITOR') do
|
72
|
+
STDERR.puts "opening #{@tracker.directory}"
|
73
|
+
|
74
|
+
if ! ENV['EDITOR']
|
75
|
+
puts "No EDITOR environment varible defined"
|
76
|
+
puts "Set your EDITOR in your .bashrc or .zshrc file by adding one of these lines:"
|
77
|
+
puts "\texport EDITOR='vim' # for vim"
|
78
|
+
puts "\texport EDITOR='subl' # for Sublime Text 2"
|
79
|
+
puts "\texport EDITOR='mate' # for Textmate"
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
|
83
|
+
# batch edit the logs
|
84
|
+
`#{ENV['EDITOR']} #{@tracker.directory}`
|
85
|
+
exit
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
parser.parse!
|
90
|
+
|
91
|
+
if @options[:list]
|
92
|
+
total, linewidth = 0, 0
|
93
|
+
day = nil
|
94
|
+
# print "@options ="
|
95
|
+
# p @options
|
96
|
+
@tracker.tasks(:from => @options[:from], :to => @options[:to]).each{ |task|
|
97
|
+
if day.nil? ||
|
98
|
+
day.year != task[:start].year ||
|
99
|
+
day.month != task[:start].month ||
|
100
|
+
day.day != task[:start].day
|
101
|
+
puts task[:start].strftime("# %F #")
|
102
|
+
day = task[:start]
|
103
|
+
end
|
104
|
+
line = "#{task[:start].strftime('%l:%M')}-" +
|
105
|
+
"#{task[:finish] ? task[:finish].strftime('%l:%M%P') : ' '*7}: " +
|
106
|
+
"#{TTimeTracker.format_minutes task[:duration]}" +
|
107
|
+
" #{task[:description]}"
|
108
|
+
linewidth = [linewidth, line.length].max
|
109
|
+
puts line
|
110
|
+
total += task[:duration]
|
111
|
+
}
|
112
|
+
puts "-" * linewidth
|
113
|
+
puts "total".ljust(13) + ": " + TTimeTracker.format_minutes(total).to_s
|
114
|
+
exit
|
115
|
+
end
|
116
|
+
|
117
|
+
def start_task(task)
|
118
|
+
task = @tracker.save(task)
|
119
|
+
puts "Started: #{task[:description]} (#{@options[:from] ? 'at ' + task[:start].strftime('%-l:%M%P') : 'now'})"
|
120
|
+
end
|
121
|
+
|
122
|
+
def finish_current_task(task = @tracker.current_task)
|
123
|
+
task[:finish] = @options[:to] || @options[:from] || Time.now
|
124
|
+
@tracker.save(task)
|
125
|
+
puts "Finished: #{task[:description]} (#{TTimeTracker.format_minutes task[:duration]})"
|
126
|
+
end
|
127
|
+
|
128
|
+
if @options[:resume]
|
129
|
+
if task = @tracker.last_task
|
130
|
+
start_task(:start => @options[:from], :finish => @options[:to], :description => task[:description])
|
131
|
+
else
|
132
|
+
STDERR.puts "No task to resume"
|
133
|
+
end
|
134
|
+
exit
|
135
|
+
end
|
136
|
+
|
137
|
+
if @options[:done]
|
138
|
+
finish_current_task
|
139
|
+
exit
|
140
|
+
end
|
141
|
+
|
142
|
+
# add a new task
|
143
|
+
if !ARGV.empty?
|
144
|
+
# end the current task if it exists
|
145
|
+
if current = @tracker.current_task
|
146
|
+
finish_current_task(current)
|
147
|
+
end
|
148
|
+
|
149
|
+
description = ARGV.join(" ")
|
150
|
+
start_task(:start => @options[:from], :finish => @options[:to], :description => description)
|
151
|
+
exit
|
152
|
+
end
|
153
|
+
|
154
|
+
# default action: show current task
|
155
|
+
unless current = @tracker.current_task
|
156
|
+
STDERR.puts "You're not working on anything"
|
157
|
+
exit
|
158
|
+
end
|
159
|
+
|
160
|
+
puts "In progress: #{current[:description]} (#{TTimeTracker.format_minutes current[:duration]})"
|
161
|
+
exit
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# yardoc info: http://cheat.errtheblog.com/s/yard/
|
2
|
+
|
3
|
+
class TTimeTracker
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
# @author Christian Genco (@cgenco)
|
7
|
+
|
8
|
+
# @attribute directory [String] the parent directory that the log files are stored in
|
9
|
+
# @attribute subdirectory [String] the subdirectory in which the current task will be stored
|
10
|
+
# @attribute filename [String] the full path to the log file of the current task
|
11
|
+
# @attribute task [String] the users entered task
|
12
|
+
# @attribute at [Time] the time that the task (or range) starts at
|
13
|
+
# @attribute to [Time] the time that the task (or range) ends at
|
14
|
+
attr_accessor :directory, :subdirectory, :filename, :task, :now #, :at, :to, :now
|
15
|
+
|
16
|
+
# A new instance of TTimeTracker.
|
17
|
+
# @param [Hash] params Options hash
|
18
|
+
# @option params [Symbol] :now the date to consider when deciding which log file to use
|
19
|
+
# @option params [Symbol] :directory the parent directory that the log files are stored in
|
20
|
+
# @option params [Symbol] :subdirectory the subdirectory in which the current task will be stored
|
21
|
+
# @option params [Symbol] :filename the full path to the log file of the current task
|
22
|
+
def initialize(params = {})
|
23
|
+
@now = params[:now] || Time.now
|
24
|
+
@directory = params[:directory] || File.join(Dir.home, '.ttimetracker')
|
25
|
+
@subdirectory = params[:subdirectory] || File.join(@directory, now.year.to_s, now.strftime("%m_%b"), '')
|
26
|
+
@filename = params[:filename] || File.join(@subdirectory, now.strftime('%Y-%m-%d') + '.csv')
|
27
|
+
self.class.mkdir @subdirectory
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns information about the specified task.
|
31
|
+
#
|
32
|
+
# @param task_name [Symbol] the stored task to return, `:current` or `:last`
|
33
|
+
def task(task_name)
|
34
|
+
task_filename = File.join(@directory, task_name.to_s)
|
35
|
+
return nil unless File.exists?(task_filename)
|
36
|
+
File.open(task_filename,'r') do |f|
|
37
|
+
line = f.gets
|
38
|
+
return if line.nil?
|
39
|
+
parse_task(line)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# equivalent to task(:current)
|
44
|
+
def current_task; task(:current); end
|
45
|
+
|
46
|
+
# equivalent to task(:last)
|
47
|
+
def last_task; task(:last); end
|
48
|
+
|
49
|
+
# returns an array of hashed tasks between the specified times. Defaults to today.
|
50
|
+
# @todo figure out how to make this work with an arbitrary directory structure
|
51
|
+
# @param [Hash] params Options hash
|
52
|
+
# @option params [Symbol] :from the starting time; includes any task that starts after it (inclusive)
|
53
|
+
# @option params [Symbol] :to the ending time; includes any task that starts before it (inclusive)
|
54
|
+
def tasks(params = {})
|
55
|
+
require 'active_support/core_ext/time/calculations'
|
56
|
+
require 'active_support/core_ext/date/calculations'
|
57
|
+
|
58
|
+
# Time.parse(Time.new.strftime("%F 0:00:00 %z"))
|
59
|
+
from = params[:from] || Time.new.beginning_of_day
|
60
|
+
# Time.parse(Time.new.strftime("%F 23:59:59 %z"))
|
61
|
+
to = params[:to] || Time.new.end_of_day
|
62
|
+
# ensure from < to
|
63
|
+
from, to = [from, to].sort
|
64
|
+
|
65
|
+
tasks = []
|
66
|
+
|
67
|
+
# first, get every task for the correct days
|
68
|
+
now = from
|
69
|
+
while now <= to
|
70
|
+
# TODO: make this work for arbitrary folder organisation structures
|
71
|
+
subdirectory = File.join(@directory, now.year.to_s, now.strftime("%m_%b"), '')
|
72
|
+
filename = File.join(subdirectory, now.strftime('%Y-%m-%d') + '.csv')
|
73
|
+
File.open(filename, 'r').each do |line|
|
74
|
+
tasks << parse_task(line, :day => now)
|
75
|
+
end if File.exists?(filename)
|
76
|
+
now = now.tomorrow
|
77
|
+
end
|
78
|
+
|
79
|
+
# now filter out tasks that don't fall within the requested timespan
|
80
|
+
tasks.delete_if{|t|
|
81
|
+
t[:start] < from || t[:start] > to
|
82
|
+
}
|
83
|
+
|
84
|
+
tasks
|
85
|
+
end
|
86
|
+
|
87
|
+
# warning: this will overwrite the current task. You need to save the current task before saving a new one.
|
88
|
+
def save(task = {})
|
89
|
+
# forget the last task
|
90
|
+
last = File.join(@directory, "last")
|
91
|
+
File.unlink(last) if File.exists?(last)
|
92
|
+
|
93
|
+
task[:start] ||= @now
|
94
|
+
|
95
|
+
# save this as the current task if it doesn't have an ending time
|
96
|
+
if !task[:finish]
|
97
|
+
File.open(File.join(@directory, "current"),'w') do |f|
|
98
|
+
f.puts [format_time(task[:start]), task[:description].strip].join(", ")
|
99
|
+
end
|
100
|
+
else
|
101
|
+
# task has start and finish time, so append it to today's log...
|
102
|
+
File.open(@filename,'a') do |f|
|
103
|
+
f.puts [format_time(task[:start]), format_time(task[:finish]), task[:description].strip].join(", ")
|
104
|
+
end
|
105
|
+
|
106
|
+
# ...and save it as "last" in case you want to resume it
|
107
|
+
File.rename(File.join(@directory, 'current'), File.join(@directory, 'last'))
|
108
|
+
end
|
109
|
+
|
110
|
+
task
|
111
|
+
end
|
112
|
+
|
113
|
+
# Converts an integer of minutes into a more human readable format.
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# format_minutes(95) #=> "1:15"
|
117
|
+
# format_minutes(5) #=> "0:05"
|
118
|
+
#
|
119
|
+
# @param minutes [Integer] a number of minutes
|
120
|
+
# @return [String] the formatted minutes
|
121
|
+
def self.format_minutes(minutes)
|
122
|
+
"#{minutes.to_i / 60}:#{'%02d' % (minutes % 60)}"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Parses a comma separated stored task in csv form
|
126
|
+
#
|
127
|
+
# @example
|
128
|
+
# parse_task("12:56, 13:10, did the dishes")
|
129
|
+
# #=> {:start=>2012-05-16 12:56:00, :finish=>2012-05-16 13:10:00, :description=>"did the dishes", :duration=>14}
|
130
|
+
# parse_task("14:32, homework")
|
131
|
+
# #=> {:start=>2012-05-16 14:32:00, :finish=>Time.now, :description=>"homework", :duration=>36}
|
132
|
+
#
|
133
|
+
# @param line [String] the CSV stored task
|
134
|
+
# @param [Hash] params Options hash
|
135
|
+
# @option params [Symbol] :day the default day to assign to times parsed. Defaults to @now.
|
136
|
+
# @return [{:start=>Time, :finish=>Time, :description=>String, :duration=>Integer}] the parsed data in the line
|
137
|
+
def parse_task(line, params = {})
|
138
|
+
def parse_time(time_string, day)
|
139
|
+
# if the time already has a date, parse that time
|
140
|
+
# else assign a date
|
141
|
+
if time_string =~ /\d{4}-\d{2}-\d{2}/
|
142
|
+
Time.parse(time_string)
|
143
|
+
else
|
144
|
+
Time.parse(day.strftime("%F ") + time_string)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
day = params[:day] || @now
|
149
|
+
data = line.split(",").map(&:strip)
|
150
|
+
start = parse_time(data.shift, day)
|
151
|
+
|
152
|
+
if data.length == 2
|
153
|
+
# if there are two more values, they are the finished time and the description
|
154
|
+
finish = parse_time(data.shift, day)
|
155
|
+
else
|
156
|
+
# otherwise the last value is the description; get finish elsewhere
|
157
|
+
finish = @now
|
158
|
+
end
|
159
|
+
|
160
|
+
description = data.shift
|
161
|
+
duration = ((finish - start).to_f / 60).ceil
|
162
|
+
|
163
|
+
return {:start => start, :finish => finish, :description => description, :duration => duration}
|
164
|
+
end
|
165
|
+
|
166
|
+
# Create directory if it doesn't exist, creating intermediate
|
167
|
+
# directories as required. Equivalent to `mkdir -p`.
|
168
|
+
#
|
169
|
+
# @param dir [String] a directory name
|
170
|
+
def self.mkdir(dir)
|
171
|
+
mkdir(File.dirname dir) unless File.dirname(dir) == dir
|
172
|
+
Dir.mkdir(dir) unless dir.empty? || File.directory?(dir)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Converts a Time object into a human readable condensed string.
|
176
|
+
# Options for strftime may be found here:
|
177
|
+
# http://www.ruby-doc.org/core-1.9.3/Time.html#method-i-strftime
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# time = Time.new #=> 2012-05-16 00:32:31 +0800
|
181
|
+
# format_time(time) #=> "00:32:31"
|
182
|
+
#
|
183
|
+
# @param time [Time] a time
|
184
|
+
# @return [String] the formatted time
|
185
|
+
def format_time(time)
|
186
|
+
time.strftime("%H:%M:%S")
|
187
|
+
end
|
188
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: t_time_tracker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Christian Genco
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-16 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chronic
|
16
|
+
requirement: &70149933481160 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.6.7
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70149933481160
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &70149933479240 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.2.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70149933479240
|
36
|
+
description: It's like a log file for your life. Keep track of everything from freelance
|
37
|
+
project billing to how many hours per week you spend eating.
|
38
|
+
email: ! '@cgenco'
|
39
|
+
executables:
|
40
|
+
- t_time_tracker
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- lib/t_time_tracker.rb
|
45
|
+
- bin/t_time_tracker
|
46
|
+
homepage: https://github.com/christiangenco/t_time_tracker
|
47
|
+
licenses: []
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
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
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.8.10
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: simple comand line time tracking
|
70
|
+
test_files: []
|
71
|
+
has_rdoc:
|