timeless 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -1
- data/LICENSE.txt +17 -18
- data/README.md +152 -0
- data/Rakefile +1 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/elisp/forms.el +22 -0
- data/exe/timeless +5 -0
- data/lib/timeless.rb +6 -5
- data/lib/timeless/cli/command_semantics.rb +352 -0
- data/lib/timeless/cli/command_syntax.rb +411 -0
- data/lib/timeless/pomodoro.rb +5 -8
- data/lib/timeless/storage.rb +27 -3
- data/lib/timeless/version.rb +1 -1
- data/timeless.gemspec +13 -7
- metadata +52 -26
- data/README.textile +0 -99
- data/bin/timeless +0 -386
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06ba9bc95d407ecb088dc4c016fc7a66c78417cf
|
4
|
+
data.tar.gz: 560ac4abb81ef3f80476a0b7baa154ff3e3257e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7b8f73506f3c399c5f17b0ce60620cfb31d63b09d87fb1bf89f3bf0babeb16cda0fae91d383405e9d848929837517e5567f186c5197c9b6cea2e49363c30a46
|
7
|
+
data.tar.gz: d44c950c14ba467675f0d0cb9c8f4a58b6ffd8ea93bba6329709944cc39aa4b9f3b45455f478a5aa39ec20c00d2cb17826e34dc5b70a2a23e235c002302f2dee
|
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,22 +1,21 @@
|
|
1
|
-
|
1
|
+
The MIT License (MIT)
|
2
2
|
|
3
|
-
|
3
|
+
Copyright (c) 2015-2017 Adolfo Villafiorita
|
4
4
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
the following conditions:
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
12
11
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
14
|
-
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
15
14
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
OF
|
22
|
-
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
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
|
+
|
data/Rakefile
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
-
|
2
|
+
task :default => :spec
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "timeless"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/elisp/forms.el
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
;; forms-mode for timeless -*- emacs-lisp -*-
|
2
|
+
|
3
|
+
(setq forms-file (expand-file-name "~/.timeless.csv"))
|
4
|
+
|
5
|
+
;; Use `forms-enumerate' to set field names and number thereof.
|
6
|
+
(setq forms-number-of-fields
|
7
|
+
(forms-enumerate
|
8
|
+
'(start-datetime
|
9
|
+
end-datetime
|
10
|
+
description)))
|
11
|
+
|
12
|
+
(setq forms-field-sep ",")
|
13
|
+
|
14
|
+
;; The format list.
|
15
|
+
(setq forms-format-list
|
16
|
+
(list
|
17
|
+
"====== Timeless Record ======\n\n"
|
18
|
+
"Start: " start-datetime "\n"
|
19
|
+
"End: " end-datetime "\n"
|
20
|
+
"Description: " description "\n"
|
21
|
+
))
|
22
|
+
|
data/exe/timeless
ADDED
data/lib/timeless.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require "timeless/version"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
require "timeless/storage"
|
4
|
+
require "timeless/stopwatch"
|
5
|
+
require "timeless/pomodoro"
|
6
|
+
|
7
|
+
require "timeless/cli/command_syntax"
|
8
|
+
require "timeless/cli/command_semantics"
|
@@ -0,0 +1,352 @@
|
|
1
|
+
require 'timeless'
|
2
|
+
|
3
|
+
require 'chronic'
|
4
|
+
require 'readline'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Timeless
|
8
|
+
module CommandSemantics
|
9
|
+
APPNAME = 'timeless'
|
10
|
+
VERSION = Timeless::VERSION
|
11
|
+
|
12
|
+
#
|
13
|
+
# Main App Starts Here!
|
14
|
+
#
|
15
|
+
def self.version opts = nil, argv = []
|
16
|
+
puts "#{APPNAME} version #{VERSION}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.man opts = nil, argv = []
|
20
|
+
path = File.join(File.dirname(__FILE__), "/../../../README.md")
|
21
|
+
file = File.open(path, "r")
|
22
|
+
contents = file.read
|
23
|
+
puts contents
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.help opts = nil, argv = []
|
27
|
+
all_commands = CommandSyntax.commands
|
28
|
+
|
29
|
+
if argv != []
|
30
|
+
argv.map { |x| puts all_commands[x.to_sym][2] }
|
31
|
+
else
|
32
|
+
puts "#{APPNAME} command [options] [args]"
|
33
|
+
puts ""
|
34
|
+
puts "Available commands:"
|
35
|
+
puts ""
|
36
|
+
all_commands.keys.each do |key|
|
37
|
+
puts " " + all_commands[key][0].banner
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.console opts, argv = []
|
43
|
+
all_commands = CommandSyntax.commands
|
44
|
+
all_commands.delete(:console)
|
45
|
+
|
46
|
+
i = 0
|
47
|
+
while true
|
48
|
+
string = Readline.readline("#{APPNAME}:%03d> " % i, true)
|
49
|
+
string.gsub!(/^#{APPNAME} /, "") # as a courtesy, remove any leading appname string
|
50
|
+
if string == "exit" or string == "quit" or string == "." then
|
51
|
+
exit 0
|
52
|
+
end
|
53
|
+
reps all_commands, string.split(' ')
|
54
|
+
i = i + 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# read-eval-print step
|
59
|
+
def self.reps all_commands, argv
|
60
|
+
if argv == [] or argv[0] == "--help" or argv[0] == "-h"
|
61
|
+
CommandSemantics.help
|
62
|
+
exit 0
|
63
|
+
else
|
64
|
+
command = argv[0]
|
65
|
+
syntax_and_semantics = all_commands[command.to_sym]
|
66
|
+
if syntax_and_semantics
|
67
|
+
opts = syntax_and_semantics[0]
|
68
|
+
function = syntax_and_semantics[1]
|
69
|
+
|
70
|
+
begin
|
71
|
+
parser = Slop::Parser.new(opts)
|
72
|
+
|
73
|
+
result = parser.parse(argv[1..-1])
|
74
|
+
options = result.to_hash
|
75
|
+
arguments = result.arguments
|
76
|
+
|
77
|
+
eval "CommandSemantics::#{function}(options, arguments)"
|
78
|
+
rescue Slop::Error => e
|
79
|
+
puts "#{APPNAME}: #{e}"
|
80
|
+
rescue Exception => e
|
81
|
+
puts e
|
82
|
+
end
|
83
|
+
else
|
84
|
+
puts "#{APPNAME}: '#{command}' is not a valid command. See '#{APPNAME} help'"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# APP SPECIFIC COMMANDS
|
91
|
+
#
|
92
|
+
def self.pom opts, argv
|
93
|
+
duration = opts.to_hash[:duration] ||
|
94
|
+
(opts.to_hash[:long] ? Timeless::Pomodoro::WORKING_LONG : Timeless::Pomodoro::WORKING)
|
95
|
+
|
96
|
+
start, stop, notes = Timeless::Pomodoro.run_pomodoro_timer(duration, argv.join(" "))
|
97
|
+
Timeless::Storage.store(start, stop, notes)
|
98
|
+
puts last_entry
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def self.start opts, argv
|
103
|
+
start = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
|
104
|
+
notes = argv.join(" ") # empty string is no args specified
|
105
|
+
force = opts.to_hash[:force]
|
106
|
+
|
107
|
+
if Timeless::Stopwatch.clocking? and not force
|
108
|
+
start, notes = Timeless::Stopwatch.get_start # for information purposes only
|
109
|
+
puts "There is a clock started at #{start} (notes: \"#{notes}\"). Use --force to override."
|
110
|
+
else
|
111
|
+
Timeless::Stopwatch.start(start, notes)
|
112
|
+
puts "Clock started at #{start ? start : Time.now}."
|
113
|
+
puts "You may want to specify notes for the entry when you stop clocking." if notes == ""
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.forget opts, argv
|
118
|
+
if File.exists?(Timeless::Stopwatch::START_FILENAME)
|
119
|
+
entry = Timeless::Storage.last
|
120
|
+
Timeless::Stopwatch.forget
|
121
|
+
puts "Forgotten timer started at #{entry[0]} (#{entry[2]})."
|
122
|
+
else
|
123
|
+
puts "You don't have an active timer"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.stop opts, argv
|
128
|
+
start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
|
129
|
+
stop = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
|
130
|
+
notes = argv.join(" ") # empty string if no args specified
|
131
|
+
|
132
|
+
manage_stop start, stop, notes, opts[:last]
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.clock opts, argv
|
136
|
+
start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
|
137
|
+
stop_opt = Chronic.parse(opts.to_hash[:stop]) # nil if no option specified
|
138
|
+
end_opt = Chronic.parse(opts.to_hash[:end]) # nil if no option specified
|
139
|
+
stop = stop_opt ? stop_opt : end_opt # stop_opt if --stop, end_opt if --end, nil otherwise
|
140
|
+
notes = argv.join(" ") # empty string if no args specified
|
141
|
+
|
142
|
+
if stop_opt and end_opt then
|
143
|
+
puts "Timeless error: specify end time with either --end or --stop (not both)"
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
manage_stop start, stop, notes, opts[:last]
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.current opts, argv
|
151
|
+
if Timeless::Stopwatch.clocking? then
|
152
|
+
start, notes = Timeless::Stopwatch.get_start
|
153
|
+
puts "Timeless: you have been clocking #{"%.0d" % ((Time.now- Time.parse(start)) / 60)} minutes"
|
154
|
+
puts "on: #{notes}" if notes
|
155
|
+
else
|
156
|
+
puts "There is no clock started."
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.last opts, argv
|
161
|
+
puts last_entry
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.list opts, argv
|
165
|
+
values = Timeless::Storage.get_key argv[0]
|
166
|
+
puts "Timeless: list of keys #{argv[0]} found in timesheets:"
|
167
|
+
values.each do |value|
|
168
|
+
puts value
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.export opts, argv
|
173
|
+
Timeless::Storage.export
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.gaps opts, argv
|
177
|
+
from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
|
178
|
+
to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
|
179
|
+
interval = opts.to_hash[:interval] || 1
|
180
|
+
|
181
|
+
entries = Timeless::Storage.get(from, to)
|
182
|
+
previous_stop = from || Time.parse(entries[0][0])
|
183
|
+
day = ""
|
184
|
+
|
185
|
+
printf "%-18s %-5s - %-5s %-6s %-s\n", "Day", "Start", "End", "Gap", "Command to fix"
|
186
|
+
entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
|
187
|
+
current_start = Time.parse(entry[0])
|
188
|
+
|
189
|
+
current_day = current_start.strftime("%a %b %d, %Y")
|
190
|
+
|
191
|
+
if (current_start.day == previous_stop.day and current_start - previous_stop > interval * 60) then
|
192
|
+
|
193
|
+
if current_day != day then
|
194
|
+
day = current_day
|
195
|
+
else
|
196
|
+
current_day = "" # avoid printing the day if same as before
|
197
|
+
end
|
198
|
+
|
199
|
+
duration = (current_start - previous_stop) / 60
|
200
|
+
printf "%-18s %5s - %5s %02i:%02i %s\n",
|
201
|
+
current_day,
|
202
|
+
previous_stop.strftime("%H:%M"),
|
203
|
+
current_start.strftime("%H:%M"),
|
204
|
+
duration / 60, duration % 60,
|
205
|
+
"timeless clock --start '#{previous_stop}' --end '#{current_start}'"
|
206
|
+
|
207
|
+
end
|
208
|
+
previous_stop = Time.parse(entry[1])
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
def self.statement opts, argv
|
214
|
+
from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
|
215
|
+
to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
|
216
|
+
|
217
|
+
entries = Timeless::Storage.get(from, to, opts.to_hash[:filter])
|
218
|
+
# it could become a function of a reporting module
|
219
|
+
printf "%-18s %-5s - %-5s %-8s %-s\n", "Day", "Start", "End", "Duration", "Notes"
|
220
|
+
total = 0
|
221
|
+
day = ""
|
222
|
+
entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
|
223
|
+
current_day = Time.parse(entry[0]).strftime("%a %b %d, %Y")
|
224
|
+
if current_day != day then
|
225
|
+
day = current_day
|
226
|
+
else
|
227
|
+
current_day = "" # avoid printing the day if same as before
|
228
|
+
end
|
229
|
+
|
230
|
+
duration = (Time.parse(entry[1]) - Time.parse(entry[0])) / 60
|
231
|
+
total = total + duration
|
232
|
+
|
233
|
+
printf "%-18s %5s - %5s %5s %s\n",
|
234
|
+
current_day,
|
235
|
+
Time.parse(entry[0]).strftime("%H:%M"),
|
236
|
+
Time.parse(entry[1]).strftime("%H:%M"),
|
237
|
+
in_hours(duration),
|
238
|
+
entry[2]
|
239
|
+
end
|
240
|
+
puts "----------------------------------------------------------------------"
|
241
|
+
printf "Total %02i:%02i\n", total / 60, total % 60
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.balance opts, argv
|
245
|
+
from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
|
246
|
+
to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
|
247
|
+
|
248
|
+
# {key1 => {value1 => total .. } , ... }
|
249
|
+
hash = Timeless::Storage::balance from, to, opts.to_hash[:filter]
|
250
|
+
|
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)
|
260
|
+
|
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
|
268
|
+
end
|
269
|
+
puts "-------------------------"
|
270
|
+
printf "%-20s%5s\n", "Total", in_hours(total)
|
271
|
+
|
272
|
+
total = 0
|
273
|
+
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
|
279
|
+
end
|
280
|
+
puts "-------------------------"
|
281
|
+
printf "%-20s%5s\n", "Total", in_hours(total)
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def self.in_hours value
|
288
|
+
sprintf "%02i:%02i", value / 60, value % 60
|
289
|
+
end
|
290
|
+
|
291
|
+
def self.last_entry
|
292
|
+
start, stop, notes = Timeless::Storage.last
|
293
|
+
|
294
|
+
interval = Time.parse(stop) - Time.parse(start)
|
295
|
+
seconds = interval % 60
|
296
|
+
minutes = (interval / 60) % 60
|
297
|
+
hours = interval / 3600
|
298
|
+
|
299
|
+
sprintf "Timeless: you clocked %02d:%02d:%02d on %s\nYou stopped clocking at: %s", hours, minutes, seconds, notes, stop
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# code shared by stop and clock (which accept slightly different options,
|
304
|
+
# but behave in the same way
|
305
|
+
#
|
306
|
+
def self.manage_stop start, stop, notes, reuse_last
|
307
|
+
# syntax check:
|
308
|
+
# - --start is illegal if there is a running clock
|
309
|
+
# - --last is illegal if there are notes
|
310
|
+
#
|
311
|
+
# however:
|
312
|
+
# - --last and notes prevail over notes stored when starting the clock
|
313
|
+
if Timeless::Stopwatch.clocking? and start
|
314
|
+
puts "Timeless error: you specified --start with a running clock. Use 'timeless forget' or drop --start"
|
315
|
+
end
|
316
|
+
if reuse_last and notes != "" then
|
317
|
+
puts "Timeless error: you specified both --last and notes. Choose one or the other"
|
318
|
+
end
|
319
|
+
|
320
|
+
if reuse_last then
|
321
|
+
_, _, notes = Timeless::Storage.last
|
322
|
+
end
|
323
|
+
|
324
|
+
if Timeless::Stopwatch.clocking?
|
325
|
+
start, stop, notes = Timeless::Stopwatch.stop(start, stop, notes) # merge passed with data in running clock
|
326
|
+
Timeless::Storage.store(start, stop, notes)
|
327
|
+
|
328
|
+
puts "Clock stopped at #{stop}."
|
329
|
+
puts last_entry
|
330
|
+
else
|
331
|
+
start = start ? start : Timeless::Storage.last[1]
|
332
|
+
stop = stop ? stop : Time.now
|
333
|
+
Timeless::Storage.store(start, stop, notes)
|
334
|
+
|
335
|
+
puts "Clock stopped at #{stop}."
|
336
|
+
puts last_entry
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# GENERAL UTILITIES
|
341
|
+
|
342
|
+
def self.backup filename
|
343
|
+
FileUtils::cp filename, filename + "~"
|
344
|
+
puts "Backup copy #{filename} created in #{filename}~."
|
345
|
+
end
|
346
|
+
|
347
|
+
def self.backup_and_write filename, content
|
348
|
+
backup(filename) if File.exist?(filename)
|
349
|
+
File.open(filename, "w") { |f| f.puts content }
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|