Skip to content

Instantly share code, notes, and snippets.

@pstaender
Created May 10, 2017 22:16
Show Gist options
  • Save pstaender/f977fad6d88b464a0991bfbbdf73446a to your computer and use it in GitHub Desktop.
Save pstaender/f977fad6d88b464a0991bfbbdf73446a to your computer and use it in GitHub Desktop.
Track your project time
#!/usr/bin/env ruby
class PunchCardError < StandardError; end
class PunchCard
SETTINGS_DIR = File.expand_path('~/.punchcard')
def initialize project
@project = project
find_or_make_settings_dir
find_or_make_file if @project
end
def start
if start_time && !end_time
puts("#{duration(start_time, timestamp)}\n#{@project}")
else
puts "'#{@project}' startet"
self.start_time = timestamp
end
end
def stop
if end_time
raise PunchCardError.new("'#{@project}' already stopped")
elsif start_time
puts "'#{@project}' stopped"
self.end_time = timestamp
else
puts "Nothing to stop"
end
end
def status
puts total
end
def details
puts @project+" (#{running_status})\n\n"
project_data.map do |line|
points = line_to_time_points(line)
if points
starttime = points[0]
endtime = points[1] || timestamp
puts duration(starttime, endtime)
end
end
puts "========\n#{total} (total)"
end
private
def running_status
start_time && !end_time ? 'running' : 'stopped'
end
def total
total = 0
project_data.map do |line|
points = line_to_time_points(line)
if points
starttime = points[0]
endtime = points[1] || timestamp
total += endtime - starttime
end
end
total
humanize_duration total
end
def duration starttime, endtime
if starttime
humanize_duration endtime - starttime
else
humanize_duration 0
end
end
def humanize_duration duration
hours = duration / (60 * 60)
minutes = (duration / 60) % 60
seconds = duration % 60
"#{decimal_digits(hours)}:#{decimal_digits(minutes)}:#{decimal_digits(seconds)}"
end
def decimal_digits digit
if digit.to_i < 10
"0#{digit}"
else
digit.to_s
end
end
def start_time
time_points ? time_points[0] : nil
end
def start_time= time
append_new_line time
end
def end_time= time
replace_last_line "#{start_time}-#{time}\n"
end
def end_time
time_points ? time_points[1] : nil
end
def time_points
line_to_time_points last_entry
end
def line_to_time_points line
matches = line.match(/^(\d+)((\-)(\d+))*$/)
matches ? [matches[1].to_i, matches[4] ? matches[4].to_i : nil] : nil
end
def last_entry
project_data.last
end
def timestamp
Time.now.to_i
end
def project_data
File.open(project_file).each_line.map { |line| line.strip }
end
def write_to_project_file! data
File.open(project_file, 'w') { |f| f.write(data) }
end
def append_new_line line
open(project_file, 'a') { |f| f.puts(line) }
end
def replace_last_line line
data = project_data
data[-1] = line
write_to_project_file! data.join("\n")
end
def project_file
SETTINGS_DIR + "/#{sanitize_filename(@project)}"
end
def find_or_make_file
write_to_project_file!(@project+"\n") unless File.exists?(project_file)
end
def find_or_make_settings_dir
Dir.mkdir(SETTINGS_DIR) unless File.exists?(SETTINGS_DIR)
end
def sanitize_filename name
name.downcase.gsub(/^.*(\\|\/)/, '').gsub(/[^0-9a-z.\-]/, '_')
end
end
## CLI
$available_actions = %w(start stop list status details)
def exit_with_error! msg
STDERR.puts msg
exit 1
end
def usage
"Usage: #{__FILE__} 'Name of my project' (#{$available_actions.join('|')})"
end
if ARGV.first == 'list'
selected_action = 'list'
project_name = nil
else
project_name = ARGV.first
selected_action = ARGV[1]
end
if selected_action && $available_actions.include?(selected_action)
exit_with_error!("1st argument has to be the project name, e.g.:\n#{usage}") if !project_name && selected_action != 'list'
punch_card = PunchCard.new project_name
begin
punch_card.send selected_action.to_s
rescue PunchCardError => e
exit_with_error! "Error: #{e.message}"
end
else
exit_with_error! "Unrecognized action #{selected_action || ''}\n#{usage}"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment