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.
Files changed (3) hide show
  1. data/bin/t_time_tracker +161 -0
  2. data/lib/t_time_tracker.rb +188 -0
  3. metadata +71 -0
@@ -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: