timetrap 1.1.1
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.
- 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
|