punched 0.1.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.
- checksums.yaml +4 -0
- data/.gitignore +2 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -0
- data/LICENSE +674 -0
- data/README.md +138 -0
- data/bin/punched +61 -0
- data/lib/punchcard.rb +316 -0
- data/punched.gemspec +19 -0
- data/spec/punchcard_spec.rb +156 -0
- metadata +54 -0
data/README.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# Punchcard
|
2
|
+
## Minimal time tracking tool for cli
|
3
|
+
|
4
|
+
[](https://travis-ci.org/pstaender/punchcard)
|
5
|
+
|
6
|
+
### Requirements
|
7
|
+
|
8
|
+
* ruby 2+
|
9
|
+
* unix bash (maybe windows works as well?!?)
|
10
|
+
|
11
|
+
### Install
|
12
|
+
|
13
|
+
```sh
|
14
|
+
$ gem install punched
|
15
|
+
```
|
16
|
+
|
17
|
+
Or choose your preferred filename and location:
|
18
|
+
|
19
|
+
```sh
|
20
|
+
$ echo '#!/usr/bin/env ruby' > /usr/local/bin/punched
|
21
|
+
$ curl https://raw.githubusercontent.com/pstaender/punchcard/master/lib/punchcard.rb >> /usr/local/bin/punched
|
22
|
+
$ curl https://raw.githubusercontent.com/pstaender/punchcard/master/bin/punched >> /usr/local/bin/punched
|
23
|
+
$ chmod +x /usr/local/bin/punched
|
24
|
+
```
|
25
|
+
|
26
|
+
### Usage
|
27
|
+
|
28
|
+
#### Start Project
|
29
|
+
|
30
|
+
```sh
|
31
|
+
$ punched start "Punchcard (programming)"
|
32
|
+
```
|
33
|
+
|
34
|
+
#### Wildcard
|
35
|
+
|
36
|
+
Save keystrokes by using wildcard. The last active project, which matches the pattern (case insensitive) will be selected:
|
37
|
+
|
38
|
+
```sh
|
39
|
+
$ punched start "Punch*"
|
40
|
+
```
|
41
|
+
|
42
|
+
#### Stop Project
|
43
|
+
|
44
|
+
```sh
|
45
|
+
$ punched stop "Punch*"
|
46
|
+
```
|
47
|
+
|
48
|
+
#### Toggle
|
49
|
+
|
50
|
+
Toggle between start and stop:
|
51
|
+
|
52
|
+
```sh
|
53
|
+
$ punched toggle "Punch*"
|
54
|
+
```
|
55
|
+
|
56
|
+
#### Status
|
57
|
+
|
58
|
+
```sh
|
59
|
+
$ punched status "Punch*"
|
60
|
+
|
61
|
+
Punchcard (programming)
|
62
|
+
01:10:09
|
63
|
+
```
|
64
|
+
|
65
|
+
#### List details
|
66
|
+
|
67
|
+
```sh
|
68
|
+
$ punched details "Punch*"
|
69
|
+
|
70
|
+
Punchcard (programming) (stopped)
|
71
|
+
|
72
|
+
00:00:08 2017-05-07 08:16:06 - 2017-05-07 08:16:14
|
73
|
+
00:04:35 2017-05-07 08:22:02 - 2017-05-07 08:26:37
|
74
|
+
...
|
75
|
+
========
|
76
|
+
01:10:04 (total)
|
77
|
+
```
|
78
|
+
|
79
|
+
#### Set Hourly Rate
|
80
|
+
|
81
|
+
```sh
|
82
|
+
$ punched set "Punch*" hourlyRate 250€
|
83
|
+
```
|
84
|
+
|
85
|
+
#### Total time in seconds
|
86
|
+
|
87
|
+
```sh
|
88
|
+
$ punched total "Punch*"
|
89
|
+
```
|
90
|
+
|
91
|
+
#### Rename and delete Project
|
92
|
+
|
93
|
+
```sh
|
94
|
+
$ punched rename "Old Title" "New Title"
|
95
|
+
```
|
96
|
+
|
97
|
+
```sh
|
98
|
+
$ punched remove "Punchcard (programming)"
|
99
|
+
```
|
100
|
+
|
101
|
+
|
102
|
+
#### List all projects with total time in CSV format
|
103
|
+
|
104
|
+
```sh
|
105
|
+
$ punched all
|
106
|
+
|
107
|
+
"project","status","last active on","total duration","hourly rate","earnings"
|
108
|
+
"Website","stopped","2017-05-07 15:50:00","04:06:00","250.0 €","1025.00 €"
|
109
|
+
"Punchcard (programming)","stopped","2017-07-11 12:47:42","01:10:04","",""
|
110
|
+
```
|
111
|
+
|
112
|
+
You can use `all` with any other action as well, e.g. `punched all stop` to stop all running projects.
|
113
|
+
|
114
|
+
Hint: Use your favorite output formatter to get a nicer project summary of your choice; e.g. with [csv2md](https://www.npmjs.com/package/csv2md):
|
115
|
+
|
116
|
+
```sh
|
117
|
+
$ punched all | csv2md --pretty
|
118
|
+
|
119
|
+
| project | status | last active on | total duration | hourly rate | earnings |
|
120
|
+
|---------------------------|---------|---------------------|----------------|-------------|----------|
|
121
|
+
| Website | stopped | 2017-05-07 15:50:00 | 04:06:00 | 250.0 € | 1025.0 € |
|
122
|
+
| Punchcard (programming) | stopped | 2017-05-07 12:47:42 | 01:10:04 | | |
|
123
|
+
```
|
124
|
+
|
125
|
+
### Store projects files in a custom folder and sync them between computers
|
126
|
+
|
127
|
+
By default, PunchCard will store the data in `~/.punchcard/`. Define your custom destination with:
|
128
|
+
|
129
|
+
```sh
|
130
|
+
export PUNCHCARD_DIR=~/Nextcloud/punchcard
|
131
|
+
```
|
132
|
+
|
133
|
+
|
134
|
+
### Tests
|
135
|
+
|
136
|
+
```sh
|
137
|
+
$ bundle exec rspec
|
138
|
+
```
|
data/bin/punched
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'punchcard'
|
4
|
+
|
5
|
+
#
|
6
|
+
# CLI Wrapper
|
7
|
+
#
|
8
|
+
|
9
|
+
def available_actions
|
10
|
+
PunchCard.new(nil).public_methods(false).reject { |item| item.to_s.end_with?('=') }.sort
|
11
|
+
end
|
12
|
+
|
13
|
+
def action_available? action
|
14
|
+
available_actions.include? action.to_sym
|
15
|
+
end
|
16
|
+
|
17
|
+
def exit_with_error! msg
|
18
|
+
STDERR.puts msg
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def usage
|
23
|
+
"Usage: #{File.basename(__FILE__)} 'Name of my project' [#{available_actions.join('|')}]"
|
24
|
+
end
|
25
|
+
|
26
|
+
def all action
|
27
|
+
puts('"project","status","last active on","total duration","hourly rate","earnings"') if action == 'csv'
|
28
|
+
Dir[PunchCard::SETTINGS_DIR+'/*'].sort_by { |f| File.mtime(f) }.reverse.each do |file|
|
29
|
+
puts `ruby #{__FILE__} #{action} '#{File.basename(file)}'`
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if ARGV.first == 'all'
|
34
|
+
all ARGV[1] ? ARGV[1] : 'csv'
|
35
|
+
exit
|
36
|
+
elsif ARGV.first && ['-h', '--help', 'help'].include?(ARGV.first)
|
37
|
+
puts usage
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
|
41
|
+
selected_action = ARGV[0]
|
42
|
+
project_name = ARGV[1]
|
43
|
+
|
44
|
+
if selected_action
|
45
|
+
if action_available?(selected_action)
|
46
|
+
exit_with_error!("2nd argument has to be the project name, e.g.:\n#{usage}") if !project_name && selected_action != 'list'
|
47
|
+
punch_card = PunchCard.new project_name
|
48
|
+
begin
|
49
|
+
arguments = ARGV.drop(2)
|
50
|
+
if arguments.size > 0
|
51
|
+
puts punch_card.send(selected_action.to_s, *arguments)
|
52
|
+
else
|
53
|
+
puts punch_card.send(selected_action.to_s)
|
54
|
+
end
|
55
|
+
rescue PunchCardError => e
|
56
|
+
exit_with_error! "Error: #{e.message}"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
exit_with_error! "Unrecognized action '#{selected_action || ''}'\n#{usage}"
|
60
|
+
end
|
61
|
+
end
|
data/lib/punchcard.rb
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
# (c) 2017 by philipp staender
|
2
|
+
|
3
|
+
class PunchCardError < StandardError;
|
4
|
+
end
|
5
|
+
|
6
|
+
class PunchCard
|
7
|
+
|
8
|
+
SETTINGS_DIR = ENV['PUNCHCARD_DIR'] || File.expand_path('~/.punchcard')
|
9
|
+
HOURLY_RATE_PATTERN = /^\s*(\d+)([^\d]+)*\s*/i
|
10
|
+
TIME_POINT_PATTERN = /^(\d+)((\-)(\d+))*$/
|
11
|
+
META_KEY_PATTERN = /^([a-zA-Z0-9]+)\:\s*(.*)$/
|
12
|
+
VERSION = '0.1.0'
|
13
|
+
|
14
|
+
attr_accessor :project
|
15
|
+
|
16
|
+
def initialize project_name
|
17
|
+
@wilcard_for_filename = ''
|
18
|
+
@meta_data = {}
|
19
|
+
find_or_make_settings_dir
|
20
|
+
if project_name
|
21
|
+
self.project = project_name
|
22
|
+
find_or_make_file
|
23
|
+
read_project_data
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def start
|
28
|
+
output = []
|
29
|
+
if start_time && !end_time
|
30
|
+
output << "'#{project}' already started (#{humanized_total} total)"
|
31
|
+
output << "#{duration(start_time, timestamp)}"
|
32
|
+
else
|
33
|
+
output << "'#{project}' started (#{humanized_total} total)"
|
34
|
+
self.start_time = timestamp
|
35
|
+
end
|
36
|
+
output.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
output = []
|
41
|
+
if end_time
|
42
|
+
output << "'#{@project}' already stopped (#{humanized_total} total)"
|
43
|
+
elsif start_time
|
44
|
+
output << "'#{@project}' stopped (#{humanized_total} total)"
|
45
|
+
self.end_time = timestamp
|
46
|
+
else
|
47
|
+
output << "Nothing to stop"
|
48
|
+
end
|
49
|
+
output.join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def toggle
|
53
|
+
if active?
|
54
|
+
stop
|
55
|
+
else
|
56
|
+
start
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def status
|
61
|
+
project_exists_or_stop!
|
62
|
+
find_or_make_file
|
63
|
+
output = []
|
64
|
+
output << (project+" (#{running_status})\n")
|
65
|
+
output << humanized_total
|
66
|
+
output.join("\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
def details
|
70
|
+
project_exists_or_stop!
|
71
|
+
find_or_make_file
|
72
|
+
output = []
|
73
|
+
output << project+" (#{running_status})\n\n"
|
74
|
+
project_data.map do |line|
|
75
|
+
points = line_to_time_points(line)
|
76
|
+
if points
|
77
|
+
starttime = points[0]
|
78
|
+
endtime = points[1] || timestamp
|
79
|
+
output << duration(starttime, endtime) + "\t" + self.class.format_time(Time.at(starttime)) + " - " + self.class.format_time(Time.at(endtime))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
output << "========\n#{humanized_total}\t(total)"
|
83
|
+
output.join("\n")
|
84
|
+
end
|
85
|
+
|
86
|
+
def csv
|
87
|
+
project_exists_or_stop!
|
88
|
+
find_or_make_file
|
89
|
+
durations = []
|
90
|
+
project_data.map do |line|
|
91
|
+
points = line_to_time_points(line)
|
92
|
+
if points
|
93
|
+
starttime = points[0]
|
94
|
+
endtime = points[1] || timestamp
|
95
|
+
durations.push duration(starttime, endtime)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
'"'+[
|
99
|
+
@project,
|
100
|
+
running_status,
|
101
|
+
self.class.format_time(File.ctime(project_file)),
|
102
|
+
humanized_total,
|
103
|
+
hourly_rate ? hourly_rate[:hourlyRate].to_s + " #{hourly_rate[:currency]}" : '',
|
104
|
+
hourly_rate ? (hourly_rate[:hourlyRate] * total / 3600.0).round(2).to_s + " #{hourly_rate[:currency]}" : '',
|
105
|
+
].join('","') + '"'
|
106
|
+
end
|
107
|
+
|
108
|
+
def remove
|
109
|
+
if File.exists?(project_file)
|
110
|
+
File.delete(project_file)
|
111
|
+
"Deleted #{project_file}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def rename new_project_name
|
116
|
+
old_filename = project_filename
|
117
|
+
data = project_data
|
118
|
+
data[0] = new_project_name
|
119
|
+
write_string_to_project_file! data.join("\n")
|
120
|
+
self.project = new_project_name
|
121
|
+
File.rename(old_filename, project_filename) && "#{old_filename} -> #{project_filename}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def project= project_name
|
125
|
+
@project = project_name
|
126
|
+
if @project.end_with?('*')
|
127
|
+
@wilcard_for_filename = "*"
|
128
|
+
@project = @project.chomp("*")
|
129
|
+
end
|
130
|
+
@project.strip
|
131
|
+
end
|
132
|
+
|
133
|
+
def project
|
134
|
+
@project.strip
|
135
|
+
end
|
136
|
+
|
137
|
+
def set key, value
|
138
|
+
raise PunchCardError.new("Key '#{key}' can only be alphanumeric") unless key.match(/^[a-zA-Z0-9]+$/)
|
139
|
+
@meta_data[key.to_sym] = value
|
140
|
+
write_to_project_file!
|
141
|
+
@meta_data
|
142
|
+
end
|
143
|
+
|
144
|
+
def total
|
145
|
+
total = 0
|
146
|
+
project_data.map do |line|
|
147
|
+
points = line_to_time_points(line)
|
148
|
+
if points
|
149
|
+
starttime = points[0]
|
150
|
+
endtime = points[1] || timestamp
|
151
|
+
total += endtime - starttime
|
152
|
+
end
|
153
|
+
end
|
154
|
+
total
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.format_time datetime
|
158
|
+
datetime.strftime('%F %T')
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def hourly_rate
|
164
|
+
hourly_rate_found = @meta_data[:hourlyRate] && @meta_data[:hourlyRate].match(HOURLY_RATE_PATTERN)
|
165
|
+
if hourly_rate_found
|
166
|
+
{
|
167
|
+
hourlyRate: hourly_rate_found[1].to_f,
|
168
|
+
currency: hourly_rate_found[2] ? hourly_rate_found[2].strip : '',
|
169
|
+
}
|
170
|
+
else
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def project_exists_or_stop!
|
176
|
+
raise PunchCardError.new("'#{@project}' does not exists") unless project_exist?
|
177
|
+
end
|
178
|
+
|
179
|
+
def active?
|
180
|
+
running_status == 'running'
|
181
|
+
end
|
182
|
+
|
183
|
+
def running_status
|
184
|
+
start_time && !end_time ? 'running' : 'stopped'
|
185
|
+
end
|
186
|
+
|
187
|
+
def humanized_total
|
188
|
+
humanize_duration total
|
189
|
+
end
|
190
|
+
|
191
|
+
def duration starttime, endtime
|
192
|
+
if starttime
|
193
|
+
humanize_duration endtime - starttime
|
194
|
+
else
|
195
|
+
humanize_duration 0
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def humanize_duration duration
|
200
|
+
hours = duration / (60 * 60)
|
201
|
+
minutes = (duration / 60) % 60
|
202
|
+
seconds = duration % 60
|
203
|
+
"#{decimal_digits(hours)}:#{decimal_digits(minutes)}:#{decimal_digits(seconds)}"
|
204
|
+
end
|
205
|
+
|
206
|
+
def decimal_digits digit
|
207
|
+
if digit.to_i < 10
|
208
|
+
"0#{digit}"
|
209
|
+
else
|
210
|
+
digit.to_s
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def start_time
|
215
|
+
time_points ? time_points[0] : nil
|
216
|
+
end
|
217
|
+
|
218
|
+
def start_time= time
|
219
|
+
append_new_line time
|
220
|
+
end
|
221
|
+
|
222
|
+
def end_time= time
|
223
|
+
replace_last_line "#{start_time}-#{time}"
|
224
|
+
end
|
225
|
+
|
226
|
+
def end_time
|
227
|
+
time_points ? time_points[1] : nil
|
228
|
+
end
|
229
|
+
|
230
|
+
def time_points
|
231
|
+
line_to_time_points last_entry
|
232
|
+
end
|
233
|
+
|
234
|
+
def line_to_time_points line
|
235
|
+
matches = line.match(TIME_POINT_PATTERN)
|
236
|
+
matches ? [matches[1].to_i, matches[4] ? matches[4].to_i : nil] : nil
|
237
|
+
end
|
238
|
+
|
239
|
+
def last_entry
|
240
|
+
project_data.last
|
241
|
+
end
|
242
|
+
|
243
|
+
def timestamp
|
244
|
+
Time.now.to_i
|
245
|
+
end
|
246
|
+
|
247
|
+
def read_project_data
|
248
|
+
title = nil
|
249
|
+
meta_data = []
|
250
|
+
timestamps = []
|
251
|
+
i = 0
|
252
|
+
File.open(project_file, "r").each_line do |line|
|
253
|
+
line.strip!
|
254
|
+
if i.zero?
|
255
|
+
title = line
|
256
|
+
elsif line.match(META_KEY_PATTERN)
|
257
|
+
set line.match(META_KEY_PATTERN)[1], line.match(META_KEY_PATTERN)[2]
|
258
|
+
elsif line.match(TIME_POINT_PATTERN)
|
259
|
+
timestamps.push line
|
260
|
+
end
|
261
|
+
i += 1
|
262
|
+
end
|
263
|
+
@project = title if title
|
264
|
+
timestamps
|
265
|
+
end
|
266
|
+
|
267
|
+
def project_data
|
268
|
+
File.open(project_file).each_line.map { |line| line.strip }
|
269
|
+
end
|
270
|
+
|
271
|
+
def write_string_to_project_file! string
|
272
|
+
File.open(project_file, 'w') { |f| f.write(string) }
|
273
|
+
end
|
274
|
+
|
275
|
+
def write_to_project_file!
|
276
|
+
timestamps = project_data.select { |line| line.match(/^\d+/) }
|
277
|
+
meta_data_lines = @meta_data.map { |key, value| "#{key}: #{value}" }
|
278
|
+
write_string_to_project_file! [@project, meta_data_lines.join("\n"), timestamps].reject(&:empty?).join("\n")
|
279
|
+
end
|
280
|
+
|
281
|
+
def append_new_line line
|
282
|
+
open(project_file, 'a') { |f| f.puts("\n"+line.to_s.strip) }
|
283
|
+
end
|
284
|
+
|
285
|
+
def replace_last_line line
|
286
|
+
data = project_data
|
287
|
+
data[-1] = line
|
288
|
+
write_string_to_project_file! data.join("\n")
|
289
|
+
end
|
290
|
+
|
291
|
+
def project_file
|
292
|
+
Dir[project_filename + @wilcard_for_filename].sort_by { |f| File.mtime(f) }.reverse.first || project_filename
|
293
|
+
end
|
294
|
+
|
295
|
+
def project_filename
|
296
|
+
SETTINGS_DIR + "/#{sanitize_filename(@project)}"
|
297
|
+
end
|
298
|
+
|
299
|
+
def project_exist?
|
300
|
+
File.exists?(project_file)
|
301
|
+
end
|
302
|
+
|
303
|
+
def find_or_make_file
|
304
|
+
write_string_to_project_file!(@project+"\n") unless project_exist?
|
305
|
+
@project = project_data.first
|
306
|
+
end
|
307
|
+
|
308
|
+
def find_or_make_settings_dir
|
309
|
+
Dir.mkdir(SETTINGS_DIR) unless File.exists?(SETTINGS_DIR)
|
310
|
+
end
|
311
|
+
|
312
|
+
def sanitize_filename name
|
313
|
+
name.downcase.gsub(/(\\|\/)/, '').gsub(/[^0-9a-z.\-]/, '_')
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
data/punched.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "punchcard"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'punched'
|
7
|
+
s.version = PunchCard::VERSION
|
8
|
+
s.authors = ["Philipp Staender"]
|
9
|
+
s.email = ["philipp.staender@gmail.com"]
|
10
|
+
s.homepage = 'https://github.com/pstaender/punchcard'
|
11
|
+
s.summary = 'Punchcard Timetracker'
|
12
|
+
s.description = 'Minimal time tracking tool for cli'
|
13
|
+
s.license = 'GPL-3.0'
|
14
|
+
s.executables = ['punched']
|
15
|
+
s.default_executable = 'punched'
|
16
|
+
s.rubyforge_project = 'punched'
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require "punchcard"
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
def example_settings_dir
|
7
|
+
File.expand_path('./punchcard_test_data')
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup_example_settings_dir
|
11
|
+
Dir.glob(example_settings_dir+'/*').each { |file| File.delete(file) }
|
12
|
+
PunchCard.send(:remove_const, :SETTINGS_DIR)
|
13
|
+
PunchCard.const_set(:SETTINGS_DIR, example_settings_dir)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe PunchCard do
|
17
|
+
|
18
|
+
before do
|
19
|
+
setup_example_settings_dir
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:example_project) { PunchCard.new('My Project') }
|
23
|
+
|
24
|
+
def random_project
|
25
|
+
PunchCard.new("My random Project #{SecureRandom.hex}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def my_project_file filename = 'my_project'
|
29
|
+
File.open("#{example_settings_dir}/#{filename}", "r").read
|
30
|
+
end
|
31
|
+
|
32
|
+
def start_and_stop
|
33
|
+
example_project.start
|
34
|
+
sleep 0.1
|
35
|
+
example_project.stop
|
36
|
+
example_project
|
37
|
+
end
|
38
|
+
|
39
|
+
def two_seconds_tracking
|
40
|
+
example_project.start
|
41
|
+
sleep 2
|
42
|
+
example_project
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should create a new PunchCard object' do
|
46
|
+
expect(example_project).to be_a(PunchCard)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should start a project' do
|
50
|
+
example_project.start
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should start and stop a project' do
|
54
|
+
start_and_stop
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should track a project time' do
|
58
|
+
timestamp = Time.now.to_i
|
59
|
+
start_and_stop
|
60
|
+
expect(my_project_file.lines.first.strip).to eq('My Project')
|
61
|
+
expect(my_project_file.lines.last.strip).to match('^'+timestamp.to_s[0..-3]+'\\d{2}\\-'+timestamp.to_s[0..-3]+'\\d{2}+$')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should calculate tracked total time' do
|
65
|
+
project = two_seconds_tracking
|
66
|
+
tracked_time = project.details.lines.last.match(/^\d{2}\:\d{2}\:(\d{2}).*total/)[1].to_i
|
67
|
+
expect(tracked_time).to be_between 1, 3
|
68
|
+
project = two_seconds_tracking
|
69
|
+
tracked_time = project.details.lines.last.match(/^\d{2}\:\d{2}\:(\d{2}).*total/)[1].to_i
|
70
|
+
expect(tracked_time).to be_between 3, 5
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should toggle' do
|
74
|
+
project = start_and_stop
|
75
|
+
expect(project.status.lines.first).to match /stopped/
|
76
|
+
project.toggle
|
77
|
+
expect(project.status.lines.first).to match /running/
|
78
|
+
project.toggle
|
79
|
+
expect(project.status.lines.first).to match /stopped/
|
80
|
+
expect(my_project_file.lines.last).to match(/^\d+/)
|
81
|
+
expect(my_project_file.lines[-2]).to match(/^\d+/)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should read and write utf8 names' do
|
85
|
+
PunchCard.new "Playing Motörhead"
|
86
|
+
expect(my_project_file('playing_mot_rhead').strip).to eq("Playing Motörhead")
|
87
|
+
project = PunchCard.new "Playing*"
|
88
|
+
expect(project.project).to eq("Playing Motörhead")
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should set hourlyRate' do
|
92
|
+
project = start_and_stop
|
93
|
+
project.set 'hourlyRate', "1000 €"
|
94
|
+
expect(my_project_file.lines[1].strip).to eq("hourlyRate: 1000 €")
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should calculate earnings' do
|
98
|
+
project = start_and_stop
|
99
|
+
project.set 'hourlyRate', "1000EURO"
|
100
|
+
project.toggle
|
101
|
+
sleep 2
|
102
|
+
project.toggle
|
103
|
+
project.toggle
|
104
|
+
sleep 2
|
105
|
+
project.toggle
|
106
|
+
expect(project.csv).to match /^"My Project","stopped","[0-9\-\s\:]+?","[0-9\:]+?","1000.0 EURO","1\.\d+ EURO"$/
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should track different projects simultanously' do
|
110
|
+
project_a = random_project
|
111
|
+
project_b = random_project
|
112
|
+
expect(project_a.project).not_to eq(project_b.project)
|
113
|
+
project_a.start
|
114
|
+
project_b.start
|
115
|
+
sleep 2
|
116
|
+
project_a.stop
|
117
|
+
sleep 2
|
118
|
+
project_b.stop
|
119
|
+
expect(project_b.total.to_i - project_a.total.to_i).to be_between(2,3)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should load latest project by wildcard' do
|
123
|
+
project_a = random_project
|
124
|
+
project = PunchCard.new "My random*"
|
125
|
+
expect(project.project).to eq(project_a.project)
|
126
|
+
sleep 1
|
127
|
+
project_b = random_project
|
128
|
+
project = PunchCard.new "My random*"
|
129
|
+
expect(project.project).to eq(project_b.project)
|
130
|
+
expect(project.project).not_to eq(project_a.project)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should rename' do
|
134
|
+
project = example_project
|
135
|
+
content = my_project_file
|
136
|
+
project.rename 'Renamed Project'
|
137
|
+
expect(File.open("#{example_settings_dir}/renamed_project", "r").read.strip).to eq(content.strip.sub(/My Project/, 'Renamed Project'))
|
138
|
+
expect(File.exists?("#{example_settings_dir}/my_project")).to be_falsey
|
139
|
+
expect(project.project).to eq('Renamed Project')
|
140
|
+
project.start
|
141
|
+
sleep 0.1
|
142
|
+
project.stop
|
143
|
+
content = File.open("#{example_settings_dir}/renamed_project", "r").read.strip
|
144
|
+
project.rename 'Other Project'
|
145
|
+
expect(File.open("#{example_settings_dir}/other_project", "r").read.strip).to eq(content.strip.sub(/Renamed Project/, 'Other Project'))
|
146
|
+
expect(File.exists?("#{example_settings_dir}/renamed_project")).to be_falsey
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should remove' do
|
150
|
+
project = example_project
|
151
|
+
expect(File.exists?("#{example_settings_dir}/my_project")).to be_truthy
|
152
|
+
project.remove
|
153
|
+
expect(File.exists?("#{example_settings_dir}/my_project")).to be_falsey
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|