timeless 0.1.0 → 0.4.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 +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
|