timecard 0.0.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/CHANGELOG +5 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +30 -0
- data/Rakefile +35 -0
- data/VERSION +1 -0
- data/bin/timecard +5 -0
- data/lib/timecard.rb +151 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/timecard_spec.rb +7 -0
- metadata +84 -0
data/CHANGELOG
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Gabe da Silveira
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
= Timecard
|
2
|
+
|
3
|
+
This is a simple CLI time tracker. You start it, you type in a task and it displays a running timer which can be
|
4
|
+
paused. When you are finished it will output a summary of tasks with individual time segments broken down.
|
5
|
+
|
6
|
+
== Installation
|
7
|
+
|
8
|
+
gem install timecard
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
|
12
|
+
The a binary utility is installed with the gem, simply call:
|
13
|
+
|
14
|
+
timecard
|
15
|
+
|
16
|
+
And you're off to the races. Note that the interactive text goes to STDERR while the final summary goes to STDOUT, so
|
17
|
+
you can redirect to a log for future reference:
|
18
|
+
|
19
|
+
timecard > time.log
|
20
|
+
|
21
|
+
== TODO
|
22
|
+
|
23
|
+
* Separate the state machine and core functionality from the CLI interface so it can be plugged into a web UI or
|
24
|
+
something that works on Windows.
|
25
|
+
* Write some meaningful specs. Obviously this will be much easier when CLI concerns aren't woven throughout the core
|
26
|
+
logic.
|
27
|
+
|
28
|
+
== Copyright
|
29
|
+
|
30
|
+
Copyright (c) 2011 Gabe da Silveira. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'jeweler'
|
2
|
+
|
3
|
+
Jeweler::Tasks.new do |gem|
|
4
|
+
gem.name = "timecard"
|
5
|
+
gem.homepage = "http://github.com/dasil003/timecard"
|
6
|
+
gem.license = "MIT"
|
7
|
+
gem.summary = "Simple CLI time tracker"
|
8
|
+
gem.description = "Simple UNIX shell time tracker"
|
9
|
+
gem.email = "gabe@websaviour.com"
|
10
|
+
gem.authors = ["Gabe da Silveira"]
|
11
|
+
|
12
|
+
gem.bindir = 'bin'
|
13
|
+
|
14
|
+
gem.add_development_dependency "rspec", "~> 2.3.0"
|
15
|
+
gem.add_development_dependency "jeweler", "~> 1.6.3"
|
16
|
+
end
|
17
|
+
Jeweler::RubygemsDotOrgTasks.new
|
18
|
+
|
19
|
+
require 'rspec/core'
|
20
|
+
require 'rspec/core/rake_task'
|
21
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
22
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
23
|
+
end
|
24
|
+
|
25
|
+
task :default => :spec
|
26
|
+
|
27
|
+
require 'rake/rdoctask'
|
28
|
+
Rake::RDocTask.new do |rdoc|
|
29
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
30
|
+
|
31
|
+
rdoc.rdoc_dir = 'rdoc'
|
32
|
+
rdoc.title = "timecard #{version}"
|
33
|
+
rdoc.rdoc_files.include('README*')
|
34
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
35
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/timecard
ADDED
data/lib/timecard.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
module Timecard
|
2
|
+
DEBUG = false
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def invoke
|
6
|
+
$stderr.puts "=== Welcome to Ruby Timecard! ==="
|
7
|
+
|
8
|
+
@tasks = []
|
9
|
+
@new_input = nil
|
10
|
+
|
11
|
+
Signal.trap("INT") do
|
12
|
+
dump_tasks
|
13
|
+
exit(0)
|
14
|
+
end
|
15
|
+
|
16
|
+
state_loop(:ask_task)
|
17
|
+
end
|
18
|
+
|
19
|
+
def state_loop(state)
|
20
|
+
loop do
|
21
|
+
$stderr.puts "Entering state #{state}..." if DEBUG
|
22
|
+
state = send(state)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def ask_task
|
27
|
+
$stderr.print "What will you work on now? "
|
28
|
+
|
29
|
+
@task_name = $stdin.readline.chomp
|
30
|
+
@tasks << {:name => @task_name, :time_pairs => []}
|
31
|
+
|
32
|
+
:time_loop
|
33
|
+
end
|
34
|
+
|
35
|
+
def time_loop
|
36
|
+
@start_time = @last_time = Time.now
|
37
|
+
|
38
|
+
await_input
|
39
|
+
|
40
|
+
loop do
|
41
|
+
if (Time.now - @last_time) > 5.0
|
42
|
+
$stderr.puts "#{clearline}Detected Sleep Auto-Pausing for #{format_seconds(Time.now - @last_time)}"
|
43
|
+
@tasks.last[:time_pairs] << [@start_time, @last_time]
|
44
|
+
@start_time = Time.now
|
45
|
+
end
|
46
|
+
|
47
|
+
next_state = process_input { |input| time_loop_transition(input) }
|
48
|
+
return next_state if next_state
|
49
|
+
|
50
|
+
elapsed = Time.now - @start_time
|
51
|
+
$stderr.print "#{clearline}*#{format_seconds(sum_time_pairs(@tasks.last) + elapsed)} * #{@task_name} (p: pause f: next task q: finish & quit)"
|
52
|
+
@last_time = Time.now
|
53
|
+
sleep(0.5)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def time_loop_transition(command)
|
58
|
+
@tasks.last[:time_pairs] << [@start_time, Time.now]
|
59
|
+
@start_time = nil
|
60
|
+
|
61
|
+
case command
|
62
|
+
when 'p'
|
63
|
+
:pause_task
|
64
|
+
when 'f'
|
65
|
+
:finish_task
|
66
|
+
when 'q'
|
67
|
+
:finish_and_quit
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def pause_task
|
72
|
+
$stderr.print "#{clearline}*#{format_seconds(sum_time_pairs(@tasks.last))} * #{@task_name} (paused, any key to resume)"
|
73
|
+
|
74
|
+
await_input
|
75
|
+
|
76
|
+
loop do
|
77
|
+
next_state = process_input { |input| :time_loop }
|
78
|
+
return next_state if next_state
|
79
|
+
|
80
|
+
sleep(0.5)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def finish_task
|
85
|
+
$stderr.print clearline
|
86
|
+
$stderr.puts ">#{format_seconds(sum_time_pairs(@tasks.last))} #{@tasks.last[:name]}"
|
87
|
+
return :ask_task
|
88
|
+
end
|
89
|
+
|
90
|
+
def finish_and_quit
|
91
|
+
$stderr.print clearline
|
92
|
+
$stderr.puts ">#{format_seconds(sum_time_pairs(@tasks.last))} #{@tasks.last[:name]}"
|
93
|
+
dump_tasks
|
94
|
+
exit(0)
|
95
|
+
end
|
96
|
+
|
97
|
+
def dump_tasks
|
98
|
+
if @tasks.any?
|
99
|
+
puts "\n"
|
100
|
+
i = 0
|
101
|
+
@tasks.each do |task|
|
102
|
+
i += 1
|
103
|
+
puts "Task ##{i}: #{task[:name]} #{format_seconds(sum_time_pairs(task))}"
|
104
|
+
task[:time_pairs].each { |pair| puts " " + pair.inspect + " " + format_seconds(pair.last - pair.first) }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def format_seconds(seconds)
|
112
|
+
seconds = seconds.to_i
|
113
|
+
minutes = seconds / 60
|
114
|
+
seconds = seconds % 60
|
115
|
+
hours = minutes / 60
|
116
|
+
minutes = minutes % 60
|
117
|
+
sprintf("%2d:%02d:%02d", hours, minutes, seconds)
|
118
|
+
end
|
119
|
+
|
120
|
+
def await_input
|
121
|
+
Thread.start do
|
122
|
+
@new_input = get_char
|
123
|
+
$stdin.flush
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_char
|
128
|
+
system("stty raw -echo") #=> Raw mode, no echo
|
129
|
+
char = $stdin.getc
|
130
|
+
system("stty -raw echo") #=> Reset terminal mode
|
131
|
+
Process.kill("INT", $$) if char == "\u0003"
|
132
|
+
char
|
133
|
+
end
|
134
|
+
|
135
|
+
def clearline
|
136
|
+
"\r\e[0K"
|
137
|
+
end
|
138
|
+
|
139
|
+
def sum_time_pairs(task)
|
140
|
+
task[:time_pairs].inject(0){ |sum,p| sum + (p.last - p.first) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def process_input
|
144
|
+
if @new_input
|
145
|
+
input = @new_input
|
146
|
+
@new_input = nil
|
147
|
+
yield(input)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'timecard'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: timecard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Gabe da Silveira
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-07-05 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.3.0
|
24
|
+
type: :development
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: jeweler
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.6.3
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id002
|
37
|
+
description: Simple UNIX shell time tracker
|
38
|
+
email: gabe@websaviour.com
|
39
|
+
executables:
|
40
|
+
- timecard
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- LICENSE.txt
|
45
|
+
- README.rdoc
|
46
|
+
files:
|
47
|
+
- CHANGELOG
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.rdoc
|
50
|
+
- Rakefile
|
51
|
+
- VERSION
|
52
|
+
- bin/timecard
|
53
|
+
- lib/timecard.rb
|
54
|
+
- spec/spec_helper.rb
|
55
|
+
- spec/timecard_spec.rb
|
56
|
+
homepage: http://github.com/dasil003/timecard
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.7.2
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: Simple CLI time tracker
|
83
|
+
test_files: []
|
84
|
+
|