t_time_tracker 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: