time_tap 0.2.0 → 0.4.0.pre

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 (42) hide show
  1. data/.gitignore +5 -37
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +3 -21
  5. data/README.md +104 -41
  6. data/Rakefile +1 -30
  7. data/bin/timetap +25 -26
  8. data/lib/time_tap.rb +126 -52
  9. data/lib/time_tap/backend.rb +9 -0
  10. data/lib/time_tap/backend/file_system.rb +36 -0
  11. data/lib/time_tap/config.yml.example +32 -0
  12. data/lib/time_tap/editor.rb +9 -0
  13. data/lib/time_tap/editor/sublime_text2.rb +15 -0
  14. data/lib/time_tap/editor/text_mate.rb +22 -0
  15. data/lib/time_tap/editor/text_mate2.rb +15 -0
  16. data/lib/time_tap/editor/xcode.rb +24 -0
  17. data/lib/time_tap/project.rb +136 -116
  18. data/lib/time_tap/server.rb +20 -20
  19. data/lib/time_tap/version.rb +3 -0
  20. data/lib/time_tap/views/project.haml +5 -3
  21. data/lib/time_tap/views/project_day.haml +1 -0
  22. data/lib/time_tap/watcher.rb +46 -43
  23. data/log/.git-keep +1 -0
  24. data/spec/lib/time_tap/project_spec.rb +12 -0
  25. data/spec/spec_helper.rb +3 -1
  26. data/spec/time_tap/backend_spec.rb +12 -0
  27. data/spec/time_tap/project_spec.rb +13 -0
  28. data/spec/time_tap/watcher_spec.rb +14 -0
  29. data/spec/time_tap_spec.rb +6 -4
  30. data/time_tap.gemspec +26 -108
  31. data/vendor/SublimeText2/.gitignore +1 -0
  32. data/vendor/SublimeText2/README.md +27 -0
  33. data/vendor/SublimeText2/TimeTap.py +10 -0
  34. data/vendor/TextMate2/TimeTap.tmbundle/Commands/Record current file.tmCommand +33 -0
  35. data/vendor/TextMate2/TimeTap.tmbundle/info.plist +16 -0
  36. data/vendor/TimeTap.tmbundle/Commands/Record current file.tmCommand +36 -0
  37. data/vendor/TimeTap.tmbundle/info.plist +18 -0
  38. metadata +161 -260
  39. data/Gemfile.lock +0 -48
  40. data/VERSION +0 -1
  41. data/config.yaml +0 -8
  42. data/lib/time_tap/editors.rb +0 -23
@@ -0,0 +1,9 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module TimeTap::Backend
4
+ def self.load backend, options = {}
5
+ require "time_tap/backend/#{backend}"
6
+ klass = const_get(backend.to_s.classify)
7
+ klass.new options
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ class TimeTap::Backend::FileSystem
2
+ def initialize options = {}
3
+ raise 'missing :file_name option' unless options[:file_name]
4
+ @file_name = options[:file_name]
5
+ end
6
+
7
+ def file_name
8
+ File.expand_path(@file_name)
9
+ end
10
+
11
+ def append_to_file content
12
+ File.open(file_name, 'a').tap do |f|
13
+ f.sync = true
14
+ f << content
15
+ end
16
+ end
17
+
18
+ def ruby19?
19
+ RUBY_VERSION >= '1.9'
20
+ end
21
+
22
+ def each_entry
23
+ return [] unless File.exists? file_name
24
+
25
+ File.open(file_name, 'r', ruby19? ? {:external_encoding => 'utf-8'} : nil) do |file|
26
+ file.each_line do |line|
27
+ time, path = line.split(": ")
28
+ yield time.to_i, path
29
+ end
30
+ end
31
+ end
32
+
33
+ def register time, path
34
+ append_to_file "#{time.to_i}: #{path}\n"
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # This is where the "db" will be saved
2
+ root: "~"
3
+
4
+ # See the README about nested projects
5
+ nested_project_layers: 1
6
+
7
+ # The port on localhost for the web interface
8
+ port: 1111
9
+
10
+ # Set your editor:
11
+ #
12
+ # editor: text_mate
13
+ # editor: sublime_text2
14
+ editor: text_mate2
15
+
16
+ # Use a custom Ruby
17
+ #
18
+ # RVM Example:
19
+ # ruby: /Users/elia/.rvm/bin/ruby-1.9.3-p286
20
+
21
+ # Replace the folders timetap will look into:
22
+ # Example:
23
+ #
24
+ # code_folders:
25
+ # - "/Volumes/Oliver/Code"
26
+ # - "~/Code/Mikamai"
27
+ # - "~/Code"
28
+ # - "~"
29
+ #
30
+
31
+
32
+
@@ -0,0 +1,9 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module TimeTap::Editor
4
+ def self.load backend, options = {}
5
+ require "time_tap/editor/#{backend}"
6
+ klass = const_get(backend.to_s.classify)
7
+ klass.new options
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ class TimeTap::Editor::SublimeText2
2
+ def initialize options = {}
3
+ end
4
+
5
+ def current_path
6
+ container_file = File.expand_path('~/.timetap.sbl2.current_file')
7
+ file = File.read(container_file).strip if File.exist? container_file
8
+
9
+ if File.exist? file
10
+ file
11
+ else
12
+ nil
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'appscript'
2
+
3
+ class TimeTap::Editor::TextMate
4
+ include Appscript
5
+
6
+ def initialize options = {}
7
+ end
8
+
9
+ def is_running?
10
+ not(`ps -ax -o comm|grep TextMate`.chomp.strip.empty?)
11
+ false
12
+ end
13
+
14
+ def current_path
15
+ if is_running?
16
+ mate = app('TextMate')
17
+ document = mate.document.get
18
+ return nil if document.blank?
19
+ path = document.first.path.get rescue nil
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ class TimeTap::Editor::TextMate2
2
+ def initialize options = {}
3
+ end
4
+
5
+ def current_path
6
+ container_file = File.expand_path('~/.timetap.tm2.current_file')
7
+ file = File.read(container_file).strip if File.exist? container_file
8
+
9
+ if File.exist? file
10
+ file
11
+ else
12
+ nil
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ require 'appscript'
2
+
3
+ class TimeTap::Editor::Xcode
4
+ include Appscript
5
+
6
+ def initialize options = {}
7
+ end
8
+
9
+ def is_running?
10
+ # Cannot use app('Xcode') because it fails when multiple Xcode versions are installed
11
+ !app('System Events').processes[its.name.eq('Xcode')].get.empty?
12
+ end
13
+
14
+ def current_path
15
+ if is_running?
16
+ # although multiple versions may be installed, tipically they will not be running simultaneously
17
+ pid = app('System Events').processes[its.name.eq('Xcode')].first.unix_id.get
18
+ xcode = app.by_pid(pid)
19
+ document = xcode.document.get
20
+ return nil if document.blank?
21
+ path = document.last.path.get rescue nil
22
+ end
23
+ end
24
+ end
@@ -1,149 +1,169 @@
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
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/core_ext/numeric/time'
13
3
 
14
- def load_file path
4
+ class TimeTap::Project
15
5
 
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(": ")
6
+ # CONFIG
19
7
 
20
- project = self[path]
21
- project << time.to_i if project
22
- end
8
+ class << self
9
+ attr_accessor :pause_limit
10
+ def load_projects
11
+ @loading ||= Thread.new do
12
+ backend.each_entry do |time, path|
13
+ register time, path
23
14
  end
24
15
  end
16
+ end
25
17
 
26
- def projects
27
- @projects ||= HashWithIndifferentAccess.new
28
- end
18
+ def projects
19
+ load_projects
20
+ @projects
21
+ end
22
+ attr_reader :backend
29
23
 
30
- def all
31
- load_file(history_file) if projects.empty?
32
- projects.values
33
- end
24
+ def reload!
25
+ @pause_limit = 30.minutes
26
+ @projects = {}.with_indifferent_access
27
+ @backend = TimeTap.backend
28
+ end
29
+ end
30
+ reload!
34
31
 
35
- def find name
36
- load_file(history_file) if projects.empty?
37
- projects[name.underscore.downcase]
38
- end
32
+ def logger
33
+ self.class.logger
34
+ end
39
35
 
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
36
 
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
37
+
38
+
39
+ class << self
40
+ def logger
41
+ TimeTap.logger
85
42
  end
86
43
 
87
- attr_reader :name, :path
88
- def initialize mid_path, name
89
- @name = name
90
- @path = File.expand_path("~/#{mid_path}/#{name}/")
91
- @pinches = []
44
+ def all
45
+ projects.values
92
46
  end
93
47
 
94
- class Pinch
95
- attr_accessor :start_time, :end_time
96
- def initialize start_time
97
- @end_time = @start_time = start_time
48
+ def register time, path
49
+ project = self[path]
50
+ if project
51
+ project << time
52
+ logger.info "[TimeTap::Project] added #{time} to project #{project.name} (#{project})"
53
+ else
54
+ logger.info "[TimeTap::Project] skipping #{time}, no project"
98
55
  end
56
+ end
99
57
 
100
- def duration
101
- end_time ? end_time - start_time : 30.seconds
102
- end
58
+ def find name
59
+ projects[name.underscore.downcase]
60
+ end
61
+
62
+ def [] path
63
+ return if path.nil? or path.empty?
64
+ path = File.expand_path(path)
65
+ how_nested = 1
103
66
 
104
- def humanized_duration
105
- seconds = duration
67
+ regex_suffix = "([^/]+)"
106
68
 
107
- hours = mins = 0
108
- if seconds >= 60 then
109
- mins = (seconds / 60).to_i
110
- seconds = (seconds % 60 ).to_i
69
+ folders = TimeTap.config[:code_folders].map do |folder|
70
+ folder = File.expand_path(folder)
71
+ folder = Dir[folder]
72
+ end.flatten
111
73
 
112
- if mins >= 60 then
113
- hours = (mins / 60).to_i
114
- mins = (mins % 60).to_i
115
- end
74
+ folders_regex = folders.map{|f| Regexp.escape f}.join('|')
75
+ res = path.scan(%r{(#{folders_regex})/#{regex_suffix}}).flatten
76
+
77
+ # res = path.scan(%r{(#{TimeTap.config[:code] || "Code"})/#{regex_suffix}}).flatten
78
+ mid_path = res[0] # not in a MatchObj group any more, so it's 0 based
79
+ name = res[how_nested]
80
+ mid_path, name = path.scan(%r{#{File.expand_path("~")}/([^/]+)/([^/]+)}).flatten if name.nil?
81
+
82
+ if name
83
+ name.chomp!
84
+ key = name.underscore.downcase
85
+ if projects[key].nil?
86
+ project = new mid_path, name
87
+ projects[key] = project
116
88
  end
117
- "#{hours}h #{mins}m #{seconds}s"
89
+ projects[key]
90
+ else
91
+ nil
118
92
  end
119
93
  end
94
+ end
120
95
 
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
96
 
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
97
+
98
+
99
+
100
+ def initialize mid_path, name
101
+ @name = name
102
+ @path = File.expand_path("~/#{mid_path}/#{name}/")
103
+ @pinches = []
104
+ end
105
+
106
+
107
+
108
+
109
+ # ATTRIBUTES
110
+
111
+ attr_reader :name, :path, :pinches
112
+
113
+ def work_time
114
+ pinches.sum(&:duration)
115
+ end
116
+
117
+ def days
118
+ pinches.group_by do |pinch|
119
+ pinch.start_time.to_date
136
120
  end
121
+ end
137
122
 
138
- def work_time
139
- pinches.map(&:duration).inject(0.seconds, &:+)
123
+
124
+
125
+ class Pinch
126
+ attr_accessor :start_time, :end_time
127
+ def initialize start_time
128
+ @end_time = @start_time = start_time
129
+ end
130
+
131
+ def duration
132
+ end_time ? end_time - start_time : 30.seconds
140
133
  end
141
134
 
135
+ def humanized_duration
136
+ seconds = duration
142
137
 
143
- def days
144
- pinches.group_by do |pinch|
145
- pinch.start_time.to_date
138
+ hours = mins = 0
139
+ if seconds >= 60 then
140
+ mins = (seconds / 60).to_i
141
+ seconds = (seconds % 60 ).to_i
142
+
143
+ if mins >= 60 then
144
+ hours = (mins / 60).to_i
145
+ mins = (mins % 60).to_i
146
+ end
146
147
  end
148
+ "#{hours}h #{mins}m #{seconds}s"
147
149
  end
148
150
  end
151
+
152
+ def << time
153
+ time = Time.at time
154
+ last_pinch = pinches.last
155
+ if pinches.empty?
156
+ pinches << Pinch.new(time)
157
+ else
158
+ last_time = last_pinch.end_time
159
+ return unless time > last_time
160
+
161
+ if (time - last_time) < self.class.pause_limit
162
+ last_pinch.end_time = time
163
+ else
164
+ pinches << Pinch.new(time)
165
+ end
166
+ end
167
+ end
168
+
149
169
  end