public
Description: Command line interface to tracking time on Basecamp.
Homepage: http://github.com/Klondike/basecamper
Clone URL: git://github.com/klondike/basecamper.git
basecamper / bin / track
100755 278 lines (224 sloc) 7.387 kb
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/usr/bin/ruby
 
require 'rubygems'
require 'basecamper'
 
 
USAGE = {
  "configure" => ["track configure [url username password ssl? [project-name]]"],
  "set" => ["track set [key value]"],
  "project" => ["track project project-name"],
  "projects" => ["track projects"],
  "log" => ["track log start-time end-time message [project-name]", "track log duration message [project-name]"],
  "undo" => ["track undo [todo-id]"],
  "status" => ["track status"],
  "times" => ["track times"],
  "help" => ["track help [subcommand]"],
  "start" => ["track start [project-name]", "track start start-time [project-name]"],
  "stop" => ["track stop [end-time] message"],
  "cancel" => ["track cancel"],
  "pause" => ["track pause"],
}
COMMANDS = USAGE.keys
 
 
# commands
 
def configure
  configuration! if ARGV.empty?
  
  url = ARGV.shift
  user_name = ARGV.shift
  password = ARGV.shift
  use_ssl = (ARGV.shift == "true")
  
  tracker.configure!(url, user_name, password, use_ssl)
  configuration!
end
 
def project
  usage! if ARGV.empty?
  tracker.set!("current_project", project_name(ARGV.shift))
  puts "Set current project to #{tracker.current_project}."
end
 
def projects
  puts "Projects:"
  tracker.projects.values.map {|v| v.capitalize_all}.sort.each {|name| puts " #{name}"}
end
 
def set
  variables! if ARGV.empty?
  usage! if ARGV[1].blank?
  
  exceptions = {"ssl" => "use_ssl", "username" => "user_name"}
  key, value = [ARGV.shift, ARGV.shift]
  
  tracker.set!(exceptions[key] || key, value)
  puts "Set #{key} to #{value}."
end
 
def log
  usage! if ARGV[0].blank? or ARGV[1].blank?
  
  if ARGV[0].to_time and ARGV[1].to_time
    start_time = ARGV.shift.to_time
    end_time = ARGV.shift.to_time
    # for example, if the times given are "10:00" and "1:30", assume the meridian changed
    if start_time > end_time
      if start_time - end_time < (12*60*60)
        end_time += (12*60*60)
      elsif start_time - end_time < (24*60*60)
        end_time += (24*60*60)
      end
    end
    
    duration = ":#{end_time.minutes_since(start_time).round_to(tracker.config.rounding.to_i)}"
  else
    duration = ARGV.shift
  end
  
  message = ARGV.shift
  project = project_name(ARGV.shift)
  
  if time = tracker.log_time(duration, message, project)
    puts "Logged time:"
    puts display(time)
  else
    error! "Couldn't log time; make sure the project name is spelled right."
  end
end
 
def start
  if tracker.started?
    puts "Already tracking time."
    status
    exit
  elsif tracker.paused?
    return pause
  end
  
  start_time = nil
  project = nil
  if ARGV.any?
    start_time = ARGV.shift if ARGV[0].to_time
    project = project_name(ARGV.shift) if ARGV.any?
    tracker.set!("current_project", project) if project
  end
  
  error! "No project specified. Specify a project name as the last argument, or set a default project in config.yml." unless tracker.current_project
  
  tracker.start!(start_time)
  puts "Started tracking time for #{tracker.current_project} at #{tracker.start_time.strftime("%I:%M%p").downcase}."
end
 
def cancel
  error! "Tracker not started. Use the 'log' command to log a complete time." unless tracker.started? or tracker.paused?
  tracker.cancel!
  puts "Canceled time tracking for #{tracker.current_project}."
end
 
def pause
  if tracker.paused?
    tracker.start!
    puts "Resumed tracking time at #{tracker.start_time.strftime("%I:%M%p").downcase} with #{tracker.minutes_elapsed} minutes elapsed."
  elsif tracker.started?
    tracker.pause!
    puts "Paused tracking time with #{tracker.minutes_elapsed} minutes elapsed."
  else
    error! "Tracker not started."
  end
end
 
def stop
  usage! if ARGV.empty?
  error! "Tracker not started. Use the 'log' command to log a complete time." unless tracker.started?
  
  end_time = ARGV.shift if ARGV[0].to_time
  message = ARGV.shift
  usage! if message.blank?
  
  error! "No project specified. Specify a project name as the last argument, or set a default project in config.yml." unless tracker.current_project
  
  time = tracker.stop!(message, end_time)
  
  if time
    puts "Logged time:"
    puts display(time)
  else
    error! "Couldn't log time; make sure the project name is spelled right."
  end
end
 
def times
  if tracker.times.empty?
    puts "No times recorded today."
    return
  end
  
  puts "Today's times:"
  
  projects = tracker.times.map {|time| time.project_id}.uniq
  sums = {}
  projects.each do |id|
    sums[id] = tracker.times.select {|time| time.project_id == id}.map {|time| time.hours.to_f}.sum
  end
  total_sum = sums.values.sum
  
  tracker.times.reverse_each {|time| puts display(time)}
  puts
  puts "Totals:"
  projects.each {|id| puts " #{tracker.project_name(id)}: #{sums[id]} hours"}
  puts
  puts " All projects: #{total_sum} hours"
end
 
def undo
  error! "No times recorded." unless tracker.times.any?
  
  time = tracker.delete_time(ARGV.shift)
  puts "Deleted time:"
  puts display(time)
end
 
def status
  if tracker.configured?
    puts "Tracker configured correctly, Basecamp communication online."
  else
    puts "Tracker not configured correctly, Basecamp communication offline."
  end
  
  puts "Current project: #{tracker.current_project || "[No project set.]"}"
  
  if tracker.started?
    puts "\nTracking started:\n #{tracker.start_time.strftime("%I:%M%p")} (#{tracker.minutes_elapsed} minutes elapsed)"
  elsif tracker.paused?
    puts "\nTracking paused:\n #{tracker.minutes_elapsed} minutes elapsed."
  else
    puts "\nNot currently tracking time."
  end
end
 
def help
  usage!(ARGV.shift) if ARGV[0].in?(COMMANDS)
  
  puts "usage: track <subcommand> [args]"
  puts "help: track help <subcommand>"
  puts "\nProject names can be a beginning fragment."
  puts "\nAvailable subcommands:"
  COMMANDS.sort.each {|command| puts " #{command}"}
end
 
 
# helper
 
def usage!(command = nil)
  puts "Usage:"
  USAGE[command || @command].each {|msg| puts " #{msg}"}
  puts
  exit
end
 
def error!(msg = nil)
  puts msg if msg
  puts
  exit
end
 
def display(time)
  " #{time.created_at.strftime("%I:%M%p").downcase} ##{time.id} [#{tracker.project_name(time.project_id)}] #{time.hours} - #{time.description}"
end
 
def tracker
  @tracker ||= Basecamper.new
end
 
def project_name(fragment)
  return if fragment.blank? or tracker.projects.nil?
  tracker.projects.values.find {|name| name == fragment or name =~ /^#{fragment}/i}
end
 
def variables!
  puts "Variables:"
  puts " url\t\tBasecamp URL. (e.g. thoughtbot.clientsection.com)"
  puts " username\tBasecamp username."
  puts " password\tBasecamp password."
  puts " ssl\t\tWhether your Basecamp uses SSL (https)."
  puts " rounding\tRound time to the nearest ___ minutes. Set to 0 to disable."
  puts
  exit
end
 
def configuration!
  puts "Configuration:"
  puts " url\t\t#{tracker.config.url}"
  puts " username\t#{tracker.config.user_name}"
  puts " password\t#{tracker.config.password}"
  puts " ssl\t\t#{tracker.config.use_ssl}"
  puts " rounding\t#{tracker.config.rounding}"
  puts
  exit
end
 
def unconfigured!
  puts
  puts "Tracker not configured correctly, cannot communicate with Basecamp."
  puts
  exit
end
 
 
@command = ARGV[0].in?(COMMANDS) ? ARGV.shift : 'help'
 
requires_connection = COMMANDS.reject {|c| c.in? ["help", "set", "configure"]}
unconfigured! if @command.in?(requires_connection) and !tracker.configured?
 
puts
self.send(@command)
puts