time_tap 0.2.0 → 0.4.0.pre

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