time_tap 0.2.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.
@@ -0,0 +1,24 @@
1
+ # Define Process::daemon without waiting for active support to be loaded
2
+ # so that the "timetap" command exits immediatly.
3
+ # from active_support-3
4
+ def Process.daemon(nochdir = nil, noclose = nil)
5
+ exit if fork # Parent exits, child continues.
6
+ Process.setsid # Become session leader.
7
+ exit if fork # Zap session leader. See [1].
8
+
9
+ unless nochdir
10
+ Dir.chdir "/" # Release old working directory.
11
+ end
12
+
13
+ File.umask 0000 # Ensure sensible umask. Adjust as needed.
14
+
15
+ unless noclose
16
+ STDIN.reopen '/dev/null' # Free file descriptors and
17
+ STDOUT.reopen '/dev/null', "a" # point them somewhere sensible.
18
+ STDERR.reopen '/dev/null', 'a'
19
+ end
20
+
21
+ trap("TERM") { exit }
22
+
23
+ return 0
24
+ end
@@ -0,0 +1,23 @@
1
+ module TimeTap
2
+ module Editors
3
+ class EditorError < StandardError
4
+ end
5
+
6
+ class TextMate
7
+ require 'appscript'
8
+ include Appscript
9
+
10
+ def is_running?
11
+ not(`ps -ax -o comm|grep TextMate`.chomp.strip.empty?)
12
+ end
13
+
14
+ def current_path
15
+ mate = app('TextMate')
16
+ document = mate.document.get
17
+ raise(EditorError) if document.blank?
18
+ path = document.first.path.get rescue nil
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,149 @@
1
+ module TimeTap
2
+ class Project
3
+ attr_reader :pinches
4
+ class << self
5
+ attr_accessor :pause_limit
6
+ def pause_limit
7
+ @pause_limit ||= 30.minutes
8
+ end
9
+
10
+ def history_file
11
+ "#{TimeTap.config[:root]}/.tap_history"
12
+ end
13
+
14
+ def load_file path
15
+
16
+ File.open(File.expand_path(path), 'r', RUBY19 ? {:external_encoding => 'utf-8'} : nil) do |file|
17
+ file.each_line do |line|
18
+ time, path = line.split(": ")
19
+
20
+ project = self[path]
21
+ project << time.to_i if project
22
+ end
23
+ end
24
+ end
25
+
26
+ def projects
27
+ @projects ||= HashWithIndifferentAccess.new
28
+ end
29
+
30
+ def all
31
+ load_file(history_file) if projects.empty?
32
+ projects.values
33
+ end
34
+
35
+ def find name
36
+ load_file(history_file) if projects.empty?
37
+ projects[name.underscore.downcase]
38
+ end
39
+
40
+ def [] path
41
+ path = File.expand_path(path)
42
+ how_nested = 1
43
+
44
+ regex_suffix = "([^/]+)"
45
+ if TimeTap.config["nested_project_layers"] && TimeTap.config["nested_project_layers"].to_i > 0
46
+ how_nested = TimeTap.config["nested_project_layers"].to_i
47
+
48
+ # nested project layers works "how many folders inside your code folder
49
+ # do you keep projects.
50
+ #
51
+ # For example, if your directory structure looks like:
52
+ # ~/Code/
53
+ # Clients/
54
+ # AcmeCorp/
55
+ # website/
56
+ # intranet
57
+ # BetaCorp/
58
+ # skunkworks/
59
+ # OpenSource/
60
+ # project_one/
61
+ # timetap/
62
+ #
63
+ # A nested_project_layers setting of 2 would mean we track "AcmeCorp", "BetaCorp", and everything
64
+ # under OpenSource, as their own projects
65
+ regex_suffix = [regex_suffix] * how_nested
66
+ regex_suffix = regex_suffix.join("/")
67
+ end
68
+
69
+ res = path.scan(%r{(#{TimeTap.config[:code] || "Code"})/#{regex_suffix}}).flatten
70
+ mid_path = res[0] # not in a MatchObj group any more, so it's 0 based
71
+ name = res[how_nested]
72
+ mid_path, name = path.scan(%r{#{File.expand_path("~")}/([^/]+)/([^/]+)}).flatten if name.nil?
73
+ if name
74
+ name.chomp!
75
+ key = name.underscore.downcase
76
+ if projects[key].nil?
77
+ project = Project.new mid_path, name
78
+ projects[key] = project
79
+ end
80
+ projects[key]
81
+ else
82
+ nil
83
+ end
84
+ end
85
+ end
86
+
87
+ attr_reader :name, :path
88
+ def initialize mid_path, name
89
+ @name = name
90
+ @path = File.expand_path("~/#{mid_path}/#{name}/")
91
+ @pinches = []
92
+ end
93
+
94
+ class Pinch
95
+ attr_accessor :start_time, :end_time
96
+ def initialize start_time
97
+ @end_time = @start_time = start_time
98
+ end
99
+
100
+ def duration
101
+ end_time ? end_time - start_time : 30.seconds
102
+ end
103
+
104
+ def humanized_duration
105
+ seconds = duration
106
+
107
+ hours = mins = 0
108
+ if seconds >= 60 then
109
+ mins = (seconds / 60).to_i
110
+ seconds = (seconds % 60 ).to_i
111
+
112
+ if mins >= 60 then
113
+ hours = (mins / 60).to_i
114
+ mins = (mins % 60).to_i
115
+ end
116
+ end
117
+ "#{hours}h #{mins}m #{seconds}s"
118
+ end
119
+ end
120
+
121
+ def << time
122
+ time = Time.at time
123
+ last_pinch = pinches.last
124
+ if pinches.empty?
125
+ pinches << Pinch.new(time)
126
+ else
127
+ last_time = last_pinch.end_time
128
+ return unless time > last_time
129
+
130
+ if (time - last_time) < self.class.pause_limit
131
+ last_pinch.end_time = time
132
+ else
133
+ pinches << Pinch.new(time)
134
+ end
135
+ end
136
+ end
137
+
138
+ def work_time
139
+ pinches.map(&:duration).inject(0.seconds, &:+)
140
+ end
141
+
142
+
143
+ def days
144
+ pinches.group_by do |pinch|
145
+ pinch.start_time.to_date
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,81 @@
1
+ require 'sinatra/base'
2
+ require 'haml'
3
+ require 'sass'
4
+ require 'action_view'
5
+
6
+ module TimeTap
7
+ class Server < Sinatra::Application
8
+
9
+ include ActionView::Helpers::DateHelper
10
+ set :haml, { :format => :html5,
11
+ :attr_wrapper => '"' ,
12
+ :encoding => RUBY19 ? 'UTF-8' : nil}
13
+ set :root, File.dirname(__FILE__)
14
+ set :views, Proc.new { File.expand_path("../views", __FILE__) }
15
+
16
+
17
+ before do
18
+ content_type "text/html", :charset => "utf-8"
19
+ Project.load_file('~/.tap_history')
20
+ end
21
+
22
+
23
+
24
+
25
+
26
+ get '/' do
27
+ sort = (params[:sort] || :last).to_sym
28
+
29
+ @projects = Project.all.sort_by do |project|
30
+ case sort
31
+ when :name; project.name
32
+ when :last; -project.pinches.last.end_time.to_i
33
+ when :elapsed; -project.work_time
34
+ end
35
+ end.select{|p| p.work_time > 30.minutes}
36
+
37
+ haml :index
38
+ end
39
+
40
+ get '/project/:name' do
41
+ @project = Project.find(params[:name])
42
+ redirect '/' if @project.nil?
43
+
44
+ haml :project
45
+ end
46
+
47
+
48
+ get "/project/:name/:day" do
49
+ @project = Project.find(params[:name])
50
+ redirect '/' if @project.nil?
51
+
52
+ days = @project.days.to_a.sort_by(&:first).reverse
53
+ @current_day = days[ params[:day].to_i ][0] #pick an arbitrary date in the pinches
54
+
55
+ @pinches = @project.days[@current_day]
56
+ haml :project_day
57
+ end
58
+
59
+
60
+ get "/stylesheet.css" do
61
+ content_type "text/css", :charset => "utf-8"
62
+ sass :stylesheet
63
+ end
64
+
65
+ get '/mate' do
66
+ tm_project = File.expand_path("#{TimeTap.config[:textmate][:projects] || 'Development/Current Projects'}/#{File.basename(params[:path])}.tmproj")
67
+ if File.exist?(tm_project)
68
+ `open "#{tm_project}"`
69
+ else
70
+ `mate "#{params[:path]}"`
71
+ end
72
+ redirect '/'
73
+ end
74
+
75
+ get '/stop' do
76
+ $stop = true
77
+ Process.exit
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,68 @@
1
+ namespace :timetap do
2
+
3
+ plist_name = "com.eliaesocietas.TimeTap.plist"
4
+ home = File.expand_path("~")
5
+ ruby = ENV['TAP_RUBY'] || "#{home}/.rvm/bin/ruby-1.9.2-p0@global"
6
+ plist_path = File.expand_path("#{home}/Library/LaunchAgents/#{plist_name}")
7
+ launcher = File.expand_path('../../../bin/timetap', __FILE__)
8
+ include_dir = '-I'+File.expand_path('../../../lib', __FILE__)
9
+
10
+ desc "Add a plist for OSX's launchd and have *TimeTap* launched automatically at login."
11
+ task :launcher do
12
+ puts "\nCreating launchd plist in\n #{plist_path}"
13
+
14
+ File.open(plist_path, 'w') do |file|
15
+ file << <<-PLIST
16
+ <?xml version="1.0" encoding="UTF-8"?>
17
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
18
+ <plist version="1.0">
19
+ <dict>
20
+ <key>Label</key>
21
+ <string>com.eliaesocietas.TimeTap</string>
22
+
23
+ <key>Program</key>
24
+ <string>#{ruby}</string>
25
+
26
+ <key>ProgramArguments</key>
27
+ <array>
28
+ <string>#{ruby}</string>
29
+ <string>#{include_dir}</string>
30
+ <string>#{launcher}</string>
31
+ <string>-f</string>
32
+ </array>
33
+
34
+ <key>OnDemand</key>
35
+ <false/>
36
+
37
+ <key>RunAtLoad</key>
38
+ <true/>
39
+ </dict>
40
+ </plist>
41
+ PLIST
42
+
43
+ puts "\nCreated. Now run:\n launchctl load ~/Library/LaunchAgents\n\n"
44
+ end
45
+ end
46
+
47
+ desc "Restarts the daemon."
48
+ task :restart do
49
+ command = "launchctl unload #{plist_path}; launchctl load #{plist_path}"
50
+ puts command
51
+ exec command
52
+ end
53
+
54
+ desc "Stops the daemon."
55
+ task :stop do
56
+ command = "launchctl unload #{plist_path}"
57
+ puts command
58
+ exec command
59
+ end
60
+
61
+ desc "Starts the daemon."
62
+ task :start do
63
+ command = "launchctl load #{plist_path}"
64
+ puts command
65
+ exec command
66
+ end
67
+
68
+ end
@@ -0,0 +1,38 @@
1
+ %table
2
+ %tr.header
3
+ %th
4
+ %a{:href => '/?'+params.merge('sort' => :name).map{|pair| pair.join('=')}.join(';')} Project
5
+ %th
6
+ %a{:href => '/?'+params.merge('sort' => :elapsed).map{|pair| pair.join('=')}.join(';')} Work Time
7
+ %th
8
+ %a{:href => '/?'+params.merge('sort' => :last).map{|pair| pair.join('=')}.join(';')} Last Access
9
+
10
+ - @projects.each do |project|
11
+ - elapsed_time = project.work_time.to_i
12
+ - hours = elapsed_time/1.hour
13
+ %tr
14
+ %th{:title => project.name}
15
+ %a.project{:href => "/project/#{project.name}", :title => "#{hours.to_i}h \n eur#{hours*20}@eur20/h \n eur#{hours*25}@eur25/h \n eur#{hours*30}@eur30/h"}= project.name ? project.name.underscore.humanize : '<i>other</i>'
16
+ %td
17
+ - if elapsed_time < 1.hours
18
+ = elapsed_minutes = elapsed_time / 1.minute
19
+ = elapsed_minutes == 1 ? 'minute' : 'minutes'
20
+ - elsif elapsed_time < 8.hours
21
+ = elapsed_hours = elapsed_time / 1.hour
22
+ = elapsed_hours == 1 ? 'hour' : 'hours'
23
+ - else
24
+ = elapsed_days = elapsed_time / 8.hours
25
+ man
26
+ = elapsed_days == 1 ? 'day' : 'days'
27
+ - if (elapsed_hours = (elapsed_time % 8.hours) / 1.hour) >= 1
28
+ and
29
+ = elapsed_hours
30
+ = elapsed_hours == 1 ? 'hour' : 'hours'
31
+
32
+
33
+ %td
34
+ %i #{time_ago_in_words Time.at(project.pinches.last.end_time)} ago
35
+ %td
36
+ %a.tip{:href => "/mate?path=#{project.path}"} mate
37
+
38
+ .clear
@@ -0,0 +1,14 @@
1
+ !!! Strict
2
+ %html(html_attrs)
3
+ %head
4
+ %title TimeTap Log
5
+ %link{ :rel => 'stylesheet', :media => 'screen', :type => "text/css", :href => "/stylesheet.css?#{File.stat(__FILE__).mtime.to_i}" }
6
+ %body
7
+ %h1
8
+ TimeTap Log
9
+ %span.tip= Time.now.to_s
10
+ #content
11
+ #page_contents= yield
12
+
13
+ %a.tip{:href => "/stop"} stop
14
+
@@ -0,0 +1,24 @@
1
+ - this_year = Time.now.year
2
+
3
+ .back
4
+ %a{:href => '/'} &laquo; Home
5
+ .clear
6
+ %h2
7
+ Project:
8
+ %b= @project.name.humanize
9
+ %span.tip (#{@project.work_time.to_i / 1.hours} hours)
10
+
11
+ %p.tip #{@project.pinches.size} pinches, #{time_ago_in_words Time.now - (@project.work_time / @project.days.size / 1.hour).hours} per day in #{@project.days.size} days of work
12
+
13
+ %table.days
14
+ - current = 0
15
+ - @project.days.to_a.sort_by(&:first).reverse.each do |day, pinches|
16
+ - time = pinches.map(&:duration).inject(0.seconds, &:+)
17
+ - if time >= 1.minute
18
+ %tr.day
19
+ %td
20
+ %a.day{ :href=> "/project/#{@project.name}/#{current}" } #{( this_year != day.year ? day.strftime("%Y %b %d, %a") : day.strftime("%b %d, %a") )}
21
+ %td
22
+ %span.work{:style => "background-color:green;color:#ccc;overflow:visible;height:1em"}= '&nbsp;' * (time / 5.minutes.to_f)
23
+ %span.tip= time_ago_in_words Time.now - time
24
+ - current = current + 1
@@ -0,0 +1,24 @@
1
+ - this_year = Time.now.year
2
+
3
+ .back
4
+ %a{:href => '/'} &laquo; Home
5
+ .clear
6
+ %h2
7
+ Project:
8
+ %b= @project.name.humanize
9
+ %span.tip (#{"%.2f" % (@project.work_time.to_i / 1.hours.to_f).to_s} hours)
10
+ %h2
11
+ Detailed View For:
12
+ %b=( this_year != @current_day.year ? @current_day.strftime("%Y %b %d, %a") : @current_day.strftime("%b %d, %a") )
13
+
14
+ %table.pinches
15
+ %tr
16
+ %th Start Time
17
+ %th End Time
18
+ %th Duration
19
+ - @pinches.each do |current_pinch|
20
+ %tr
21
+ %td= current_pinch.start_time.strftime("%H:%M:%S")
22
+ %td= current_pinch.end_time.strftime("%H:%M:%S")
23
+ %td= current_pinch.humanized_duration
24
+