timeless 0.5.1 → 0.6.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: 89d84ba2c5ef3404ab9ca4e2168b04990a1889f79b17c95e7762c618937b2c6e
4
- data.tar.gz: ef7e2525c43a7d5479206ffbeebc9495699c093d55d817310e2c0127237ea898
3
+ metadata.gz: f271794edee45ff107dcd0d7858e9fb01c4f670e8b98b76e2c70de39d3353498
4
+ data.tar.gz: 9e80d347a218de303b7697265e07410d1a460b4262c928fe48bfe711a83002fe
5
5
  SHA512:
6
- metadata.gz: 5c8d3766566e43d4766087c1bcb955f5167fed66c50b9809045c7d607745d3b006153396b283c67ae88110854f8c134697f22c473c851eab73ad95eb3a6d97b0
7
- data.tar.gz: 14953c9913b8ef6b6d29056d880086f27d62b0a210bfab4b297e98c71699ba85ee7061b030a0f1c6352741f8e24d6bc254d5589a2d0af05aa5d67af180b0ef0b
6
+ metadata.gz: c7d5c0d76111c83e186b38f6be6ccb1d4170ccd27e427f775fa81ce3bcfd69173228b8fe47adea40e926c37f61cb3d1b6c75e808c6926eeb801d9119ce3a5400
7
+ data.tar.gz: 95b7c4020f915c4807b71c5d3afa05da17e1c1da18bd9b161bfc346a554e935e006d6305b326743d57363ce1a7bb75172a4e24330d913147514e4f73a4cbdf54
@@ -0,0 +1,202 @@
1
+ #+TITLE: Timeless README
2
+ #+AUTHOR: Adolfo Villafiorita
3
+ #+STARTUP: showall
4
+
5
+ Timeless is a simple command line time tracker which supports
6
+ stopwatches, pomodoro timers, and key-value pair in notes.
7
+
8
+ Some non nominal conditions are also handled (e.g., starting a clock
9
+ twice, trying to stop when no clock was started).
10
+
11
+ * Installation
12
+ :PROPERTIES:
13
+ :CUSTOM_ID: installation
14
+ :END:
15
+
16
+ Install the gem:
17
+
18
+ #+BEGIN_EXAMPLE
19
+ $ gem install timeless
20
+ #+END_EXAMPLE
21
+
22
+ * Usage
23
+ :PROPERTIES:
24
+ :CUSTOM_ID: usage
25
+ :END:
26
+
27
+ Run a pomodoro timer:
28
+
29
+ #+BEGIN_EXAMPLE
30
+ timeless pom p:project meeting with c:john
31
+ timeless pom --duration 60 p:prj2
32
+ timeless pom --long # default to 50 minutes
33
+ #+END_EXAMPLE
34
+
35
+ Start clocking using a stopwatch:
36
+
37
+ #+BEGIN_EXAMPLE
38
+ timeless start requirements doc
39
+ timeless start --at 'thirty minutes ago' fixing bug 182 for c:tim
40
+ timeless start --force --at 'five minutes ago' requirements document for p:prj1
41
+ #+END_EXAMPLE
42
+
43
+ Stop clocking:
44
+
45
+ #+BEGIN_EXAMPLE
46
+ timeless stop
47
+ timeless stop forgot notes on start
48
+ timeless stop --last # reuse the notes of previous entry
49
+ timeless stop --at 'five minutes ago'
50
+ timeless stop --start '1 hour ago' --at 'now' clocked a full entry
51
+ #+END_EXAMPLE
52
+
53
+ Enter a full entry:
54
+
55
+ #+BEGIN_EXAMPLE
56
+ timeless clock activity on p:project # from end of last entry to now
57
+ timeless clock --start 'three hours ago' --stop 'five minutes ago'
58
+ #+END_EXAMPLE
59
+
60
+ What was i doing?
61
+
62
+ #+BEGIN_EXAMPLE
63
+ timeless current # print current entry
64
+ timeless forget # forget pending clock
65
+ #+END_EXAMPLE
66
+
67
+ What did I do?
68
+
69
+ #+BEGIN_EXAMPLE
70
+ timeless last # print last entry
71
+ #+END_EXAMPLE
72
+
73
+ Reporting and exporting
74
+
75
+ #+BEGIN_EXAMPLE
76
+ timeless statement
77
+ timeless balance --from yesterday --filter p:project
78
+ timeless export
79
+ #+END_EXAMPLE
80
+
81
+ Notice that even though =timeless= uses CSV as its native format, the
82
+ export command generates a CSV file which is more easily parsed by a
83
+ spreadsheet application such as LibreOffice.
84
+
85
+ If you do not want to type =timeless= every time, you can invoke a
86
+ console:
87
+
88
+ #+BEGIN_EXAMPLE
89
+ timeless console
90
+ timeless:000> start
91
+ timeless:001> stop
92
+ timeless:002>
93
+ #+END_EXAMPLE
94
+
95
+ More information with:
96
+
97
+ #+BEGIN_EXAMPLE
98
+ timeless help # get list of available commands
99
+ timeless help command # help about command
100
+ timeless man # output this README file
101
+ #+END_EXAMPLE
102
+
103
+ * Key-Value pairs
104
+ :PROPERTIES:
105
+ :CUSTOM_ID: key-value-pairs
106
+ :END:
107
+
108
+ Timeless has some basic support for key-pair values in the notes
109
+ field.
110
+
111
+ A key-pair value is entered as:
112
+
113
+ #+BEGIN_EXAMPLE
114
+ key:value
115
+ #+END_EXAMPLE
116
+
117
+ where =key= is any string starting with a letter and containing letters,
118
+ numbers, and underscores; and =value= is *anything but a space*.
119
+
120
+ For instance a note field could look like:
121
+
122
+ #+BEGIN_EXAMPLE
123
+ working for c:john_smith on a:writing_documentation
124
+ #+END_EXAMPLE
125
+
126
+ Key value pairs can be used to assign a special meaning to some
127
+ strings and improve filtering and exporting.
128
+
129
+ Three special keys are =p= for projects, =c= for clients, and =a= for
130
+ activity. These keys get dedicated columns in the csv file when using
131
+ the =export= command.
132
+
133
+ * If something goes wrong
134
+ :PROPERTIES:
135
+ :CUSTOM_ID: if-something-goes-wrong
136
+ :END:
137
+
138
+ Timeless writes entries on =~/.timeless.csv=, storing the following
139
+ information:
140
+
141
+ 1. start date and time
142
+ 2. end date and time
143
+ 3. notes
144
+
145
+ You can edit the file to manually fix entries if you make some
146
+ mistake.
147
+
148
+ The =~/timeless.csv= file should contain only correct entries, that
149
+ is, entries with start, stop, and (possibly empty) notes.
150
+
151
+ When using a stopwatch, a temporary file =~/.timeless-tmp.csv= is used
152
+ to store the timestamp of the start command.
153
+
154
+ * Version History
155
+ :PROPERTIES:
156
+ :CUSTOM_ID: version-history
157
+ :END:
158
+
159
+ *** 0.6.0
160
+
161
+ - support for generic keys in notes. Possibility of specifying the same
162
+ key multiple times in a single note field
163
+ - balance now produces distinct reports for each key (on top of the three
164
+ already generated for projects, activities, and clients)
165
+ - management of empty lines in ~/timeless.csv
166
+
167
+ *** 0.5.0
168
+
169
+ - updated origin in Gem specification
170
+
171
+ *** 0.4.0
172
+
173
+ - second public release
174
+ - new =balance= command
175
+ - the =report= command has been renamed =statement=
176
+ - interactive =console=
177
+ - more detailed help for commands
178
+ - fixed a bug in the last command: now it returns the last activity in
179
+ chronological order (rather than the last activity which has been
180
+ clocked)
181
+
182
+ *** 0.2.0
183
+
184
+ - initial public release
185
+
186
+ ** License
187
+ :PROPERTIES:
188
+ :CUSTOM_ID: license
189
+ :END:
190
+
191
+ Licensed under the terms of the MIT License.
192
+
193
+ ** Contributing
194
+ :PROPERTIES:
195
+ :CUSTOM_ID: contributing
196
+ :END:
197
+
198
+ 1. Fork it ( https://github.com/[my-github-username]/timeless/fork )
199
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
200
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
201
+ 4. Push to the branch (`git push origin my-new-feature`)
202
+ 5. Create a new Pull Request
@@ -1,5 +1,7 @@
1
1
  ;; forms-mode for timeless -*- emacs-lisp -*-
2
2
 
3
+ (require 'forms)
4
+
3
5
  (setq forms-file (expand-file-name "~/.timeless.csv"))
4
6
 
5
7
  ;; Use `forms-enumerate' to set field names and number thereof.
@@ -17,7 +17,7 @@ module Timeless
17
17
  end
18
18
 
19
19
  def self.man opts = nil, argv = []
20
- path = File.join(File.dirname(__FILE__), "/../../../README.md")
20
+ path = File.join(File.dirname(__FILE__), "/../../../README.org")
21
21
  file = File.open(path, "r")
22
22
  contents = file.read
23
23
  puts contents
@@ -215,6 +215,7 @@ module Timeless
215
215
  to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
216
216
 
217
217
  entries = Timeless::Storage.get(from, to, opts.to_hash[:filter])
218
+
218
219
  # it could become a function of a reporting module
219
220
  printf "%-18s %-5s - %-5s %-8s %-s\n", "Day", "Start", "End", "Duration", "Notes"
220
221
  total = 0
@@ -248,42 +249,31 @@ module Timeless
248
249
  # {key1 => {value1 => total .. } , ... }
249
250
  hash = Timeless::Storage::balance from, to, opts.to_hash[:filter]
250
251
 
251
- total = 0
252
- puts "Projects"
253
- puts "========"
254
- hash["p"].each do |key, value|
255
- printf "%-20s%5s\n", key, in_hours(value)
256
- total += value
257
- end
258
- puts "-------------------------"
259
- printf "%-20s%5s\n", "Total", in_hours(total)
252
+ report "Projects", "p", hash
253
+ report "Clients", "c", hash
254
+ report "Activities", "a", hash
260
255
 
261
- total = 0
262
- puts ""
263
- puts "Clients"
264
- puts "======="
265
- hash["c"].each do |key, value|
266
- printf "%-20s%5s\n", key, in_hours(value)
267
- total += value
256
+ remaining_tags = hash.reject! { |k| ["p", "c", "a"].include? k }
257
+ remaining_tags.keys.each do |k|
258
+ report k, k, remaining_tags
268
259
  end
269
- puts "-------------------------"
270
- printf "%-20s%5s\n", "Total", in_hours(total)
260
+ end
261
+
262
+ private
271
263
 
264
+ def self.report title, key, hash
272
265
  total = 0
273
266
  puts ""
274
- puts "Activities"
275
- puts "=========="
276
- hash["a"].each do |key, value|
277
- printf "%-20s%5s\n", key, in_hours(value)
278
- total += value
267
+ puts title
268
+ puts "=" * title.size
269
+ (hash[key] || []).each do |k, v|
270
+ printf "%-20s%5s\n", k, in_hours(v)
271
+ total += v
279
272
  end
280
273
  puts "-------------------------"
281
274
  printf "%-20s%5s\n", "Total", in_hours(total)
282
275
  end
283
276
 
284
-
285
- private
286
-
287
277
  def self.in_hours value
288
278
  sprintf "%02i:%02i", value / 60, value % 60
289
279
  end
@@ -372,7 +372,12 @@ DESCRIPTION
372
372
  a project "project_1", using "p:project_1"
373
373
 
374
374
  EXAMPLES
375
+ # extract all entries which have the string p:project_1 in the notes field
376
+ # (that is, all entries related to project_1)
375
377
  timeless statement --filter p:project_1
378
+
379
+ # extract all entries which have the word documentation in the notes field
380
+ timeless statement --filter documentation
376
381
  EOS
377
382
  return { statement: [opts, :statement, help] }
378
383
  end
@@ -407,5 +412,36 @@ EXAMPLES
407
412
  EOS
408
413
  return { balance: [opts, :balance, help] }
409
414
  end
415
+
416
+ def self.export_opts
417
+ opts = Slop::Options.new
418
+ opts.banner = "export -- export timeless.csv to CSV"
419
+ help = <<EOS
420
+ NAME
421
+ #{opts.banner}
422
+
423
+ SYNOPSYS
424
+ #{opts.to_s}
425
+
426
+ DESCRIPTION
427
+ Print data to CSV, with the following columns:
428
+
429
+ - Start Date
430
+ - Start Time
431
+ - End Date
432
+ - End Time
433
+ - Duration in seconds
434
+ - First project found in notes
435
+ - First activity found in notes
436
+ - First client found in notes
437
+ - Notes attached to entry
438
+
439
+ EXAMPLES
440
+ timeless export
441
+ timeless export > data.csv
442
+ EOS
443
+ return { export: [opts, :export, help] }
444
+ end
445
+
410
446
  end
411
447
  end
@@ -11,11 +11,11 @@ module Timeless
11
11
  end
12
12
 
13
13
  def self.last
14
- CSV.read(TIMELESS_FILE).sort { |x, y| x[1] <=> y[1] }.last
14
+ my_read(TIMELESS_FILE).sort { |x, y| x[1] <=> y[1] }.last
15
15
  end
16
16
 
17
17
  def self.get from_date=nil, to_date=nil, filter=nil
18
- entries = CSV.read(TIMELESS_FILE)
18
+ entries = my_read(TIMELESS_FILE)
19
19
  entries.select do |x|
20
20
  (from_date ? Time.parse(x[0]) >= from_date : true) and
21
21
  (to_date ? Time.parse(x[1]) <= to_date : true) and
@@ -23,70 +23,85 @@ module Timeless
23
23
  end
24
24
  end
25
25
 
26
- # get all unique keys of a given type in file
27
- def self.get_key key
28
- entries = CSV.read(TIMELESS_FILE)
29
- entries.map { |x| extract_kpv key, x[2] }.uniq.sort
26
+ # get all unique tags in file
27
+ def self.get_tags
28
+ tags_regexp = /([a-zA-Z][0-9a-zA-Z_]*):([^ ]+)/
29
+
30
+ entries = my_read(TIMELESS_FILE)
31
+ entries.map { |x|
32
+ # match returns only the first match, while scan does it for the string
33
+ # scan returns an array of array ("a:b c:d".scan(tags_regexp) => [["a", "b"], ["c", "d"]]
34
+ (x[2] || "").scan(tags_regexp).map { |x| x[0] }
35
+ }.flatten.uniq
36
+ end
37
+
38
+ # get all unique keys of a given tag in file
39
+ def self.get_key tag
40
+ entries = my_read(TIMELESS_FILE)
41
+ entries.map { |x| extract_kpv tag, x[2] }.flatten.uniq.sort
30
42
  end
31
43
 
32
44
  def self.balance from, to, filter
33
- entries = Timeless::Storage.get(from, to, filter)
34
-
35
- hash = {"p" => {}, "c" => {}, "a" => {}}
45
+ # hash of all keys defined in file
46
+ array = get_tags
47
+ hash = Hash[array.collect { |item| [item, {}] } ]
36
48
 
49
+ entries = Timeless::Storage.get(from, to, filter)
37
50
  entries.each do |entry|
38
51
  start = Time.parse(entry[0])
39
52
  stop = Time.parse(entry[1])
40
53
  duration = (stop - start) / 60
41
54
 
42
- project = extract_kpv "p", entry[2]
43
- client = extract_kpv "c", entry[2]
44
- activity = extract_kpv "a", entry[2]
45
-
46
- hash["p"][project] = (hash["p"][project] || 0) + duration if project != ""
47
- hash["c"][client] = (hash["c"][client] || 0) + duration if client != ""
48
- hash["a"][activity] = (hash["a"][activity] || 0) + duration if activity != ""
49
-
55
+ hash.keys.each do |key|
56
+ values = extract_kpv key, entry[2]
57
+ values.each do |value|
58
+ hash[key][value] = (hash[key][value] || 0) + duration
59
+ end
60
+ end
50
61
  end
51
62
  hash
52
63
  end
53
-
54
-
55
64
  def self.export
56
- entries = CSV.read(TIMELESS_FILE)
65
+ entries = my_read(TIMELESS_FILE)
57
66
 
58
67
  CSV { |csvout| csvout << ["Start Date", "Start Time", "End Date", "End Time", "Duration (s)", "Project", "Client", "Activity", "Notes"] }
59
68
  CSV do |csvout|
60
69
  entries.each do |entry|
61
- start = Time.parse(entry[0])
62
- stop = Time.parse(entry[1])
63
-
64
- start_date = start.strftime("%Y-%m-%d")
65
- start_time = start.strftime("%H:%M:%S")
66
-
67
- stop_date = stop.strftime("%Y-%m-%d")
68
- stop_time = stop.strftime("%H:%M:%S")
69
-
70
- duration = stop - start
71
-
72
- # extract project and client, if present
73
- project = extract_kpv "p", entry[2]
74
- client = extract_kpv "c", entry[2]
75
- activity = extract_kpv "a", entry[2]
76
-
77
- notes = entry[2]
78
-
79
- csvout << [start_date, start_time, stop_date, stop_time, duration, project, client, activity, notes]
70
+ if entry[0] and entry[1]
71
+ start = Time.parse(entry[0])
72
+ stop = Time.parse(entry[1])
73
+
74
+ start_date = start.strftime("%Y-%m-%d")
75
+ start_time = start.strftime("%H:%M:%S")
76
+
77
+ stop_date = stop.strftime("%Y-%m-%d")
78
+ stop_time = stop.strftime("%H:%M:%S")
79
+
80
+ duration = stop - start
81
+
82
+ # extract first project, first client, and first activity, if present
83
+ project = extract_kpv("p", entry[2]).first
84
+ client = extract_kpv("c", entry[2]).first
85
+ activity = extract_kpv("a", entry[2]).first
86
+
87
+ notes = entry[2]
88
+
89
+ csvout << [start_date, start_time, stop_date, stop_time, duration, project, client, activity, notes]
90
+ end
80
91
  end
81
92
  end
82
93
  end
83
94
 
84
95
  private
85
96
 
86
- # return the value of a key-pair value in string or "" if not present
97
+ # read the timeless file into a CSV skipping lines with empty start end stop time
98
+ def self.my_read timeless_file
99
+ CSV.read(timeless_file).select { |x| x[0] != nil and x[1] != nil }
100
+ end
101
+
102
+ # return all the values of a given key in string
87
103
  def self.extract_kpv key, string
88
- match = string.match(/#{key}:([^ ]+)/)
89
- match ? match[1] : ""
104
+ (string || "").scan(/(#{key}):([^ ]+)/).map { |x| x[1] }
90
105
  end
91
106
  end
92
107
  end
@@ -1,3 +1,3 @@
1
1
  module Timeless
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timeless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adolfo Villafiorita
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-30 00:00:00.000000000 Z
11
+ date: 2020-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -107,7 +107,7 @@ files:
107
107
  - ".gitignore"
108
108
  - Gemfile
109
109
  - LICENSE.txt
110
- - README.md
110
+ - README.org
111
111
  - Rakefile
112
112
  - bin/console
113
113
  - bin/setup
data/README.md DELETED
@@ -1,152 +0,0 @@
1
- Timeless
2
- ========
3
-
4
- Timeless is a simple command line time tracker.
5
-
6
- Installation
7
- ------------
8
-
9
- Add this line to your application's Gemfile:
10
-
11
- ruby
12
- gem 'timeless'
13
-
14
- And then execute:
15
-
16
- $ bundle
17
-
18
- Or install it yourself as:
19
-
20
- $ gem install timeless
21
-
22
- Usage
23
- -----
24
-
25
- timeless is a simple command line time tracker. Entries store the
26
- following information:
27
-
28
- 1. start time
29
- 2. end time
30
- 3. notes
31
-
32
- timeless allows one to
33
-
34
- - run a pomodoro timer
35
- - clock time using a "stopwatch" (with start and stop commands)
36
- - manually entering data
37
- - print a text report, possibly filtered by date ranges of notes'
38
- content
39
- - export to csv
40
-
41
- Timeless has also some basic support for key-pair values in the notes
42
- form. They can be used to assign a special meaning to some strings and
43
- improve filtering and exporting. Two special keys are `p` for projects
44
- and `c` for clients. These two keys are exported in dedicated columns
45
- when exporting data to csv.
46
-
47
- Some non nominal conditions are also handled (e.g., starting a clock
48
- twice, trying to stop when no clock was started).
49
-
50
- If you do not want to type `timeless` every time, you can invoke a console:
51
-
52
- timeless console
53
- timeless:000> start
54
- timeless:001> stop
55
- timeless:002>
56
-
57
- More information with:
58
-
59
- timeless help # get list of available commands
60
- timeless help command # help about command
61
- timeless man # output this README file
62
-
63
-
64
- Examples
65
- --------
66
-
67
- Get information about command syntax:
68
-
69
- timeless -h
70
-
71
- Run a pomodoro timer:
72
-
73
- timeless pom p:project meeting with c:john
74
- timeless pom --duration 60 p:prj2
75
- timeless pom --long # default to 50 minutes
76
-
77
- Start clocking using a stopwatch:
78
-
79
- timeless start requirements doc
80
- timeless start --at 'thirty minutes ago' fixing bug 182 for c:tim
81
- timeless start --force --at 'five minutes ago' requirements document for p:prj1
82
-
83
- Stop clocking:
84
-
85
- timeless stop
86
- timeless stop forgot notes on start
87
- timeless stop --last # reuse the notes of last entry
88
- timeless stop --at 'five minutes ago'
89
- timeless stop --start '1 hour ago' --at 'now' clocked a full entry
90
-
91
- Enter a full entry:
92
-
93
- timeless clock activity on p:project # from end of last entry to now
94
- timeless clock --start 'three hours ago' --stop 'five minutes ago'
95
-
96
- What was i doing?
97
-
98
- timeless current # print current entry
99
- timeless forget # forget pending clock
100
-
101
- What did I do?
102
-
103
- timeless last # print last entry
104
-
105
- Reporting and exporting
106
-
107
- timeless statement
108
- timeless balance --from yesterday --filter p:project
109
- timeless export
110
-
111
- Notice that even though `timeless` uses CSV as its native format, the
112
- export command exports to a CSV file which is more easily parsed by a
113
- Spreadsheet such as LibreOffice.
114
-
115
- If something goes wrong
116
- -----------------------
117
-
118
- Data is stored in the CSV file `~/.timeless.csv`. You can edit the file
119
- to manually fix entries if you make some mistake.
120
-
121
- Version History
122
- ---------------
123
-
124
- **0.4.0**
125
-
126
- - second public release
127
- - new `balance` command
128
- - the `report` command has been renamed `statement`
129
- - interactive `console`
130
- - more detailed help for commands
131
- - fixed a bug in the last command: now it returns the last activity in
132
- chronological order (rather than the last activity which has been clocked)
133
-
134
- **0.2.0**
135
- - initial public release
136
-
137
-
138
-
139
- License
140
- -------
141
-
142
- Licensed under the terms of the MIT License.
143
-
144
- Contributing
145
- ------------
146
-
147
- 1. Fork it ( https://github.com/\[my-github-username\]/timeless/fork )
148
- 2. Create your feature branch (\`git checkout -b my-new-feature\`)
149
- 3. Commit your changes (\`git commit -am 'Add some feature'\`)
150
- 4. Push to the branch (\`git push origin my-new-feature\`)
151
- 5. Create a new Pull Request
152
-