punched 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b31fb8d911d693ff343d9becd044d9c3378b64ba8214e6175aec5e6e042ce621
4
- data.tar.gz: aba03f814669c72f40e8cb2a56317aac1313ae3033f627fcc3ae9f83cc33ad03
3
+ metadata.gz: 3d977bf420412857af8b4b497b7eea7bcf68a102c3ce82bc6d7bac251ffb3753
4
+ data.tar.gz: 0c2e123a93f7dc608eb3feaf63501b1f2de35f43f61110b5cc98db314083aa2a
5
5
  SHA512:
6
- metadata.gz: 9bd2d23729a8fbde5073d9aaa907d8585ea10c063ffeb3176e942cf6ce2fae7fdd078b038e7a520f03a39f4037c385278fe2c1a8b7e28b058bf9586ded70094c
7
- data.tar.gz: d4111205855e18c030236011ba93e02b6780771595df1c755d61095f5d6fcec0425cf22d56ca0da723c6e7bd5dabd2120ff4cc2035e80dcb62579fea8d3d15a7
6
+ metadata.gz: 9656f8be00bd6cb0768982eb9671f58544d12260453d943fcbdfa5f097215599b6281f0bb2984f450ed258c3af7a9aae470d0fb228fac21418c4aaa243c7cba5
7
+ data.tar.gz: 22f015763eb98da0de64fcf742a7f1907d81ab66a8b2a4c82092697e568c6728eafb38d5eaf2827c845685264c5960fca1540539738b22dc91d53adf667a72c6
data/.travis.yml CHANGED
@@ -5,6 +5,7 @@ rvm:
5
5
  - 2.3
6
6
  - 2.4
7
7
  - 2.5
8
+ - 2.6
8
9
  - ruby-head
9
10
  script: bundle exec rspec
10
11
  matrix:
data/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  [![Build Status](https://img.shields.io/travis/pstaender/punched.svg?branch=v1.0.0&style=flat-square)](https://travis-ci.org/pstaender/punched)
5
5
 
6
+ [![asciicast](https://asciinema.org/a/222572.svg)](https://asciinema.org/a/222572)
7
+
6
8
  ### Requirements
7
9
 
8
10
  * ruby 2.1+
data/bin/punched CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
- $:.push File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
3
3
 
4
- require 'punchcard'
4
+ require 'punchcard.rb'
5
5
  require 'csv'
6
6
  require 'markdown-tables'
7
7
  require 'date'
8
+ require 'json'
9
+
10
+ class UnknownActionError < StandardError; end
8
11
 
9
12
  #
10
13
  # CLI Wrapper
@@ -14,11 +17,11 @@ def available_actions
14
17
  PunchCard.new(nil).public_methods(false).reject { |item| item.to_s.end_with?('=') || item.to_s == 'project' }.concat([:all]).sort
15
18
  end
16
19
 
17
- def action_available? action
20
+ def action_available?(action)
18
21
  available_actions.include? action.to_sym
19
22
  end
20
23
 
21
- def exit_with_error! msg
24
+ def exit_with_error!(msg)
22
25
  STDERR.puts msg
23
26
  exit 1
24
27
  end
@@ -27,22 +30,21 @@ def usage
27
30
  "Usage: #{File.basename(__FILE__)} #{available_actions.join('|')} 'Name of my project'"
28
31
  end
29
32
 
30
- def all action
31
- available_formats = %w(csv plain md)
33
+ def all(action)
34
+ available_formats = %w[csv plain md]
32
35
  unless available_formats.include?(action)
33
36
  raise "Format #{action} is not supported. Possible formats are: #{available_formats.join(',')}"
34
37
  end
35
- labels = ["project","status","last active on","total duration","hourly rate","earnings"]
36
- data = Dir[PunchCard::SETTINGS_DIR+'/*'].map { |file|
37
- data = CSV.parse(`ruby #{__FILE__} csv '#{File.basename(file)}'`).first
38
+
39
+ labels = ['project', 'status', 'last active on', 'total duration', 'hourly rate', 'earnings']
40
+ data = Dir[PunchCard::SETTINGS_DIR + '/*'].map do |file|
41
+ data = CSV.parse(call_punchcard('csv', File.basename(file)))[0]
38
42
  last_activity = !data[2].empty? ? DateTime.parse(data[2]).to_time.to_i : 0
39
43
  data.push(last_activity)
40
44
  data
41
- }.sort { |a, b|
42
- a.last <=> b.last
43
- }.reverse.map { |row|
45
+ end.sort_by(&:last).reverse.map do |row|
44
46
  row[0...-1]
45
- }
47
+ end
46
48
 
47
49
  return puts('No record(s) so far') if data.empty?
48
50
 
@@ -58,27 +60,18 @@ def all action
58
60
  end
59
61
  end
60
62
 
61
- if ARGV.first == 'all'
62
- all ARGV[1] || 'plain'
63
- exit
64
- elsif ARGV.first && ['-h', '--help', 'help'].include?(ARGV.first)
65
- puts usage
66
- exit
67
- end
68
-
69
- selected_action = ARGV[0]
70
- project_name = ARGV[1]
71
-
72
- if selected_action
63
+ def call_punchcard(selected_action, project_name)
73
64
  if action_available?(selected_action)
74
- exit_with_error!("2nd argument has to be the project name, e.g.:\n#{usage}") if !project_name && selected_action != 'list'
65
+ if !project_name && selected_action != 'list'
66
+ exit_with_error!("2nd argument has to be the project name, e.g.:\n#{usage}")
67
+ end
75
68
  punch_card = PunchCard.new project_name
76
69
  begin
77
70
  arguments = ARGV.drop(2)
78
- if arguments.size > 0
79
- puts punch_card.send(selected_action.to_s, *arguments)
71
+ if !arguments.empty?
72
+ return punch_card.send(selected_action.to_s, *arguments)
80
73
  else
81
- puts punch_card.send(selected_action.to_s)
74
+ return punch_card.send(selected_action.to_s)
82
75
  end
83
76
  rescue PunchCardError => e
84
77
  exit_with_error! "Error: #{e.message}"
@@ -87,3 +80,30 @@ if selected_action
87
80
  exit_with_error! "Unrecognized action '#{selected_action || ''}'\n#{usage}"
88
81
  end
89
82
  end
83
+
84
+ if ['-h', '--help', 'help'].include?(ARGV.first)
85
+ puts(usage)
86
+ exit
87
+ end
88
+
89
+ selected_action = ARGV[0]
90
+ project_name = ARGV[1]
91
+
92
+ if selected_action
93
+ begin
94
+ if selected_action == 'all'
95
+ all(ARGV[1] || 'plain')
96
+ else
97
+ result = call_punchcard(selected_action, project_name)
98
+ if result.is_a?(Hash)
99
+ puts result.to_json
100
+ else
101
+ puts result
102
+ end
103
+ end
104
+ rescue PunchCardError => e
105
+ exit_with_error! "Error: #{e.message}"
106
+ rescue UnknownActionError => e
107
+ exit_with_error! "Unrecognized action '#{selected_action || ''}'\n#{usage}"
108
+ end
109
+ end
data/lib/punchcard.rb CHANGED
@@ -1,4 +1,4 @@
1
- # (c) 2017-2018 by philipp staender
1
+ # (c) 2017-2019 by Philipp Staender
2
2
 
3
3
  require 'date'
4
4
  require 'time'
@@ -7,31 +7,30 @@ class PunchCardError < StandardError
7
7
  end
8
8
 
9
9
  class PunchCard
10
-
11
10
  SETTINGS_DIR = ENV['PUNCHCARD_DIR'] || File.expand_path('~/.punchcard')
12
- HOURLY_RATE_PATTERN = /^\s*(\d+)([^\d]+)*\s*/i
13
- TIME_POINT_PATTERN = /^((\d+|.+?\s[\+\-]\d{4}?\s*)(\-)*(\d+|\s.+\d?)*)$/
14
- META_KEY_PATTERN = /^([a-zA-Z0-9]+)\:\s*(.*)$/
15
- VERSION = '1.0.4'
11
+ HOURLY_RATE_PATTERN = /^\s*(\d+)([^\d]+)*\s*/i.freeze
12
+ TIME_POINT_PATTERN = /^((\d+|.+?\s[\+\-]\d{4}?\s*)(\-)*(\d+|\s.+\d?)*)$/.freeze
13
+ META_KEY_PATTERN = /^([a-zA-Z0-9]+)\:\s*(.*)$/.freeze
14
+ VERSION = '1.1.0'.freeze
16
15
 
17
16
  attr_accessor :project
18
17
 
19
- def initialize project_name
18
+ def initialize(project_name)
20
19
  @wilcard_for_filename = ''
21
20
  @meta_data = {}
22
21
  find_or_make_settings_dir
23
- if project_name
24
- self.project = project_name
25
- find_or_make_file
26
- read_project_data
27
- end
22
+ return unless project_name
23
+
24
+ self.project = project_name
25
+ find_or_make_file
26
+ read_project_data
28
27
  end
29
28
 
30
29
  def start
31
30
  output = []
32
31
  if start_time && !end_time
33
32
  output << "'#{project}' already started (#{humanized_total} total)"
34
- output << "#{duration(start_time, timestamp)}"
33
+ output << duration(start_time, timestamp).to_s
35
34
  else
36
35
  output << "'#{project}' started (#{humanized_total} total)"
37
36
  self.start_time = timestamp
@@ -47,7 +46,7 @@ class PunchCard
47
46
  output << "'#{@project}' stopped (#{humanized_total} total)"
48
47
  self.end_time = timestamp
49
48
  else
50
- output << "Nothing to stop"
49
+ output << 'Nothing to stop'
51
50
  end
52
51
  output.join("\n")
53
52
  end
@@ -64,7 +63,7 @@ class PunchCard
64
63
  project_exists_or_stop!
65
64
  find_or_make_file
66
65
  output = []
67
- output << (project+" (#{running_status})\n")
66
+ output << (project + " (#{running_status})\n")
68
67
  output << humanized_total
69
68
  output.join("\n")
70
69
  end
@@ -73,14 +72,18 @@ class PunchCard
73
72
  project_exists_or_stop!
74
73
  find_or_make_file
75
74
  output = []
76
- output << project+" (#{running_status})\n\n"
77
- project_data.map do |line|
75
+ data = project_data
76
+ data[0] = "#{data[0]} (#{running_status})"
77
+ data.map do |line|
78
78
  points = line_to_time_points(line)
79
- if points
80
- starttime = points[0]
81
- endtime = points[1] || timestamp
82
- output << duration(starttime, endtime) + "\t" + self.class.format_time(Time.at(starttime)) + " - " + self.class.format_time(Time.at(endtime))
79
+ unless points
80
+ output << line + "\n"
81
+ next
83
82
  end
83
+
84
+ starttime = points[0]
85
+ endtime = points[1] || timestamp
86
+ output << duration(starttime, endtime) + "\t" + self.class.format_time(Time.at(starttime)) + ' - ' + self.class.format_time(Time.at(endtime))
84
87
  end
85
88
  output << "========\n#{humanized_total}\t(total)"
86
89
  output.join("\n")
@@ -93,31 +96,31 @@ class PunchCard
93
96
  last_activity = nil
94
97
  project_data.map do |line|
95
98
  points = line_to_time_points(line)
96
- if points
97
- starttime = points[0]
98
- endtime = points[1] || timestamp
99
- last_activity = points[1] || points[0]
100
- durations.push duration(starttime, endtime)
101
- end
99
+ next unless points
100
+
101
+ starttime = points[0]
102
+ endtime = points[1] || timestamp
103
+ last_activity = points[1] || points[0]
104
+ durations.push duration(starttime, endtime)
102
105
  end
103
- '"'+[
104
- @project,
105
- running_status,
106
- last_activity ? self.class.format_time(Time.at(last_activity).to_datetime) : '',
107
- humanized_total,
108
- hourly_rate ? hourly_rate[:hourlyRate].to_s + " #{hourly_rate[:currency]}" : '',
109
- hourly_rate ? (hourly_rate[:hourlyRate] * total / 3600.0).round(2).to_s + " #{hourly_rate[:currency]}" : '',
106
+ '"' + [
107
+ @project,
108
+ running_status,
109
+ last_activity ? self.class.format_time(Time.at(last_activity).to_datetime) : '',
110
+ humanized_total,
111
+ hourly_rate ? hourly_rate[:hourlyRate].to_s + " #{hourly_rate[:currency]}" : '',
112
+ hourly_rate ? (hourly_rate[:hourlyRate] * total / 3600.0).round(2).to_s + " #{hourly_rate[:currency]}" : ''
110
113
  ].join('","') + '"'
111
114
  end
112
115
 
113
116
  def remove
114
- if File.exists?(project_file)
117
+ if File.exist?(project_file)
115
118
  File.delete(project_file)
116
119
  "Deleted #{project_file}"
117
120
  end
118
121
  end
119
122
 
120
- def rename new_project_name
123
+ def rename(new_project_name)
121
124
  old_filename = project_filename
122
125
  data = project_data
123
126
  data[0] = new_project_name
@@ -126,11 +129,11 @@ class PunchCard
126
129
  File.rename(old_filename, project_filename) && "#{old_filename} -> #{project_filename}"
127
130
  end
128
131
 
129
- def project= project_name
132
+ def project=(project_name)
130
133
  @project = project_name
131
134
  if @project.end_with?('*')
132
- @wilcard_for_filename = "*"
133
- @project = @project.chomp("*")
135
+ @wilcard_for_filename = '*'
136
+ @project = @project.chomp('*')
134
137
  end
135
138
  @project.strip
136
139
  end
@@ -139,8 +142,9 @@ class PunchCard
139
142
  @project.strip
140
143
  end
141
144
 
142
- def set key, value
143
- raise PunchCardError.new("Key '#{key}' can only be alphanumeric") unless key.match(/^[a-zA-Z0-9]+$/)
145
+ def set(key, value)
146
+ raise PunchCardError, "Key '#{key}' can only be alphanumeric" unless key =~ /^[a-zA-Z0-9]+$/
147
+
144
148
  @meta_data[key.to_sym] = value
145
149
  write_to_project_file!
146
150
  @meta_data
@@ -150,16 +154,16 @@ class PunchCard
150
154
  total = 0
151
155
  project_data.map do |line|
152
156
  points = line_to_time_points(line)
153
- if points
154
- starttime = points[0]
155
- endtime = points[1] || timestamp
156
- total += endtime - starttime
157
- end
157
+ next unless points
158
+
159
+ starttime = points[0]
160
+ endtime = points[1] || timestamp
161
+ total += endtime - starttime
158
162
  end
159
163
  total
160
164
  end
161
165
 
162
- def self.format_time datetime
166
+ def self.format_time(datetime)
163
167
  datetime.strftime('%F %T')
164
168
  end
165
169
 
@@ -167,18 +171,16 @@ class PunchCard
167
171
 
168
172
  def hourly_rate
169
173
  hourly_rate_found = @meta_data[:hourlyRate] && @meta_data[:hourlyRate].match(HOURLY_RATE_PATTERN)
170
- if hourly_rate_found
171
- {
172
- hourlyRate: hourly_rate_found[1].to_f,
173
- currency: hourly_rate_found[2] ? hourly_rate_found[2].strip : '',
174
- }
175
- else
176
- nil
177
- end
174
+ return unless hourly_rate_found
175
+
176
+ {
177
+ hourlyRate: hourly_rate_found[1].to_f,
178
+ currency: hourly_rate_found[2] ? hourly_rate_found[2].strip : ''
179
+ }
178
180
  end
179
181
 
180
182
  def project_exists_or_stop!
181
- raise PunchCardError.new("'#{@project}' does not exists") unless project_exist?
183
+ raise PunchCardError, "'#{@project}' does not exists" unless project_exist?
182
184
  end
183
185
 
184
186
  def active?
@@ -193,7 +195,7 @@ class PunchCard
193
195
  humanize_duration total
194
196
  end
195
197
 
196
- def duration starttime, endtime
198
+ def duration(starttime, endtime)
197
199
  if starttime
198
200
  humanize_duration endtime - starttime
199
201
  else
@@ -201,14 +203,14 @@ class PunchCard
201
203
  end
202
204
  end
203
205
 
204
- def humanize_duration duration
206
+ def humanize_duration(duration)
205
207
  hours = duration / (60 * 60)
206
208
  minutes = (duration / 60) % 60
207
209
  seconds = duration % 60
208
210
  "#{decimal_digits(hours)}:#{decimal_digits(minutes)}:#{decimal_digits(seconds)}"
209
211
  end
210
212
 
211
- def decimal_digits digit
213
+ def decimal_digits(digit)
212
214
  if digit.to_i < 10
213
215
  "0#{digit}"
214
216
  else
@@ -220,11 +222,11 @@ class PunchCard
220
222
  time_points ? time_points[0] : nil
221
223
  end
222
224
 
223
- def start_time= time
225
+ def start_time=(time)
224
226
  append_new_line timestamp_to_time(time)
225
227
  end
226
228
 
227
- def end_time= time
229
+ def end_time=(time)
228
230
  replace_last_line "#{timestamp_to_time(start_time)} - #{timestamp_to_time(time)}"
229
231
  end
230
232
 
@@ -236,20 +238,20 @@ class PunchCard
236
238
  line_to_time_points last_entry
237
239
  end
238
240
 
239
- def line_to_time_points line
241
+ def line_to_time_points(line)
240
242
  matches = line.match(TIME_POINT_PATTERN)
241
-
243
+
242
244
  time_points = matches ? [string_to_timestamp(matches[2]), string_to_timestamp(matches[4])] : nil
243
245
  if time_points && time_points.reject(&:nil?).empty?
244
246
  nil
245
247
  else
246
248
  time_points
247
249
  end
248
-
249
250
  end
250
251
 
251
252
  def string_to_timestamp(str)
252
253
  return str if str.nil?
254
+
253
255
  str.strip!
254
256
  # This is some legacy... previous versions stored timestamp,
255
257
  # but now punched stores date-time strings for better readability.
@@ -271,10 +273,9 @@ class PunchCard
271
273
 
272
274
  def read_project_data
273
275
  title = nil
274
- meta_data = []
275
276
  timestamps = []
276
277
  i = 0
277
- File.open(project_file, "r").each_line do |line|
278
+ File.open(project_file, 'r').each_line do |line|
278
279
  line.strip!
279
280
  if i.zero?
280
281
  title = line
@@ -290,10 +291,10 @@ class PunchCard
290
291
  end
291
292
 
292
293
  def project_data
293
- File.open(project_file).each_line.map { |line| line.strip }
294
+ File.open(project_file).each_line.map(&:strip)
294
295
  end
295
296
 
296
- def write_string_to_project_file! string
297
+ def write_string_to_project_file!(string)
297
298
  File.open(project_file, 'w') { |f| f.write(string) }
298
299
  end
299
300
 
@@ -303,11 +304,11 @@ class PunchCard
303
304
  write_string_to_project_file! [@project, meta_data_lines.join("\n"), timestamps].reject(&:empty?).join("\n")
304
305
  end
305
306
 
306
- def append_new_line line
307
- open(project_file, 'a') { |f| f.puts("\n"+line.to_s.strip) }
307
+ def append_new_line(line)
308
+ open(project_file, 'a') { |f| f.puts("\n" + line.to_s.strip) }
308
309
  end
309
310
 
310
- def replace_last_line line
311
+ def replace_last_line(line)
311
312
  data = project_data
312
313
  data[-1] = line
313
314
  write_string_to_project_file! data.join("\n")
@@ -322,20 +323,19 @@ class PunchCard
322
323
  end
323
324
 
324
325
  def project_exist?
325
- File.exists?(project_file)
326
+ File.exist?(project_file)
326
327
  end
327
328
 
328
329
  def find_or_make_file
329
- write_string_to_project_file!(@project+"\n") unless project_exist?
330
+ write_string_to_project_file!(@project + "\n") unless project_exist?
330
331
  @project = project_data.first
331
332
  end
332
333
 
333
334
  def find_or_make_settings_dir
334
- Dir.mkdir(SETTINGS_DIR) unless File.exists?(SETTINGS_DIR)
335
+ Dir.mkdir(SETTINGS_DIR) unless File.exist?(SETTINGS_DIR)
335
336
  end
336
337
 
337
- def sanitize_filename name
338
- name.downcase.gsub(/(\\|\/)/, '').gsub(/[^0-9a-z.\-]/, '_')
338
+ def sanitize_filename(name)
339
+ name.downcase.gsub(%r{(\\|/)}, '').gsub(/[^0-9a-z.\-]/, '_')
339
340
  end
340
-
341
341
  end
data/punched.gemspec CHANGED
@@ -1,22 +1,20 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "punchcard"
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
+ require 'punchcard'
4
3
 
5
4
  Gem::Specification.new do |s|
6
5
  s.name = 'punched'
7
6
  s.version = PunchCard::VERSION
8
- s.authors = ["Philipp Staender"]
9
- s.email = ["philipp.staender@gmail.com"]
7
+ s.authors = ['Philipp Staender']
8
+ s.email = ['pstaender@mailbox.org']
10
9
  s.homepage = 'https://github.com/pstaender/punchcard'
11
10
  s.summary = 'Punchcard Timetracker'
12
11
  s.description = 'Minimal time tracking tool for cli'
13
12
  s.license = 'GPL-3.0'
14
13
  s.executables = ['punched']
15
- s.default_executable = 'punched'
16
14
  s.rubyforge_project = 'punched'
17
- s.require_paths = ["lib"]
15
+ s.require_paths = ['lib']
18
16
  s.required_ruby_version = '>= 2.1'
19
17
  s.files = `git ls-files`.split("\n")
20
-
18
+
21
19
  s.add_dependency 'markdown-tables', '~> 1.0.2'
22
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: punched
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philipp Staender
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-20 00:00:00.000000000 Z
11
+ date: 2019-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: markdown-tables
@@ -26,7 +26,7 @@ dependencies:
26
26
  version: 1.0.2
27
27
  description: Minimal time tracking tool for cli
28
28
  email:
29
- - philipp.staender@gmail.com
29
+ - pstaender@mailbox.org
30
30
  executables:
31
31
  - punched
32
32
  extensions: []