timetrap 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +23 -0
- data/README.md +183 -0
- data/Rakefile +60 -0
- data/VERSION.yml +4 -0
- data/bin/dev_t +4 -0
- data/bin/t +11 -0
- data/lib/timetrap/cli.rb +227 -0
- data/lib/timetrap/formatters/csv.rb +19 -0
- data/lib/timetrap/formatters/ical.rb +29 -0
- data/lib/timetrap/formatters/text.rb +55 -0
- data/lib/timetrap/helpers.rb +59 -0
- data/lib/timetrap/models.rb +80 -0
- data/lib/timetrap.rb +80 -0
- data/spec/timetrap_spec.rb +622 -0
- metadata +129 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
http://www.opensource.org/licenses/mit-license.php
|
2
|
+
|
3
|
+
The MIT License
|
4
|
+
|
5
|
+
Copyright (c) 2009 Sam Goldstein
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
15
|
+
all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
Timetrap
|
2
|
+
========
|
3
|
+
|
4
|
+
Timetrap is a utility which provides an easy to use command line interface for
|
5
|
+
tracking what you spend your time on. It is a ruby port of Trevor Caira's
|
6
|
+
Timebook, a small python utility. It contains several enhancement over
|
7
|
+
Timebook notably the ability to parse natural language time strings. This
|
8
|
+
makes commands such as ``t in --at "30 minutes ago"`` possible. Timetrap is
|
9
|
+
also able to export entries to several formats (e.g. ical, csv) and is designed
|
10
|
+
to be easily extended to support additional export formats.
|
11
|
+
Timetrap maintains its state in a sqlite3 database.
|
12
|
+
|
13
|
+
To install:
|
14
|
+
|
15
|
+
$ gem sources -a http://gems.github.com (you only have to do this once)
|
16
|
+
$ sudo gem install samg-timetrap
|
17
|
+
|
18
|
+
This will place a ``t`` executable in your path.
|
19
|
+
|
20
|
+
Original Timebook available at:
|
21
|
+
http://bitbucket.org/trevor/timebook/src/
|
22
|
+
|
23
|
+
|
24
|
+
Concepts
|
25
|
+
--------
|
26
|
+
|
27
|
+
Timetrap maintains a list of *timesheets* -- distinct lists of timed *periods*.
|
28
|
+
Each period has a start and end time, with the exception of the most recent
|
29
|
+
period, which may have no end time set. This indicates that this period is
|
30
|
+
still running. Timesheets containing such periods are considered *active*. It
|
31
|
+
is possible to have multiple timesheets active simultaneously, though a single
|
32
|
+
time sheet may only have one period running at once.
|
33
|
+
|
34
|
+
Interactions with timetrap are performed through the ``t`` command on the
|
35
|
+
command line. ``t`` is followed by one of timetrap's subcommands. Often used
|
36
|
+
subcommands include ``in``, ``out``, ``switch``, ``now``, ``list`` and
|
37
|
+
``display``. Commands may be abbreviated as long as they are unambiguous: thus
|
38
|
+
``t switch foo`` and ``t s foo`` are identical. With the default command set,
|
39
|
+
no two commands share the first same letter, thus it is only necessary to type
|
40
|
+
the first letter of a command. Likewise, commands which display timesheets
|
41
|
+
accept abbreviated timesheet names. ``t display f`` is thus equivalent to ``t
|
42
|
+
display foo`` if ``foo`` is the only timesheet which begins with "f". Note that
|
43
|
+
this does not apply to ``t switch``, since this command also creates
|
44
|
+
timesheets. (Using the earlier example, if ``t switch f`` is entered, it would
|
45
|
+
thus be ambiguous whether a new timesheet ``f`` or switching to the existing
|
46
|
+
timesheet ``foo`` was desired).
|
47
|
+
|
48
|
+
Usage
|
49
|
+
-----
|
50
|
+
|
51
|
+
The basic usage is as follows:
|
52
|
+
|
53
|
+
$ t switch writing
|
54
|
+
$ t in document timetrap --at "10 minutes ago"
|
55
|
+
$ t out
|
56
|
+
|
57
|
+
The first command, ``t switch writing``, switches to the timesheet "writing"
|
58
|
+
(or creates it if it does not exist). ``t in document timetrap --at "10 minutes
|
59
|
+
ago"`` creates a new period in the current timesheet, and annotates it with the
|
60
|
+
description "document timetrap". The optional ``--at`` flag can be passed to start
|
61
|
+
the entry at a time other than the present. The ``--at`` flag is able to parse
|
62
|
+
natural language times (via Chronic: http://chronic.rubyforge.org/) and will
|
63
|
+
understand 'friday 13:00', 'mon 2:35', '4pm', etc. (also true of the ``edit``
|
64
|
+
command's ``--start`` and ``--end`` flags.) Note that this command would be in
|
65
|
+
error if the ``writing`` timesheet was already active. Finally, ``t out``
|
66
|
+
records the current time as the end time for the most recent period in the
|
67
|
+
``writing`` timesheet.
|
68
|
+
|
69
|
+
To display the current timesheet, invoke the ``t display`` command::
|
70
|
+
|
71
|
+
$ t display
|
72
|
+
Timesheet: timetrap
|
73
|
+
Day Start End Duration Notes
|
74
|
+
Mon Apr 13, 2009 15:46:51 - 17:03:50 1:16:59 improved display functionality
|
75
|
+
17:25:59 - 17:26:02 0:00:03
|
76
|
+
18:38:07 - 18:38:52 0:00:45 working on list
|
77
|
+
22:37:38 - 23:38:43 1:01:05 work on kill
|
78
|
+
2:18:52
|
79
|
+
Tue Apr 14, 2009 00:41:16 - 01:40:19 0:59:03 gem packaging
|
80
|
+
10:20:00 - 10:48:10 0:28:10 enhance edit
|
81
|
+
1:27:13
|
82
|
+
---------------------------------------------------------
|
83
|
+
Total 3:46:05
|
84
|
+
|
85
|
+
Each period in the timesheet is listed on a row. If the timesheet is active,
|
86
|
+
the final period in the timesheet will have no end time. After each day, the
|
87
|
+
total time tracked in the timesheet for that day is listed. Note that this is
|
88
|
+
computed by summing the durations of the periods beginning in the day. In the
|
89
|
+
last row, the total time tracked in the timesheet is shown.
|
90
|
+
|
91
|
+
Commands
|
92
|
+
--------
|
93
|
+
**archives**
|
94
|
+
Archives the selected entries (by moving them to a sheet called ``_[SHEET]``)
|
95
|
+
These entries can be seen by running ``t display _[SHEET]``.
|
96
|
+
usage: ``t archive [--start DATE] [--end DATE] [SHEET]``
|
97
|
+
|
98
|
+
**backend**
|
99
|
+
Run an interactive database session on the timetrap database. Requires the
|
100
|
+
sqlite3 command.
|
101
|
+
|
102
|
+
usage: ``t backend``
|
103
|
+
|
104
|
+
**display**
|
105
|
+
Display a given timesheet. If no timesheet is specified, show the current
|
106
|
+
timesheet. If ``all`` is passed as SHEET display all timesheets. Accepts
|
107
|
+
an optional ``--ids`` flag which will include the entries' ids in the output.
|
108
|
+
This is useful when editing an non running entry with ``edit``.
|
109
|
+
|
110
|
+
Display is designed to support a variety of export formats that can be
|
111
|
+
specified by passing the ``--format`` flag. This currently defaults to
|
112
|
+
text. iCal and csv output are also supported.
|
113
|
+
|
114
|
+
Display also allows the use of a ``--round`` or ``-r`` flag which will round
|
115
|
+
all times to 15 minute increments. See global options below.
|
116
|
+
|
117
|
+
usage: ``t display [--ids] [--round] [--start DATE] [--end DATE] [--format FMT] [SHEET | all]``
|
118
|
+
|
119
|
+
**edit**
|
120
|
+
Inserts a note associated with the an entry in the timesheet, or edits the
|
121
|
+
start or end times. Defaults to the current time although an ``--id`` flag can
|
122
|
+
be passed with the entry's id (see display.)
|
123
|
+
|
124
|
+
usage: ``t edit [--id ID] [--start TIME] [--end TIME] [NOTES]``
|
125
|
+
|
126
|
+
**format**
|
127
|
+
Deprecated
|
128
|
+
Alias for display
|
129
|
+
|
130
|
+
**in**
|
131
|
+
Start the timer for the current timesheet. Must be called before out. Notes
|
132
|
+
may be specified for this period. This is exactly equivalent to
|
133
|
+
``t in; t edit NOTES``. Accepts an optional --at flag.
|
134
|
+
|
135
|
+
usage: ``t in [--at TIME] [NOTES]``
|
136
|
+
|
137
|
+
**kill**
|
138
|
+
Delete a timesheet or an entry. Entry's are referenced using an ``--id``
|
139
|
+
flag (see display). Sheets are referenced by name.
|
140
|
+
|
141
|
+
usage: ``t kill [--id ID] [TIMESHEET]``
|
142
|
+
|
143
|
+
**list**
|
144
|
+
List the available timesheets.
|
145
|
+
|
146
|
+
usage: ``t list``
|
147
|
+
|
148
|
+
**now**
|
149
|
+
Print the current sheet, whether it's active, and if so, how long it has been
|
150
|
+
active and what notes are associated with the current period.
|
151
|
+
|
152
|
+
usage: ``t now``
|
153
|
+
|
154
|
+
**out**
|
155
|
+
Stop the timer for the current timesheet. Must be called after in. Accepts an
|
156
|
+
optional --at flag.
|
157
|
+
|
158
|
+
usage: ``t out [--at TIME]``
|
159
|
+
|
160
|
+
**running**
|
161
|
+
Print all active sheets and any messages associated with them.
|
162
|
+
|
163
|
+
usage: ``t running``
|
164
|
+
|
165
|
+
**switch**
|
166
|
+
Switch to a new timesheet. this causes all future operation (except switch)
|
167
|
+
to operate on that timesheet. The default timesheet is called "default".
|
168
|
+
|
169
|
+
usage: ``t switch TIMESHEET``
|
170
|
+
|
171
|
+
**week**
|
172
|
+
Shortcut for display with start date set to monday of this week
|
173
|
+
|
174
|
+
usage: ``t week [--ids] [--end DATE] [--format FMT] [SHEET | all]``
|
175
|
+
|
176
|
+
Global Options
|
177
|
+
--------
|
178
|
+
|
179
|
+
**rounding**
|
180
|
+
passing a ``--round`` or ``-r`` flag to any command will round entry start
|
181
|
+
and end times to the closest 15 minute increment. This flag only affects the
|
182
|
+
display commands (e.g. display, list, week, etc.) and is non-destructive.
|
183
|
+
The actual start and end time stored by Timetrap are unaffected.
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec/rake/spectask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
task :default => :spec
|
6
|
+
|
7
|
+
desc "Run all specs in spec directory"
|
8
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
9
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::RDocTask.new do |rd|
|
13
|
+
rd.main = "README"
|
14
|
+
rd.rdoc_dir = 'doc'
|
15
|
+
rd.rdoc_files.include("README", "**/*.rb")
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |s|
|
21
|
+
s.name = %q{timetrap}
|
22
|
+
s.version = "0.1.2"
|
23
|
+
|
24
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
25
|
+
s.authors = ["Sam Goldstein"]
|
26
|
+
s.date = %q{2009-04-14}
|
27
|
+
s.description = %q{Command line time tracker}
|
28
|
+
s.email = %q{sgrock@gmail.com}
|
29
|
+
s.has_rdoc = true
|
30
|
+
s.homepage = "http://github.com/samg/timetrap/tree/master"
|
31
|
+
s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
s.bindir = "bin"
|
34
|
+
s.executables = ['t']
|
35
|
+
s.summary = %q{Command line time tracker}
|
36
|
+
s.add_dependency("sequel", ">= 2.12.0")
|
37
|
+
s.add_dependency("chronic", ">= 0.2.3")
|
38
|
+
s.add_dependency("getopt-declare", ">= 1.28")
|
39
|
+
s.add_dependency("icalendar", ">= 1.1.0")
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 2
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
46
|
+
s.add_runtime_dependency(%q<mime-types>, [">= 1.15"])
|
47
|
+
s.add_runtime_dependency(%q<diff-lcs>, [">= 1.1.2"])
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<mime-types>, [">= 1.15"])
|
50
|
+
s.add_dependency(%q<diff-lcs>, [">= 1.1.2"])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<mime-types>, [">= 1.15"])
|
54
|
+
s.add_dependency(%q<diff-lcs>, [">= 1.1.2"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue LoadError
|
58
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
59
|
+
end
|
60
|
+
|
data/VERSION.yml
ADDED
data/bin/dev_t
ADDED
data/bin/t
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
begin
|
3
|
+
require 'timetrap'
|
4
|
+
rescue LoadError
|
5
|
+
if File.symlink? __FILE__
|
6
|
+
require File.dirname(File.readlink(__FILE__)) + '/../lib/timetrap'
|
7
|
+
else
|
8
|
+
require File.dirname(__FILE__) + '/../lib/timetrap'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
Timetrap::CLI.invoke
|
data/lib/timetrap/cli.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
module Timetrap
|
2
|
+
module CLI
|
3
|
+
extend Helpers
|
4
|
+
attr_accessor :args
|
5
|
+
extend self
|
6
|
+
|
7
|
+
USAGE = <<-EOF
|
8
|
+
|
9
|
+
Timetrap - Simple Time Tracking
|
10
|
+
|
11
|
+
Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]
|
12
|
+
|
13
|
+
where COMMAND is one of:
|
14
|
+
* archive - move entries to a hidden sheet (by default named '_[SHEET]') so
|
15
|
+
they're out of the way.
|
16
|
+
usage: t archive [--start DATE] [--end DATE] [SHEET]
|
17
|
+
-s, --start <date:qs> Include entries that start on this date or later
|
18
|
+
-e, --end <date:qs> Include entries that start on this date or earlier
|
19
|
+
* backend - open an sqlite shell to the database
|
20
|
+
usage: t backend
|
21
|
+
* display - display the current timesheet or a specific. Pass `all' as
|
22
|
+
SHEET to display all sheets.
|
23
|
+
usage: t display [--ids] [--start DATE] [--end DATE] [--format FMT] [SHEET | all]
|
24
|
+
-v, --ids Print database ids (for use with edit)
|
25
|
+
-s, --start <date:qs> Include entries that start on this date or later
|
26
|
+
-e, --end <date:qs> Include entries that start on this date or earlier
|
27
|
+
-f, --format <format> The output format. Currently supports ical, csv, and
|
28
|
+
text (default).
|
29
|
+
* edit - alter an entry's note, start, or end time. Defaults to the active entry
|
30
|
+
usage: t edit [--id ID] [--start TIME] [--end TIME] [NOTES]
|
31
|
+
-i, --id <id:i> Alter entry with id <id> instead of the running entry
|
32
|
+
-s, --start <time:qs> Change the start time to <time>
|
33
|
+
-e, --end <time:qs> Change the end time to <time>
|
34
|
+
* format - deprecated: alias for display
|
35
|
+
* in - start the timer for the current timesheet
|
36
|
+
usage: t in [--at TIME] [NOTES]
|
37
|
+
-a, --at <time:qs> Use this time instead of now
|
38
|
+
* kill - delete a timesheet
|
39
|
+
usage: t kill [--id ID] [TIMESHEET]
|
40
|
+
-i, --id <id:i> Alter entry with id <id> instead of the running entry
|
41
|
+
* list - show the available timesheets
|
42
|
+
usage: t list
|
43
|
+
* now - show the status of the current timesheet
|
44
|
+
usage: t now
|
45
|
+
* out - stop the timer for the current timesheet
|
46
|
+
usage: t out [--at TIME]
|
47
|
+
-a, --at <time:qs> Use this time instead of now
|
48
|
+
* running - show all running timesheets
|
49
|
+
usage: t running
|
50
|
+
* switch - switch to a new timesheet
|
51
|
+
usage: t switch TIMESHEET
|
52
|
+
* week - shortcut for display with start date set to monday of this week
|
53
|
+
usage: t week [--ids] [--end DATE] [--format FMT] [SHEET | all]
|
54
|
+
|
55
|
+
OTHER OPTIONS
|
56
|
+
-h, --help Display this help
|
57
|
+
-r, --round Round output to 15 minute start and end times.
|
58
|
+
EOF
|
59
|
+
|
60
|
+
def parse arguments
|
61
|
+
args.parse arguments
|
62
|
+
end
|
63
|
+
|
64
|
+
def invoke
|
65
|
+
args['-h'] ? say(USAGE) : invoke_command_if_valid
|
66
|
+
end
|
67
|
+
|
68
|
+
def commands
|
69
|
+
Timetrap::CLI::USAGE.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
|
70
|
+
end
|
71
|
+
|
72
|
+
def say *something
|
73
|
+
puts *something
|
74
|
+
end
|
75
|
+
|
76
|
+
def invoke_command_if_valid
|
77
|
+
command = args.unused.shift
|
78
|
+
set_global_options
|
79
|
+
case (valid = commands.select{|name| name =~ %r|^#{command}|}).size
|
80
|
+
when 0 then say "Invalid command: #{command}"
|
81
|
+
when 1 then send valid[0]
|
82
|
+
else
|
83
|
+
say "Ambiguous command: #{command}" if command
|
84
|
+
say(USAGE)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# currently just sets whether output should be rounded to 15 min intervals
|
89
|
+
def set_global_options
|
90
|
+
Timetrap::Entry.round = true if args['-r']
|
91
|
+
end
|
92
|
+
|
93
|
+
def archive
|
94
|
+
ee = selected_entries
|
95
|
+
out = "Archive #{ee.count} entries? "
|
96
|
+
print out
|
97
|
+
if $stdin.gets =~ /\Aye?s?\Z/i
|
98
|
+
ee.all.each do |e|
|
99
|
+
next unless e.end
|
100
|
+
e.update :sheet => "_#{e.sheet}"
|
101
|
+
end
|
102
|
+
else
|
103
|
+
say "archive aborted!"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def edit
|
108
|
+
entry = args['-i'] ? Entry[args['-i']] : Timetrap.active_entry
|
109
|
+
say "can't find entry" && return unless entry
|
110
|
+
entry.update :start => args['-s'] if args['-s'] =~ /.+/
|
111
|
+
entry.update :end => args['-e'] if args['-e'] =~ /.+/
|
112
|
+
entry.update :note => unused_args if unused_args =~ /.+/
|
113
|
+
end
|
114
|
+
|
115
|
+
def backend
|
116
|
+
exec "sqlite3 #{DB_NAME}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def in
|
120
|
+
Timetrap.start unused_args, args['-a']
|
121
|
+
end
|
122
|
+
|
123
|
+
def out
|
124
|
+
Timetrap.stop args['-a']
|
125
|
+
end
|
126
|
+
|
127
|
+
def kill
|
128
|
+
if e = Entry[args['-i']]
|
129
|
+
out = "are you sure you want to delete entry #{e.id}? "
|
130
|
+
out << "(#{e.note}) " if e.note.to_s =~ /.+/
|
131
|
+
print out
|
132
|
+
if $stdin.gets =~ /\Aye?s?\Z/i
|
133
|
+
e.destroy
|
134
|
+
say "it's dead"
|
135
|
+
else
|
136
|
+
say "will not kill"
|
137
|
+
end
|
138
|
+
elsif (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet = unused_args)
|
139
|
+
victims = Entry.filter(:sheet => sheet).count
|
140
|
+
print "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
|
141
|
+
if $stdin.gets =~ /\Aye?s?\Z/i
|
142
|
+
Timetrap.kill_sheet sheet
|
143
|
+
say "killed #{victims} entries"
|
144
|
+
else
|
145
|
+
say "will not kill"
|
146
|
+
end
|
147
|
+
else
|
148
|
+
victim = args['-i'] ? args['-i'].to_s.inspect : sheet.inspect
|
149
|
+
say "can't find #{victim} to kill", 'sheets:', *sheets
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def display
|
154
|
+
begin
|
155
|
+
fmt_klass = if args['-f']
|
156
|
+
Timetrap::Formatters.const_get("#{args['-f'].classify}")
|
157
|
+
else
|
158
|
+
Timetrap::Formatters::Text
|
159
|
+
end
|
160
|
+
rescue
|
161
|
+
say "Invalid format specified `#{args['-f']}'"
|
162
|
+
return
|
163
|
+
end
|
164
|
+
say Timetrap.format(fmt_klass, selected_entries.order(:start).all)
|
165
|
+
end
|
166
|
+
alias_method :format, :display
|
167
|
+
|
168
|
+
|
169
|
+
def switch
|
170
|
+
sheet = unused_args
|
171
|
+
if not sheet =~ /.+/ then say "No sheet specified"; return end
|
172
|
+
say "Switching to sheet " + Timetrap.switch(sheet)
|
173
|
+
end
|
174
|
+
|
175
|
+
def list
|
176
|
+
sheets = Entry.sheets.map do |sheet|
|
177
|
+
sheet_atts = {:total => 0, :running => 0, :today => 0}
|
178
|
+
Timetrap::Entry.filter(:sheet => sheet).inject(sheet_atts) do |m, e|
|
179
|
+
e_end = e.end_or_now
|
180
|
+
m[:name] ||= sheet
|
181
|
+
m[:total] += (e_end.to_i - e.start.to_i)
|
182
|
+
m[:running] += (e_end.to_i - e.start.to_i) unless e.end
|
183
|
+
m[:today] += (e_end.to_i - e.start.to_i) if same_day?(Time.now, e.start)
|
184
|
+
m
|
185
|
+
end
|
186
|
+
end
|
187
|
+
if sheets.empty? then say "No sheets found"; return end
|
188
|
+
width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
|
189
|
+
say " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
|
190
|
+
sheets.each do |sheet|
|
191
|
+
star = sheet[:name] == Timetrap.current_sheet ? '*' : ' '
|
192
|
+
say "#{star}%-#{width}s%-12s%-12s%s" % [
|
193
|
+
sheet[:running],
|
194
|
+
sheet[:today],
|
195
|
+
sheet[:total]
|
196
|
+
].map(&method(:format_seconds)).unshift(sheet[:name])
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def now
|
201
|
+
if Timetrap.running?
|
202
|
+
out = "#{Timetrap.current_sheet}: #{format_duration(Timetrap.active_entry.start, Timetrap.active_entry.end_or_now)}".gsub(/ /, ' ')
|
203
|
+
out << " (#{Timetrap.active_entry.note})" if Timetrap.active_entry.note =~ /.+/
|
204
|
+
say out
|
205
|
+
else
|
206
|
+
say "#{Timetrap.current_sheet}: not running"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def running
|
211
|
+
say "Running Timesheets:"
|
212
|
+
say Timetrap::Entry.filter(:end => nil).map{|e| " #{e.sheet}: #{e.note}"}.uniq.sort
|
213
|
+
end
|
214
|
+
|
215
|
+
def week
|
216
|
+
args['-s'] = Date.today.wday == 1 ? Date.today.to_s : Date.parse(Chronic.parse(%q(last monday)).to_s).to_s
|
217
|
+
display
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def unused_args
|
223
|
+
args.unused.join(' ')
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Timetrap
|
2
|
+
module Formatters
|
3
|
+
class Csv
|
4
|
+
attr_reader :output
|
5
|
+
|
6
|
+
def initialize entries
|
7
|
+
@output = entries.inject("start,end,note\n") do |out, e|
|
8
|
+
next(out) unless e.end
|
9
|
+
out << %|"#{e.start.strftime(time_format)}","#{e.end.strftime(time_format)}","#{e.note}"\n|
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def time_format
|
15
|
+
"%Y-%m-%d %H:%M:%S"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'icalendar'
|
2
|
+
require 'date'
|
3
|
+
module Timetrap
|
4
|
+
module Formatters
|
5
|
+
class Ical
|
6
|
+
include Icalendar
|
7
|
+
def calendar
|
8
|
+
@calendar ||= Calendar.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def output
|
12
|
+
calendar.to_ical
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize entries
|
16
|
+
entries.each do |e|
|
17
|
+
next unless e.end
|
18
|
+
calendar.event do
|
19
|
+
dtstart DateTime.parse(e.start.to_s)
|
20
|
+
dtend DateTime.parse(e.end.to_s)
|
21
|
+
summary e.note
|
22
|
+
description e.note
|
23
|
+
end
|
24
|
+
end
|
25
|
+
calendar.publish
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Timetrap
|
2
|
+
module Formatters
|
3
|
+
class Text
|
4
|
+
attr_accessor :output
|
5
|
+
include Timetrap::Helpers
|
6
|
+
|
7
|
+
def initialize entries
|
8
|
+
self.output = ''
|
9
|
+
sheets = entries.inject({}) do |h, e|
|
10
|
+
h[e.sheet] ||= []
|
11
|
+
h[e.sheet] << e
|
12
|
+
h
|
13
|
+
end
|
14
|
+
(sheet_names = sheets.keys.sort).each do |sheet|
|
15
|
+
self.output << "Timesheet: #{sheet}\n"
|
16
|
+
id_heading = Timetrap::CLI.args['-v'] ? 'Id' : ' '
|
17
|
+
self.output << "#{id_heading} Day Start End Duration Notes\n"
|
18
|
+
last_start = nil
|
19
|
+
from_current_day = []
|
20
|
+
sheets[sheet].each_with_index do |e, i|
|
21
|
+
from_current_day << e
|
22
|
+
e_end = e.end_or_now
|
23
|
+
self.output << "%-4s%16s%11s -%9s%10s %s\n" % [
|
24
|
+
(Timetrap::CLI.args['-v'] ? e.id : ''),
|
25
|
+
format_date_if_new(e.start, last_start),
|
26
|
+
format_time(e.start),
|
27
|
+
format_time(e.end),
|
28
|
+
format_duration(e.start, e_end),
|
29
|
+
e.note
|
30
|
+
]
|
31
|
+
|
32
|
+
nxt = sheets[sheet].map[i+1]
|
33
|
+
if nxt == nil or !same_day?(e.start, nxt.start)
|
34
|
+
self.output << "%52s\n" % format_total(from_current_day)
|
35
|
+
from_current_day = []
|
36
|
+
else
|
37
|
+
end
|
38
|
+
last_start = e.start
|
39
|
+
end
|
40
|
+
self.output << <<-OUT
|
41
|
+
---------------------------------------------------------
|
42
|
+
OUT
|
43
|
+
self.output << " Total%43s\n" % format_total(sheets[sheet])
|
44
|
+
self.output << "\n" unless sheet == sheet_names.last
|
45
|
+
end
|
46
|
+
if sheets.size > 1
|
47
|
+
self.output << <<-OUT
|
48
|
+
-------------------------------------------------------------
|
49
|
+
OUT
|
50
|
+
self.output << "Grand Total%41s\n" % format_total(sheets.values.flatten)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Timetrap
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
def selected_entries
|
5
|
+
ee = if (sheet = sheet_name_from_string(unused_args)) == 'all'
|
6
|
+
Timetrap::Entry.filter('sheet not like ? escape "!"', '!_%')
|
7
|
+
elsif sheet =~ /.+/
|
8
|
+
Timetrap::Entry.filter('sheet = ?', sheet)
|
9
|
+
else
|
10
|
+
Timetrap::Entry.filter('sheet = ?', Timetrap.current_sheet)
|
11
|
+
end
|
12
|
+
ee = ee.filter(:start >= Date.parse(args['-s'])) if args['-s']
|
13
|
+
ee = ee.filter(:start <= Date.parse(args['-e']) + 1) if args['-e']
|
14
|
+
ee
|
15
|
+
end
|
16
|
+
|
17
|
+
def format_time time
|
18
|
+
return '' unless time.respond_to?(:strftime)
|
19
|
+
time.strftime('%H:%M:%S')
|
20
|
+
end
|
21
|
+
|
22
|
+
def format_date time
|
23
|
+
return '' unless time.respond_to?(:strftime)
|
24
|
+
time.strftime('%a %b %d, %Y')
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_date_if_new time, last_time
|
28
|
+
return '' unless time.respond_to?(:strftime)
|
29
|
+
same_day?(time, last_time) ? '' : format_date(time)
|
30
|
+
end
|
31
|
+
|
32
|
+
def same_day? time, other_time
|
33
|
+
format_date(time) == format_date(other_time)
|
34
|
+
end
|
35
|
+
|
36
|
+
def format_duration stime, etime
|
37
|
+
return '' unless stime and etime
|
38
|
+
secs = etime.to_i - stime.to_i
|
39
|
+
format_seconds secs
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_seconds secs
|
43
|
+
"%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_total entries
|
47
|
+
secs = entries.inject(0){|m, e|e_end = e.end_or_now; m += e_end.to_i - e.start.to_i if e_end && e.start;m}
|
48
|
+
"%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
|
49
|
+
end
|
50
|
+
|
51
|
+
def sheet_name_from_string string
|
52
|
+
return "all" if string =~ /^\W*all\W*$/
|
53
|
+
return "" unless string =~ /.+/
|
54
|
+
DB[:entries].filter(:sheet.like("#{string}%")).first[:sheet]
|
55
|
+
rescue
|
56
|
+
""
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|