Created
May 10, 2017 22:16
-
-
Save pstaender/f977fad6d88b464a0991bfbbdf73446a to your computer and use it in GitHub Desktop.
Track your project time
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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