punched 1.0.4 → 1.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 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: []