1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
|
require 'yaml'
require 'rexml/document'
require 'net/http'
require 'net/https'
require 'chronic'
require 'trollop'
require 'highline/import'
require 'hcl/timesheet_resource'
require 'hcl/project'
require 'hcl/task'
require 'hcl/day_entry'
# Workaround for annoying SSL warning:
# >> warning: peer certificate won't be verified in this SSL session
# http://www.5dollarwhitebox.org/drupal/node/64
class Net::HTTP
alias_method :old_initialize, :initialize
def initialize(*args)
old_initialize(*args)
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
end
class HCl
VERSION_FILE = File.dirname(__FILE__) + '/../VERSION.yml'
SETTINGS_FILE = "#{ENV['HOME']}/.hcl_settings"
CONFIG_FILE = "#{ENV['HOME']}/.hcl_config"
class UnknownCommand < StandardError; end
def self.command *args
hcl = new.process_args(*args).run
end
def run
if @command
if respond_to? @command
result = send @command, *@args
if not result.nil?
if result.respond_to? :to_a
puts result.to_a.join(', ')
elsif result.respond_to? :to_s
puts result
end
end
else
raise UnknownCommand, "unrecognized command `#{@command}'"
end
else
show
end
end
def initialize
@version = YAML::load(File.read(VERSION_FILE))
read_config
read_settings
end
def version
[:major, :minor, :patch].map { |v| @version[v] }.join('.')
end
def process_args *args
version_string = version
Trollop::options(args) do
version "HCl #{version_string}"
stop_on %w[ show tasks set unset note add rm start stop ]
banner <<-EOM
HCl is a command-line client for manipulating Harvest time sheets.
Commands:
hcl show [date]
hcl tasks
hcl aliases
hcl set <key> <value ...>
hcl start <task> [msg]
hcl stop [msg]
hcl note <msg>
Examples:
$ hcl tasks
$ hcl start 1234 4567 this is my log message
$ hcl set task.mytask 1234 4567
$ hcl start mytask this is my next log message
$ hcl show yesterday
$ hcl show last tuesday
Options:
EOM
end
@command = args.shift
@args = args
self
end
def tasks
tasks = Task.all
if tasks.empty?
puts "No cached tasks. Run `hcl show' to populate the cache and try again."
else
tasks.each { |task| puts "#{task.project.id} #{task.id}\t#{task}" }
end
nil
end
def read_config
if File.exists? CONFIG_FILE
config = YAML::load File.read(CONFIG_FILE)
TimesheetResource.configure config
elsif File.exists? old_conf = File.dirname(__FILE__) + "/../hcl_conf.yml"
config = YAML::load File.read(old_conf)
TimesheetResource.configure config
write_config config
else
config = {}
puts "Please specify your Harvest credentials.\n"
config['login'] = ask("Email Address: ")
config['password'] = ask("Password: ") { |q| q.echo = false }
config['subdomain'] = ask("Subdomain: ")
TimesheetResource.configure config
write_config config
end
end
def write_config config
puts "Writing configuration to #{CONFIG_FILE}."
File.open(CONFIG_FILE, 'w') do |f|
f.write config.to_yaml
end
end
def read_settings
if File.exists? SETTINGS_FILE
@settings = YAML.load(File.read(SETTINGS_FILE))
else
@settings = {}
end
end
def write_settings
File.open(SETTINGS_FILE, 'w') do |f|
f.write @settings.to_yaml
end
nil
end
def set key = nil, *args
if key.nil?
@settings.each do |k, v|
puts "#{k}: #{v}"
end
else
value = args.join(' ')
@settings ||= {}
@settings[key] = value
write_settings
end
nil
end
def unset key
@settings.delete key
write_settings
end
def aliases
@settings.keys.select { |s| s =~ /^task\./ }.map { |s| s.slice(5..-1) }
end
def start *args
ident = args.shift
task_ids = if @settings.key? "task.#{ident}"
@settings["task.#{ident}"].split(/\s+/)
else
[ident, args.shift]
end
task = Task.find *task_ids
if task.nil?
puts "Unknown project/task alias, try one of the following: #{aliases.join(', ')}."
exit 1
end
task.start(*args)
puts "Started timer for #{task}."
end
def stop
entry = DayEntry.with_timer
if entry
entry.toggle
puts "Stopped #{entry}."
else
puts "No running timers found."
end
end
def note *args
message = args.join ' '
entry = DayEntry.with_timer
if entry
entry.append_note message
puts "Added note '#{message}' to #{entry}."
else
puts "No running timers found."
end
end
def show *args
date = args.empty? ? nil : Chronic.parse(args.join(' '))
total_hours = 0.0
DayEntry.all(date).each do |day|
running = day.running? ? '(running) ' : ''
puts "\t#{as_hours day.hours}\t#{running}#{day.project} #{day.notes}"[0..78]
total_hours = total_hours + day.hours.to_f
end
puts "\t" + '-' * 13
puts "\t#{as_hours total_hours}\ttotal"
end
# Convert from decimal to a string of the form HH:MM.
def as_hours hours
minutes = hours.to_f * 60.0
sprintf "%d:%02d", (minutes / 60).to_i, (minutes % 60).to_i
end
end
|