timeless 0.5.1 → 0.6.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: 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
-