tasklogger 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +80 -0
- data/bin/tasklogger +161 -0
- data/lib/task.rb +53 -0
- data/lib/task_logger.rb +68 -0
- metadata +76 -0
data/Rakefile
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "rake/testtask"
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
t.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
require "rubygems"
|
12
|
+
require "rake/gempackagetask"
|
13
|
+
require "rake/rdoctask"
|
14
|
+
|
15
|
+
# This builds the actual gem. For details of what all these options
|
16
|
+
# mean, and other ones you can add, check the documentation here:
|
17
|
+
#
|
18
|
+
# http://rubygems.org/read/chapter/20
|
19
|
+
#
|
20
|
+
spec = Gem::Specification.new do |s|
|
21
|
+
|
22
|
+
# Change these as appropriate
|
23
|
+
s.name = "tasklogger"
|
24
|
+
s.version = "0.0.2"
|
25
|
+
s.summary = "A simple task/time logging utility"
|
26
|
+
s.author = "Chris Roos"
|
27
|
+
s.email = "chris@seagul.co.uk"
|
28
|
+
s.homepage = "http://chrisroos.co.uk"
|
29
|
+
|
30
|
+
s.has_rdoc = true
|
31
|
+
# You should probably have a README of some kind. Change the filename
|
32
|
+
# as appropriate
|
33
|
+
# s.extra_rdoc_files = %w(README)
|
34
|
+
# s.rdoc_options = %w(--main README)
|
35
|
+
|
36
|
+
# Add any extra files to include in the gem (like your README)
|
37
|
+
s.files = %w(Rakefile) + Dir.glob("{bin,test,lib/**/*}")
|
38
|
+
s.executables = FileList["bin/**"].map { |f| File.basename(f) }
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
|
41
|
+
# If you want to depend on other gems, add them here, along with any
|
42
|
+
# relevant versions
|
43
|
+
# s.add_dependency("some_other_gem", "~> 0.1.0")
|
44
|
+
s.add_dependency 'fastercsv'
|
45
|
+
|
46
|
+
# If your tests use any gems, include them here
|
47
|
+
# s.add_development_dependency("mocha") # for example
|
48
|
+
s.add_development_dependency 'mocha'
|
49
|
+
end
|
50
|
+
|
51
|
+
# This task actually builds the gem. We also regenerate a static
|
52
|
+
# .gemspec file, which is useful if something (i.e. GitHub) will
|
53
|
+
# be automatically building a gem for this project. If you're not
|
54
|
+
# using GitHub, edit as appropriate.
|
55
|
+
#
|
56
|
+
# To publish your gem online, install the 'gemcutter' gem; Read more
|
57
|
+
# about that here: http://gemcutter.org/pages/gem_docs
|
58
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
59
|
+
pkg.gem_spec = spec
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "Build the gemspec file #{spec.name}.gemspec"
|
63
|
+
task :gemspec do
|
64
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
65
|
+
File.open(file, "w") {|f| f << spec.to_ruby }
|
66
|
+
end
|
67
|
+
|
68
|
+
task :package => :gemspec
|
69
|
+
|
70
|
+
# Generate documentation
|
71
|
+
Rake::RDocTask.new do |rd|
|
72
|
+
|
73
|
+
rd.rdoc_files.include("lib/**/*.rb")
|
74
|
+
rd.rdoc_dir = "rdoc"
|
75
|
+
end
|
76
|
+
|
77
|
+
desc 'Clear out RDoc and generated packages'
|
78
|
+
task :clean => [:clobber_rdoc, :clobber_package] do
|
79
|
+
rm "#{spec.name}.gemspec"
|
80
|
+
end
|
data/bin/tasklogger
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fastercsv'
|
6
|
+
require 'task_logger'
|
7
|
+
|
8
|
+
unless description = ARGV[0]
|
9
|
+
puts "Please enter a description for this task."
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
default_timelog = File.join(ENV['HOME'], 'timelog.csv')
|
14
|
+
timelog = ENV['TIMELOG_DATA'] ? ENV['TIMELOG_DATA'] : default_timelog
|
15
|
+
|
16
|
+
# puts "Logging task data to #{timelog}"
|
17
|
+
|
18
|
+
time_format = "%Y-%m-%d %H:%M"
|
19
|
+
|
20
|
+
if description == 'open'
|
21
|
+
`mate #{timelog}`
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
if description == 'stop'
|
26
|
+
entries = FasterCSV.read(timelog)
|
27
|
+
last_entry = entries.last
|
28
|
+
if last_entry[1]
|
29
|
+
puts "The last entry already has an end time and I'm not going to overwrite it."
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
last_entry[1] = Time.now.strftime(time_format)
|
33
|
+
File.open(timelog, 'w') do |file|
|
34
|
+
entries.each do |entry|
|
35
|
+
file.puts entry.to_csv
|
36
|
+
end
|
37
|
+
end
|
38
|
+
`mate #{timelog}`
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
def hms(duration)
|
43
|
+
hours = duration.to_i / 3600
|
44
|
+
seconds = duration.to_i % 3600
|
45
|
+
minutes = seconds / 60
|
46
|
+
seconds = seconds % 60
|
47
|
+
[hours, minutes, seconds].join(":")
|
48
|
+
end
|
49
|
+
|
50
|
+
def decimal_hours(seconds)
|
51
|
+
minutes = seconds / 60.0
|
52
|
+
minutes / 60.0
|
53
|
+
end
|
54
|
+
|
55
|
+
if description == 'report'
|
56
|
+
require 'time'
|
57
|
+
|
58
|
+
if project_filter = ARGV[1]
|
59
|
+
csv_data = File.read(timelog)
|
60
|
+
total_duration = 0
|
61
|
+
|
62
|
+
puts ''
|
63
|
+
header = 'Start'.ljust(23) + 'End'.ljust(23) + 'Seconds'.ljust(8) + 'H:M:S'.ljust(8) + "Hours".ljust(8) + 'Project'.ljust(12) + 'Description'
|
64
|
+
puts header
|
65
|
+
puts '=' * header.length
|
66
|
+
|
67
|
+
previous_day, day_duration = nil, 0
|
68
|
+
|
69
|
+
rows = FasterCSV.parse(csv_data)
|
70
|
+
matching_entries = rows.select do |row|
|
71
|
+
start_time, end_time, project_name, description = row
|
72
|
+
project_name == project_filter
|
73
|
+
end
|
74
|
+
|
75
|
+
matching_entries.each do |row|
|
76
|
+
start_time, end_time, project_name, description = row
|
77
|
+
|
78
|
+
duration = 0
|
79
|
+
if end_time
|
80
|
+
start_time = Time.parse(start_time)
|
81
|
+
end_time = Time.parse(end_time)
|
82
|
+
duration = (end_time - start_time)
|
83
|
+
end
|
84
|
+
|
85
|
+
if (row != matching_entries.first and previous_day != start_time.send(:to_date))
|
86
|
+
# We're on a new day
|
87
|
+
puts ' ' + '-'*7 + ' ' + '-'*7 + ' ' + '-'*7
|
88
|
+
print ' '
|
89
|
+
print day_duration.to_s.ljust(8)
|
90
|
+
print hms(day_duration).ljust(8)
|
91
|
+
print format("%0.2f", decimal_hours(day_duration)).ljust(8)
|
92
|
+
puts ''
|
93
|
+
puts ''
|
94
|
+
day_duration = 0
|
95
|
+
end
|
96
|
+
previous_day = start_time.send(:to_date)
|
97
|
+
day_duration += duration
|
98
|
+
|
99
|
+
total_duration += duration
|
100
|
+
row.insert(2, duration)
|
101
|
+
row.insert(3, hms(duration))
|
102
|
+
row.insert(4, decimal_hours(duration))
|
103
|
+
|
104
|
+
print Time.parse(row[0]).strftime("%a %d %b %Y %H:%M").ljust(23)
|
105
|
+
print Time.parse(row[1]).strftime("%a %d %b %Y %H:%M").ljust(23)
|
106
|
+
print row[2].to_s.ljust(8)
|
107
|
+
print row[3].to_s.ljust(8)
|
108
|
+
print format("%0.2f", row[4]).ljust(8)
|
109
|
+
print row[5].to_s.ljust(12)
|
110
|
+
puts row[6]
|
111
|
+
|
112
|
+
if row == matching_entries.last
|
113
|
+
# We're on the last day
|
114
|
+
print ' '
|
115
|
+
print day_duration.to_s.ljust(8)
|
116
|
+
print hms(day_duration).ljust(8)
|
117
|
+
print format("%0.2f", decimal_hours(day_duration)).ljust(8)
|
118
|
+
puts ''
|
119
|
+
puts ''
|
120
|
+
day_duration = 0
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
puts ''
|
125
|
+
puts 'Total duration (H:M:S): ' + hms(total_duration)
|
126
|
+
puts "Total duration (decimal hours): #{decimal_hours(total_duration)}"
|
127
|
+
puts ''
|
128
|
+
|
129
|
+
else
|
130
|
+
d = Hash.new(0)
|
131
|
+
|
132
|
+
csv_data = File.read(timelog)
|
133
|
+
FasterCSV.parse(csv_data).each do |row|
|
134
|
+
start_time, end_time, project_name, description = row
|
135
|
+
if end_time
|
136
|
+
start_time = Time.parse(start_time)
|
137
|
+
end_time = Time.parse(end_time)
|
138
|
+
duration = (end_time - start_time)
|
139
|
+
d[project_name] += duration
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
d.each_pair do |project_name, duration|
|
144
|
+
p [project_name, hms(duration)]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
exit
|
148
|
+
end
|
149
|
+
|
150
|
+
if description == 'resume'
|
151
|
+
TaskLogger.new(timelog).resume
|
152
|
+
exit
|
153
|
+
end
|
154
|
+
|
155
|
+
if description == 'list'
|
156
|
+
TaskLogger.new(timelog).list
|
157
|
+
exit
|
158
|
+
end
|
159
|
+
|
160
|
+
# Otherwise we're starting a new task
|
161
|
+
TaskLogger.new(timelog).start description
|
data/lib/task.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Task
|
4
|
+
|
5
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M"
|
6
|
+
DEFAULT_PROJECT = '<project-name>'
|
7
|
+
|
8
|
+
attr_reader :started_at, :finished_at
|
9
|
+
attr_accessor :project, :description
|
10
|
+
|
11
|
+
def self.from_array(task_data)
|
12
|
+
description = task_data[3]
|
13
|
+
started_at = Time.parse(task_data[0]) if task_data[0]
|
14
|
+
finished_at = Time.parse(task_data[1]) if task_data[1]
|
15
|
+
project = task_data[2]
|
16
|
+
new(description, started_at, finished_at, project)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(description, started_at = nil, finished_at = nil, project = nil)
|
20
|
+
@started_at = started_at || Time.now
|
21
|
+
@finished_at = finished_at
|
22
|
+
@project = project || DEFAULT_PROJECT
|
23
|
+
@description = description
|
24
|
+
end
|
25
|
+
|
26
|
+
def restart!
|
27
|
+
@started_at = Time.now
|
28
|
+
@finished_at = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def finish!
|
32
|
+
@finished_at = Time.now unless finished?
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
[formatted_started_at, formatted_finished_at, @project, @description]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def finished?
|
42
|
+
@finished_at
|
43
|
+
end
|
44
|
+
|
45
|
+
def formatted_started_at
|
46
|
+
@started_at.strftime(TIME_FORMAT) if @started_at
|
47
|
+
end
|
48
|
+
|
49
|
+
def formatted_finished_at
|
50
|
+
@finished_at.strftime(TIME_FORMAT) if @finished_at
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/task_logger.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'task'
|
2
|
+
|
3
|
+
class TaskLogger
|
4
|
+
|
5
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M"
|
6
|
+
|
7
|
+
def initialize(tasks_path)
|
8
|
+
@tasks_path = tasks_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(task_description)
|
12
|
+
task = Task.new(task_description)
|
13
|
+
if last_task_data = tasks.pop
|
14
|
+
last_task = Task.from_array(last_task_data)
|
15
|
+
last_task.finish!
|
16
|
+
tasks << last_task.to_a
|
17
|
+
end
|
18
|
+
tasks << task.to_a
|
19
|
+
write_tasks
|
20
|
+
end
|
21
|
+
|
22
|
+
def resume
|
23
|
+
exit_if_missing_data
|
24
|
+
|
25
|
+
last_task_data = tasks.pop
|
26
|
+
last_task = Task.from_array(last_task_data)
|
27
|
+
last_task.finish!
|
28
|
+
|
29
|
+
penultimate_task_data = tasks.last
|
30
|
+
penultimate_task = Task.from_array(penultimate_task_data)
|
31
|
+
penultimate_task.restart!
|
32
|
+
|
33
|
+
tasks << last_task.to_a
|
34
|
+
tasks << penultimate_task.to_a
|
35
|
+
|
36
|
+
write_tasks
|
37
|
+
end
|
38
|
+
|
39
|
+
def list
|
40
|
+
exit_if_missing_data
|
41
|
+
|
42
|
+
tasks[-5, 5].each do |task|
|
43
|
+
puts task.to_csv
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def tasks
|
50
|
+
@tasks ||= File.exists?(@tasks_path) ? FasterCSV.read(@tasks_path) : []
|
51
|
+
end
|
52
|
+
|
53
|
+
def exit_if_missing_data
|
54
|
+
if tasks.empty?
|
55
|
+
puts "Error. The timelog data cannot be found."
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def write_tasks
|
61
|
+
File.open(@tasks_path, 'w') do |file|
|
62
|
+
tasks.each do |task|
|
63
|
+
file.puts task.to_csv
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tasklogger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Roos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-04-26 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: fastercsv
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mocha
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: chris@seagul.co.uk
|
37
|
+
executables:
|
38
|
+
- tasklogger
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- Rakefile
|
45
|
+
- lib/task.rb
|
46
|
+
- lib/task_logger.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://chrisroos.co.uk
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.5
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: A simple task/time logging utility
|
75
|
+
test_files: []
|
76
|
+
|