diff options
| author | felix <felix@seconddrawer.com.au> | 2011-05-23 11:36:35 +0000 |
|---|---|---|
| committer | felix <felix@seconddrawer.com.au> | 2011-05-23 11:36:35 +0000 |
| commit | 0e387a8724178e6ebe442d7cbc22e30e951ed505 (patch) | |
| tree | f702940ea34fb362f964c9fd99c598bb22633311 | |
| parent | a3ee431daeced139b315f09e580bcb5457de9dc9 (diff) | |
| download | timetrackr-0e387a8724178e6ebe442d7cbc22e30e951ed505.tar.gz timetrackr-0e387a8724178e6ebe442d7cbc22e30e951ed505.tar.bz2 | |
refactor, added json
| -rwxr-xr-x | bin/timetrackr | 162 | ||||
| -rw-r--r-- | lib/timetrackr.rb | 96 | ||||
| -rw-r--r-- | lib/timetrackr/cli.rb | 180 | ||||
| -rw-r--r-- | lib/timetrackr/database.rb | 103 | ||||
| -rw-r--r-- | lib/timetrackr/json.rb | 68 | ||||
| -rw-r--r-- | lib/timetrackr/period.rb | 15 | ||||
| -rw-r--r-- | lib/timetrackr/sqlite.rb | 103 | ||||
| -rw-r--r-- | lib/timetrackr/yaml.rb | 95 |
8 files changed, 473 insertions, 349 deletions
diff --git a/bin/timetrackr b/bin/timetrackr index 1a3c2ba..20d949f 100755 --- a/bin/timetrackr +++ b/bin/timetrackr @@ -1,168 +1,12 @@ #!/usr/bin/env ruby +# encoding: utf-8 lib_dir = File.dirname(__FILE__) + '/../lib' $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) require 'timetrackr' +require 'timetrackr/cli' -DEFAULTS = { - 'backend' => 'yaml', - 'verbose' => false, - 'single_task' => false, - 'path' => File.join(ENV['HOME'],'.timetrackr.db'), - 'relative_format' => "%2<hours>dh %2<minutes>dm %2<seconds>ds", - 'absolute_time' => "%H:%M", - 'absolute_day' => "%Y-%m-%d" -} - -def show_help - version = File.exist?('VERSION') ? File.read('VERSION') : "" - puts "timetrackr version #{version}" - puts <<HELP - - timetrackr [command] [options] - - Available commands: - - start [task] start a task - stop [task] stop a task (or 'all') - switch TASK switch tasks - time [task] show time for a task (or 'all') - log [task] show time log for a task (or 'all') - - Global options - -h --help show this help - -v --verbose be noisy -HELP -end - -def format_time(time, fmt_str) - hours = time.to_i/3600.to_i - minutes = (time/60 - hours * 60).to_i - seconds = (time - (minutes * 60 + hours * 3600)) - format(fmt_str,{ - :hours => hours, - :minutes => minutes, - :seconds => seconds}) -end - -config = {} -config_file = File.join(ENV['HOME'],'.timetrackrrc') -if File.exist?(config_file) - require 'yaml' - config = YAML.load_file(config_file) -end - -# global options -while (cmd = ARGV.shift) && cmd.start_with?('-') - if ['-v','--verbose'].include? cmd - config['verbose'] = true - end - if ['-h','--help'].include? cmd - show_help - exit 1 - end -end - -config = DEFAULTS.merge(config || {}) -$verbose = config['verbose'] -trackr = TimeTrackr.create(config['backend'], config) - -# -# commands -# -case cmd -when 'start','in','s' - task = ARGV.shift - notes = ARGV.join(' ') - # switch tasks if config says so - if config['single_task'] && trackr.current != task - trackr.current.each { |t| - trackr.stop(t) unless t == task - } - puts "Switched to task '#{task}'" if $verbose - else - puts "Started task '#{task}'" if $verbose - end - trackr.start(task, notes) - -when 'stop','out','kill','k' - if ARGV[0] == 'all' || ARGV[0].nil? - tasks = trackr.tasks - else - tasks = ARGV - end - tasks.each do |task| - trackr.stop(task) - puts "Stopped task '#{task}'" if $verbose - end - -when 'switch','sw' - task = ARGV.shift - notes = ARGV.join(' ') - trackr.current.each do |t| - trackr.stop(t) unless t == task - end - trackr.start(task, notes) - puts "Switched to task '#{task}'" if $verbose - -when 'time','status',nil - task = ARGV.shift - if task && trackr.tasks.include?(task) - tasks = [*task] - else - tasks = trackr.tasks.each - end - tasks.each do |task| - total = trackr.history(task).reduce(0){ |t, period| - t = t + period.length - } - name = trackr.current.include?(task) ? task+' *' : task - puts name.ljust(15) << format_time(total,config['relative_format']) - end - -when 'log' - if ARGV[0] == 'all' || ARGV[0].nil? - tasks = trackr.tasks - else - tasks = ARGV - end - table = [] - periods = tasks.each.collect{ |t| trackr.history(t) }.flatten - lastday = nil - table << periods.sort{|x,y| x.start <=> y.start}.collect{ |period| - currday = period.start.strftime(config['absolute_day']) - day = (currday == lastday) ? ' ' : currday - lastday = currday - name = period.current? ? period.task+' *' : period.task - start = period.start.strftime(config['absolute_time']) - stop = period.current? ? ' ' : period.stop.strftime(config['absolute_time']) - length = format_time(period.length, config['relative_format']) - notes = period.notes - "#{day.ljust(12)} #{name.ljust(15)} #{start} - #{stop.ljust(5)} #{length} #{notes}" - } - puts table - - -when 'clear','delete','del' - tasks = ARGV - tasks = trackr.tasks if task == 'all' - tasks.each do |task| - trackr.clear(task) - puts "Task '#{task}' cleared" if $verbose - end - -when 'rename','mv' - from = ARGV.shift - to = ARGV.shift - trackr.rename(from,to) - puts "Renamed '#{from}' to '#{to}'" if $verbose - -else - puts "'#{cmd}' is not a valid command" - show_help -end - -trackr.close +TimeTrackr::CLI.run(ARGV) # vim: set ts=2 sw=2 tw=80 ft=ruby fdm=syntax et fen : diff --git a/lib/timetrackr.rb b/lib/timetrackr.rb index ae3165b..7a5b17e 100644 --- a/lib/timetrackr.rb +++ b/lib/timetrackr.rb @@ -1,92 +1,10 @@ -autoload 'YamlTimeTrackr', 'timetrackr/yaml' -autoload 'SqliteTimeTrackr', 'timetrackr/sqlite' -autoload 'Period', 'timetrackr/period' +module TimeTrackr -class TimeTrackr - def self.create(type, options={}) - case type.to_s - when 'yaml' - begin - require 'yaml' - log = YamlTimeTrackr.new(options['path']) - puts 'Loaded yaml tracker' if $verbose - rescue LoadError - puts 'Yaml not found' - end - when 'sqlite' - begin - require 'sqlite3' - log = SqliteTimeTrackr.new(options['path']) - puts 'Loaded sqlite tracker' if $verbose - rescue LoadError - puts 'Sqlite not found' - end - else - raise "Bad log type: #{type}" - end - log - end - - # - # return an array of current tasks - # - def current - raise 'Not Implemented' - end - - # - # start a period with optional notes - # - def start(task,notes) - raise 'Not implemented' - end - - # - # stop a period - # - def stop(task) - raise 'Not implemented' - end - - # - # return an array of all tasks - # - def tasks - raise 'Not Implemented' - end - - # - # time in task in seconds - # - def time(task) - end - - # - # get task history as an array of Periods - # - def history(task) - raise 'Not Implemented' - end - - # - # rename a task - # - def rename(from, to) - raise 'Not implemented' - end - - # - # clear an task - # - def clear(task) - raise 'Not Implemented' - end - - # - # cleanup and close - # - def close - end + require 'time' + require 'timetrackr/database' + require 'timetrackr/period' + autoload 'YamlDatabase', 'timetrackr/yaml' + autoload 'SqliteDatabase', 'timetrackr/sqlite' + autoload 'JsonDatabase', 'timetrackr/json' end - diff --git a/lib/timetrackr/cli.rb b/lib/timetrackr/cli.rb new file mode 100644 index 0000000..80be593 --- /dev/null +++ b/lib/timetrackr/cli.rb @@ -0,0 +1,180 @@ +module TimeTrackr + class CLI + DEFAULTS = { + 'backend' => 'yaml', + 'verbose' => false, + 'single_task' => false, + 'path' => File.join(ENV['HOME'],'.timetrackr.db'), + 'relative_format' => "%2<hours>dh %2<minutes>dm %2<seconds>ds", + 'absolute_time' => "%H:%M", + 'absolute_day' => "%Y-%m-%d" + } + + # + # static method to get config file and run the tracker + # + def self.run(args) + config = {} + config_file = File.join(ENV['HOME'],'.timetrackrrc') + if File.exist?(config_file) + require 'yaml' + config = YAML.load_file(config_file) + end + + # global options + while (cmd = args.shift) && cmd.start_with?('-') + if ['-v','--verbose'].include? cmd + config['verbose'] = true + end + if ['-h','--help'].include? cmd + cmd = 'help' + end + end + config = DEFAULTS.merge(config || {}) + + cli = TimeTrackr::CLI.new(config) + cli.run(cmd, args) + end + + def initialize(config) + @config = config + @verbose = config['verbose'] + @trackr = TimeTrackr::Database.create(config['backend'], config) + end + + # + # run a command on the tracker + # + def run(cmd,args) + case cmd + when 'start','in','s' + task = args.shift + notes = args.join(' ') + # switch tasks if config says so + if @config['single_task'] && @trackr.current != task + @trackr.current.each { |t| + @trackr.stop(t) unless t == task + } + puts "Switched to task '#{task}'" if @verbose + else + puts "Started task '#{task}'" if @verbose + end + @trackr.start(task, notes) + + when 'stop','out','kill','k' + if args[0] == 'all' || args[0].nil? + tasks = @trackr.tasks + else + tasks = args + end + tasks.each do |task| + @trackr.stop(task) + puts "Stopped task '#{task}'" if @verbose + end + + when 'switch','sw' + task = args.shift + notes = args.join(' ') + @trackr.current.each do |t| + @trackr.stop(t) unless t == task + end + @trackr.start(task, notes) + puts "Switched to task '#{task}'" if @verbose + + when 'time','status',nil + task = args.shift + if task && @trackr.tasks.include?(task) + tasks = [*task] + else + tasks = @trackr.tasks.each + end + tasks.each do |task| + total = @trackr.history(task).reduce(0){ |t, period| + t = t + period.length + } + name = @trackr.current.include?(task) ? task+' *' : task + puts name.ljust(15) << format_time(total,@config['relative_format']) + end + + when 'log' + if args[0] == 'all' || args[0].nil? + tasks = @trackr.tasks + else + tasks = args + end + table = [] + periods = tasks.each.collect{ |t| @trackr.history(t) }.flatten + lastday = nil + table << periods.sort{|x,y| x.start <=> y.start}.collect{ |period| + currday = period.start.strftime(@config['absolute_day']) + day = (currday == lastday) ? ' ' : currday + lastday = currday + name = period.current? ? period.task+' *' : period.task + start = period.start.strftime(@config['absolute_time']) + stop = period.current? ? ' ' : period.stop.strftime(@config['absolute_time']) + length = format_time(period.length, @config['relative_format']) + notes = period.notes + "#{day.ljust(12)} #{name.ljust(15)} #{start} - #{stop.ljust(5)} #{length} #{notes}" + } + puts table + + + when 'clear','delete','del' + tasks = args + tasks = @trackr.tasks if task == 'all' + tasks.each do |task| + @trackr.clear(task) + puts "Task '#{task}' cleared" if @verbose + end + + when 'rename','mv' + from = args.shift + to = args.shift + @trackr.rename(from,to) + puts "Renamed '#{from}' to '#{to}'" if @verbose + + when 'help' + show_help + + else + puts "'#{cmd}' is not a valid command" + show_help + end + + @trackr.close + end + + protected + + def format_time(time, fmt_str) + hours = time.to_i/3600.to_i + minutes = (time/60 - hours * 60).to_i + seconds = (time - (minutes * 60 + hours * 3600)) + format(fmt_str,{ + :hours => hours, + :minutes => minutes, + :seconds => seconds}) + end + + def show_help + version = File.exist?('VERSION') ? File.read('VERSION') : "" + puts "timetrackr version #{version}" + puts <<HELP + + timetrackr [command] [options] + + Available commands: + + start [task] start a task + stop [task] stop a task (or 'all') + switch TASK switch tasks + time [task] show time for a task (or 'all') + log [task] show time log for a task (or 'all') + + Global options + -h --help show this help + -v --verbose be noisy +HELP + end + end +end diff --git a/lib/timetrackr/database.rb b/lib/timetrackr/database.rb new file mode 100644 index 0000000..3c728ea --- /dev/null +++ b/lib/timetrackr/database.rb @@ -0,0 +1,103 @@ + +module TimeTrackr + class Database + def self.create(type, options={}) + case type.to_s + when 'yaml' + begin + require 'yaml' + db = TimeTrackr::YamlDatabase.new(options['path']) + puts 'Loaded YAML tracker' if options['verbose'] + rescue LoadError + puts 'YAML not found' + end + + when 'sqlite' + begin + require 'sqlite3' + db = TimeTrackr::SqliteDatabase.new(options['path']) + puts 'Loaded sqlite tracker' if options['verbose'] + rescue LoadError + puts 'Sqlite not found' + end + + when 'json' + begin + require 'json' + db = TimeTrackr::JsonDatabase.new(options['path']) + puts 'Loaded JSON database' if options['verbose'] + rescue LoadError + puts 'JSON not found' + end + + else + raise "Bad log type: #{type}" + end + db + end + + # + # return an array of current tasks + # + def current + raise 'Not Implemented' + end + + # + # start a period with optional notes + # + def start(task,notes) + raise 'Not implemented' + end + + # + # stop a period + # + def stop(task) + raise 'Not implemented' + end + + # + # return an array of all tasks + # + def tasks + raise 'Not Implemented' + end + + # + # time in task in seconds + # + def time(task) + raise 'Not implemented' + end + + # + # get task history as an array of Periods + # + def history(task) + raise 'Not Implemented' + end + + # + # rename a task + # + def rename(from, to) + raise 'Not implemented' + end + + # + # clear an task + # + def clear(task) + raise 'Not Implemented' + end + + # + # cleanup and close + # + def close + end + + end +end + diff --git a/lib/timetrackr/json.rb b/lib/timetrackr/json.rb new file mode 100644 index 0000000..9b919d1 --- /dev/null +++ b/lib/timetrackr/json.rb @@ -0,0 +1,68 @@ +module TimeTrackr + class JsonDatabase < TimeTrackr::Database + + def initialize(path) + @log_path = path + if !File.exist? @log_path + @db = {'current' => [], 'tasks' => {}} + write_file + end + File.open(@log_path,'r') do |fh| + @db = JSON.load(fh) + end + end + + def current + @db['current'] + end + + def tasks + @db['tasks'].keys.compact.uniq || [] + end + + def start(task, notes) + @db['tasks'][task] = Array[] unless @db['tasks'][task] + if !@db['current'].include?(task) + @db['current'].unshift(task) + @db['tasks'][task].push({'start' => Time.now, 'notes' => notes}) + end + end + + def stop(task) + if @db['current'].include?(task) + @db['current'].delete(task) + @db['tasks'][task].last['stop'] = Time.now + end + end + + def history(task, p_begin=nil, p_end=nil) + @db['tasks'][task].sort{|x,y| x['start'] <=> y['start']}.collect {|p| + Period.new(task,p['start'],p['stop'],p['notes']) + } unless !@db['tasks'].include? task + end + + def rename(from, to) + @db['tasks'][to] = @db['tasks'].delete(from) + if @db['current'].delete(from) + @db['current'].unshift(to) + end + end + + def close + write_file + end + + def clear(task) + @db['current'].delete(task) + @db['tasks'].delete(task) + end + + private + + def write_file + File.open(@log_path,'w') do |fh| + JSON.dump(@db,fh) + end + end + end +end diff --git a/lib/timetrackr/period.rb b/lib/timetrackr/period.rb index 4dd8b57..ad9010f 100644 --- a/lib/timetrackr/period.rb +++ b/lib/timetrackr/period.rb @@ -9,13 +9,22 @@ class Period @notes = notes end + def start + @start.class == Time ? @start : Time.parse(@start) + end + + def stop + return nil if @stop.nil? + @stop.class == Time ? @stop : Time.parse(@stop) + end + def length - stop = @stop || Time.now - stop - @start + stop = self.stop || Time.now + stop - self.start end def current? - @stop.nil? + self.stop.nil? end end diff --git a/lib/timetrackr/sqlite.rb b/lib/timetrackr/sqlite.rb index 99d95c4..0d4b811 100644 --- a/lib/timetrackr/sqlite.rb +++ b/lib/timetrackr/sqlite.rb @@ -1,70 +1,71 @@ -class SqliteTimeTrackr < TimeTrackr +module TimeTrackr + class SqliteDatabase < TimeTrackr::Database - def initialize(path) - @log_path = path - if !File.exist? @log_path - @db = SQLite3::Database.new(@log_path) - sql_events = "CREATE TABLE events ( + def initialize(path) + @log_path = path + if !File.exist? @log_path + @db = SQLite3::Database.new(@log_path) + sql_events = "CREATE TABLE events ( id INTEGER PRIMARY KEY, task TEXT, start TIME, stop TIME, notes TEXT);" @db.execute(sql_events) - else - @db = SQLite3::Database.open(@log_path) + else + @db = SQLite3::Database.open(@log_path) + end + @db.type_translation = true end - @db.type_translation = true - puts "Using DB file '#{@log_path}'" if $verbose - end - def current - sql = "SELECT DISTINCT task FROM events WHERE stop IS NULL;" - @db.execute(sql).collect{|row| - row.first - } - end + def current + sql = "SELECT DISTINCT task FROM events WHERE stop IS NULL;" + @db.execute(sql).collect{|row| + row.first + } + end - def tasks - sql = "SELECT DISTINCT task FROM events;" - @db.execute(sql).collect{ |row| - row.first - } - end + def tasks + sql = "SELECT DISTINCT task FROM events;" + @db.execute(sql).collect{ |row| + row.first + } + end - def start(task, notes) - sql = "SELECT id FROM events WHERE task = :task AND stop IS NULL;" - exists = @db.get_first_value(sql, 'task' => task) - if !exists - sql = "INSERT INTO events (task,start,notes) VALUES (:task,:start,:notes);" - @db.execute(sql,'task' => task, 'start' => Time.now.to_s, 'notes' => notes) + def start(task, notes) + sql = "SELECT id FROM events WHERE task = :task AND stop IS NULL;" + exists = @db.get_first_value(sql, 'task' => task) + if !exists + sql = "INSERT INTO events (task,start,notes) VALUES (:task,:start,:notes);" + @db.execute(sql,'task' => task, 'start' => Time.now.to_s, 'notes' => notes) + end end - end - def stop(task) - sql = "SELECT id FROM events WHERE task = :task AND stop IS NULL;" - exists = @db.get_first_value(sql, 'task' => task) - if exists - sql = "UPDATE events SET stop = :stop WHERE id = :current;" - @db.execute(sql, 'current' => exists, 'stop' => Time.now.to_s) + def stop(task) + sql = "SELECT id FROM events WHERE task = :task AND stop IS NULL;" + exists = @db.get_first_value(sql, 'task' => task) + if exists + sql = "UPDATE events SET stop = :stop WHERE id = :current;" + @db.execute(sql, 'current' => exists, 'stop' => Time.now.to_s) + end end - end - def history(task, p_begin=nil, p_end=nil) - sql = "SELECT start, stop, notes FROM events WHERE task = :task ORDER BY start;" - @db.execute(sql,'task' => task).collect{ |row| - Period.new(task,row[0],row[1],row[2]) - } - end + def history(task, p_begin=nil, p_end=nil) + sql = "SELECT start, stop, notes FROM events WHERE task = :task ORDER BY start;" + @db.execute(sql,'task' => task).collect{ |row| + Period.new(task,row[0],row[1],row[2]) + } + end - def rename(from, to) - sql = "UPDATE events SET task = :to WHERE task = :from;" - @db.execute(sql, 'to' => to, 'from' => from) - end + def rename(from, to) + sql = "UPDATE events SET task = :to WHERE task = :from;" + @db.execute(sql, 'to' => to, 'from' => from) + end - def clear(task) - sql = "DELETE FROM events WHERE task = :task;" - @db.execute(sql, 'task' => task) - end + def clear(task) + sql = "DELETE FROM events WHERE task = :task;" + @db.execute(sql, 'task' => task) + end + end end diff --git a/lib/timetrackr/yaml.rb b/lib/timetrackr/yaml.rb index 2c02e23..787415e 100644 --- a/lib/timetrackr/yaml.rb +++ b/lib/timetrackr/yaml.rb @@ -10,68 +10,69 @@ # :notes: "blah blah blah" # -class YamlTimeTrackr < TimeTrackr +module TimeTrackr + class YamlDatabase < TimeTrackr::Database - def initialize(path) - @log_path = path - if !File.exist? @log_path - @db = {:current => [], :tasks => {}} - write_file + def initialize(path) + @log_path = path + if !File.exist? @log_path + @db = {:current => [], :tasks => {}} + write_file + end + @db = YAML.load_file(@log_path) end - @db = YAML.load_file(@log_path) - puts "Using log file '#{@log_path}'" if $verbose - end - def current - @db[:current] - end + def current + @db[:current] + end - def tasks - @db[:tasks].keys.compact.uniq || [] - end + def tasks + @db[:tasks].keys.compact.uniq || [] + end - def start(task, notes) - @db[:tasks][task] = Array[] unless @db[:tasks][task] - if !@db[:current].include?(task) - @db[:current].unshift(task) - @db[:tasks][task].push({:start => Time.now, :notes => notes}) + def start(task, notes) + @db[:tasks][task] = Array[] unless @db[:tasks][task] + if !@db[:current].include?(task) + @db[:current].unshift(task) + @db[:tasks][task].push({:start => Time.now, :notes => notes}) + end end - end - def stop(task) - if @db[:current].include?(task) - @db[:current].delete(task) - @db[:tasks][task].last[:stop] = Time.now + def stop(task) + if @db[:current].include?(task) + @db[:current].delete(task) + @db[:tasks][task].last[:stop] = Time.now + end end - end - def history(task, p_begin=nil, p_end=nil) - @db[:tasks][task].sort{|x,y| x[:start] <=> y[:start]}.collect {|p| - Period.new(task,p[:start],p[:stop],p[:notes]) - } unless !@db[:tasks].include? task - end + def history(task, p_begin=nil, p_end=nil) + @db[:tasks][task].sort{|x,y| x[:start] <=> y[:start]}.collect {|p| + Period.new(task,p[:start],p[:stop],p[:notes]) + } unless !@db[:tasks].include? task + end - def rename(from, to) - @db[:tasks][to] = @db[:tasks].delete(from) - if @db[:current].delete(from) - @db[:current].unshift(to) + def rename(from, to) + @db[:tasks][to] = @db[:tasks].delete(from) + if @db[:current].delete(from) + @db[:current].unshift(to) + end end - end - def close - write_file - end + def close + write_file + end - def clear(task) - @db[:current].delete(task) - @db[:tasks].delete(task) - end + def clear(task) + @db[:current].delete(task) + @db[:tasks].delete(task) + end - private + private - def write_file - File.open(@log_path,'w') do |fh| - YAML.dump(@db,fh) + def write_file + File.open(@log_path,'w') do |fh| + YAML.dump(@db,fh) + end end end end |
