time_tap 0.2.0

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