tracking 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,35 @@
1
- #utilities for tracking's config file
1
+ require 'fileutils'
2
+ require 'yaml'
2
3
 
3
- #imports
4
- require "yaml"
5
-
6
- #config module methods
7
4
  module Tracking
5
+ # Contains methods to interface with tracking's config file.
6
+ #
8
7
  # similar to Sam Goldstein's config.rb for timetrap
9
8
  # @see https://github.com/samg/timetrap/
10
9
  module Config
11
-
12
10
  extend self
13
11
 
14
- PATH = File.join(ENV["HOME"], ".tracking", "config.yml")
15
-
16
- #default values
12
+ # The path to the config file
13
+ PATH = File.join(ENV['HOME'], '.tracking', 'config.yml')
14
+
15
+ # The path to the config file's parent directory
16
+ DIR = File.join(ENV['HOME'], '.tracking')
17
+
18
+ # Default config hash
19
+ #
20
+ # @return [Hash<Symbol,Object>] the default config in a hash
17
21
  def defaults
18
22
  {
19
23
  # path to the data file (string, ~ can be used)
20
- :data_file => "~/.tracking/data.csv",
24
+ :data_file => '~/.tracking/data.csv',
21
25
  # number of lines to be displayed at once by default (integer)
22
26
  :lines => 10,
23
27
  # width of the task name column, in characters (integer)
24
28
  :task_width => 40,
25
29
  # format to use for elapsed time display (:colons or :letters)
26
30
  :elapsed_format => :colons,
31
+ # toggle colored display of the current (last) task
32
+ :color_current_task => true,
27
33
  # toggle header describing tracking's display columns (true or false)
28
34
  :show_header => true,
29
35
  # toggle display of seconds in elapsed time (true of false)
@@ -31,30 +37,43 @@ module Tracking
31
37
  }
32
38
  end
33
39
 
34
- #accessor for values in the config
40
+ # Overloading [] operator
41
+ #
42
+ # Accessor for values in the config
43
+ #
44
+ # @param [Symbol] key the key in the config hash
45
+ # @return [Object] the value associated with that key
35
46
  def [] key
47
+ write unless File.exist? PATH
36
48
  data = YAML.load_file PATH
37
49
  defaults.merge(data)[key]
38
50
  end
39
51
 
40
- #setter for keys in config
52
+ # Overloading []= operator
53
+ #
54
+ # Setter for values in the config
55
+ #
56
+ # @param [Symbol] key the key you are setting a value for
57
+ # @param [Object] value the value you associated with the key
41
58
  def []= key, value
59
+ write unless File.exist? PATH
42
60
  data = YAML.load_file PATH
43
61
  configs = defaults.merge(data)
44
62
  configs[key] = value
45
- File.open(PATH, "w") do |fh|
63
+ File.open(PATH, 'w') do |fh|
46
64
  fh.puts(configs.to_yaml)
47
65
  end
48
66
  end
49
67
 
50
- #writes the config file path
68
+ # Writes the configs to the file config.yml
51
69
  def write
52
70
  configs = if File.exist? PATH
53
71
  defaults.merge(YAML.load_file PATH)
54
72
  else
55
73
  defaults
56
74
  end
57
- File.open(PATH, "w") do |fh|
75
+ FileUtils.mkdir DIR unless File.directory? DIR
76
+ File.open(PATH, 'w') do |fh|
58
77
  fh.puts configs.to_yaml
59
78
  end
60
79
  end
@@ -1,76 +1,96 @@
1
- #tracking's core
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require 'time'
4
+ require 'csv'
2
5
 
3
- #imports
4
- require "yaml"
5
- require "time"
6
- require "csv"
7
-
8
- #model/controller module methods
9
6
  module Tracking
7
+ # Tracking's core. Contains methods for manipulating tasks in the data file
8
+ # and preparing its data for a user interface.
10
9
  module List
11
-
12
10
  extend self
13
11
 
14
- Config = YAML.load_file(ENV["HOME"] + "/.tracking/config.yml")
15
- $data_file = File.expand_path(Config[:data_file])
16
- $csv_options = { :col_sep => "\t" }
12
+ # The path to tracking's data file
13
+ @data_file = File.expand_path(Config[:data_file])
17
14
 
18
- #adds an item to the list
19
- def add item
20
- date = Time.now.to_s
21
- File.open($data_file, "a") do |file|
22
- file << [ date, item ].to_csv($csv_options)
23
- end
24
- end
15
+ # The options tracking uses for Ruby's CSV interface
16
+ @csv_options = { :col_sep => "\t" }
25
17
 
26
- #deletes an item from the list
27
- def delete
28
- lines = File.readlines $data_file
29
- lines.pop #or delete specific lines in the future
30
- File.open($data_file, "w") do |file|
31
- lines.each do |line|
32
- file << line
18
+ # Reads part of the data file and creates Task objects from that data
19
+ #
20
+ # @param [Integer] max the maximum number of items to get from the end of
21
+ # the data file
22
+ #
23
+ # @return [Array] an array of Task objects
24
+ def get max=Config[:lines]
25
+ if File.exist? @data_file
26
+ all_lines = CSV.read(@data_file, @csv_options)
27
+ lines = all_lines[-max..-1]
28
+ lines = all_lines if lines.nil?
29
+
30
+ tasks = []
31
+ lines.each_with_index do |line, i|
32
+ tasks << create_task_from_data(line, lines[i+1])
33
33
  end
34
+ return tasks
35
+ else
36
+ return []
34
37
  end
35
38
  end
36
39
 
37
- #clears the entire list
38
- def clear
39
- FileUtils.rm $data_file
40
- FileUtils.touch $data_file
40
+ # Generates a Task object from one or two lines of semi-parsed CSV data
41
+ #
42
+ # @param [Array] line the line of semi-parsed CSV data to use
43
+ # @param [Array] next_line the next line of data, if it exists
44
+ #
45
+ # @return [Task] the generated Task object
46
+ def create_task_from_data(line, next_line=nil)
47
+ name = line[1]
48
+ start_time = Time.parse line[0]
49
+ end_time = next_line.nil? ? Time.now : Time.parse(next_line[0])
50
+
51
+ return Task.new(name, start_time, end_time)
41
52
  end
42
53
 
43
- #gets and formats the amount of time passed between two times
44
- def get_elapsed_time(time1, time2)
45
- #calculate the elapsed time and break it down into different units
46
- seconds = (time2 - time1).floor
47
- minutes = hours = days = 0
48
- if seconds >= 60
49
- minutes = seconds / 60
50
- seconds = seconds % 60
51
- if minutes >= 60
52
- hours = minutes / 60
53
- minutes = minutes % 60
54
- if hours >= 24
55
- days = hours / 24
56
- hours = hours % 24
54
+ # Adds a task to the list
55
+ #
56
+ # @param [String] name the name of the task to add to the list
57
+ def add name, time=Time.now
58
+ FileUtils.touch @data_file unless File.exist? @data_file
59
+ File.open(@data_file, 'a') do |file|
60
+ file << [ time.to_s, name ].to_csv(@csv_options)
61
+ end
62
+ end
63
+
64
+ # Deletes the last task from the list
65
+ def delete
66
+ if File.exist? @data_file
67
+ lines = File.readlines @data_file
68
+ lines.pop # Or delete specific lines in the future
69
+ File.open(@data_file, 'w') do |file|
70
+ lines.each do |line|
71
+ file << line
57
72
  end
58
73
  end
59
74
  end
60
- #return a string of the formatted elapsed time
61
- case Config[:elapsed_format]
62
- when :colons
63
- if Config[:show_elapsed_seconds]
64
- return "%02d:%02d:%02d:%02d" % [days, hours, minutes, seconds]
65
- else
66
- return "%02d:%02d:%02d" % [days, hours, minutes]
67
- end
68
- when :letters
69
- if Config[:show_elapsed_seconds]
70
- return "%02dd %02dh %02dm %02ds" % [days, hours, minutes, seconds]
71
- else
72
- return "%02dd %02dh %02dm" % [days, hours, minutes]
73
- end
75
+ end
76
+
77
+ # Renames the last task in the list
78
+ #
79
+ # @param [String] name the new name for the last task
80
+ def rename name
81
+ # get task data
82
+ old_task = get(1).first
83
+ # delete last task
84
+ delete
85
+ # add new task with old time
86
+ add(name, old_task.raw(:start_time))
87
+ end
88
+
89
+ # Clears the entire list
90
+ def clear
91
+ if File.exist? @data_file
92
+ FileUtils.rm @data_file
93
+ FileUtils.touch @data_file
74
94
  end
75
95
  end
76
96
 
@@ -0,0 +1,99 @@
1
+ module Tracking
2
+ # The class for all Task objects created by List and passed to interfaces.
3
+ # Holds all relevant data for Tasks, as well as methods for computing new Task
4
+ # data as needed. Tasks internally save data (in their appropriate types) to
5
+ # intance variables, while public accessors return data in strings for
6
+ # display.
7
+ class Task
8
+
9
+ # Creates a new Task object. Data passed into its arguments is kept
10
+ # (unchanged) in instance variables.
11
+ #
12
+ # @param [String] name the tasks's name
13
+ # @param [Time] start_time the tasks's start time
14
+ # @param [Time] end_time the tasks's end time
15
+ def initialize(name, start_time, end_time)
16
+ @name = name
17
+ @start_time = start_time
18
+ @end_time = end_time
19
+ end
20
+
21
+ # Gets raw data from the task object, without doing any conversions or
22
+ # formatting
23
+ #
24
+ # @param [Symbol] key the key of the desired value
25
+ #
26
+ # @return the value of the requested key
27
+ def raw key
28
+ case key
29
+ when :name
30
+ return @name
31
+ when :start_time
32
+ return @start_time
33
+ when :end_time
34
+ return @end_time
35
+ end
36
+ end
37
+
38
+ # Converts the task object into a string (for debugging)
39
+ def to_s
40
+ return "name: #{name}; start: #{@start_time}; end: #{@end_time};"
41
+ end
42
+
43
+ # Calculates the length of strings from Task#elapsed_time (using the current
44
+ # elapsed time format).
45
+ #
46
+ # @return [String] the length of strings from Task#elapsed_time
47
+ def self.elapsed_time_length
48
+ test_task = Task.new('test', Time.now, Time.now)
49
+ return test_task.elapsed_time.length
50
+ end
51
+
52
+ # Accessor for this tasks's name (read/write)
53
+ attr_accessor :name
54
+
55
+ # Formats and returns the start time of this task.
56
+ #
57
+ # @return [String] the formatted start time of this task
58
+ def start_time
59
+ return @start_time.strftime('%H:%M')
60
+ end
61
+
62
+ # Calculates, formats, and returns the elapsed time of this task.
63
+ #
64
+ # @return [String] the formatted elapsed time of this task
65
+ def elapsed_time
66
+ # Calculate the elapsed time and break it down into different units
67
+ seconds = (@end_time - @start_time).floor
68
+ minutes = hours = days = 0
69
+ if seconds >= 60
70
+ minutes = seconds / 60
71
+ seconds = seconds % 60
72
+ if minutes >= 60
73
+ hours = minutes / 60
74
+ minutes = minutes % 60
75
+ if hours >= 24
76
+ days = hours / 24
77
+ hours = hours % 24
78
+ end
79
+ end
80
+ end
81
+ # Return a string of the formatted elapsed time
82
+ case Config[:elapsed_format]
83
+ when :colons
84
+ if Config[:show_elapsed_seconds]
85
+ return '%02d:%02d:%02d:%02d' % [days, hours, minutes, seconds]
86
+ else
87
+ return '%02d:%02d:%02d' % [days, hours, minutes]
88
+ end
89
+ when :letters
90
+ if Config[:show_elapsed_seconds]
91
+ return '%02dd %02dh %02dm %02ds' % [days, hours, minutes, seconds]
92
+ else
93
+ return '%02dd %02dh %02dm' % [days, hours, minutes]
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,36 @@
1
+ require 'helper'
2
+ require 'fileutils'
3
+
4
+ describe Tracking::CLI do
5
+
6
+ before :each do
7
+ FileUtils.cd File.expand_path('~/.tracking')
8
+ FileUtils.mkdir 'test_backup'
9
+ %w(config.yml data.csv).each do |f|
10
+ FileUtils.mv(f, 'test_backup') if File.exist? f
11
+ end
12
+ end
13
+
14
+ after :each do
15
+ FileUtils.cd File.expand_path('~/.tracking/test_backup')
16
+ %w(config.yml data.csv).each do |f|
17
+ FileUtils.mv(f, File.expand_path('..')) if File.exist? f
18
+ end
19
+ FileUtils.cd File.expand_path('..')
20
+ FileUtils.rmdir 'test_backup'
21
+ end
22
+
23
+ it 'performs a few operations on a new list and then clears it' do
24
+ capture_output do
25
+ Tracking::List.clear
26
+ Tracking::CLI.display
27
+ Tracking::List.add 'first task'
28
+ Tracking::List.add 'second task'
29
+ Tracking::List.rename 'second task, renamed'
30
+ Tracking::CLI.display
31
+ Tracking::List.delete
32
+ Tracking::List.clear
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts 'Run `bundle install` to install missing gems'
7
+ exit e.status_code
8
+ end
9
+
10
+ require_relative '../lib/tracking'
11
+
12
+ def capture_output &block
13
+ original_stdout = $stdout
14
+ $stdout = fake = StringIO.new
15
+ begin
16
+ yield
17
+ ensure
18
+ $stdout = original_stdout
19
+ end
20
+ fake.string
21
+ end
@@ -0,0 +1,81 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "tracking"
8
+ s.version = "1.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Nicolas McCurdy"]
12
+ s.date = "2012-09-10"
13
+ s.description = "See README for more information."
14
+ s.email = "thenickperson@gmail.com"
15
+ s.executables = ["tracking"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.md"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".travis.yml",
23
+ "CHANGELOG.md",
24
+ "Gemfile",
25
+ "LICENSE.txt",
26
+ "README.md",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/tracking",
30
+ "lib/tracking.rb",
31
+ "lib/tracking/cli.rb",
32
+ "lib/tracking/config.rb",
33
+ "lib/tracking/list.rb",
34
+ "lib/tracking/task.rb",
35
+ "spec/cli_spec.rb",
36
+ "spec/helper.rb",
37
+ "tracking.gemspec"
38
+ ]
39
+ s.homepage = "http://github.com/thenickperson/tracking"
40
+ s.licenses = ["MIT"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = "1.8.24"
43
+ s.summary = "A simple and configurable command line time tracker."
44
+
45
+ if s.respond_to? :specification_version then
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<colorize>, ["~> 0.5"])
50
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
51
+ s.add_development_dependency(%q<bundler>, [">= 1.0.0"])
52
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
53
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
54
+ s.add_development_dependency(%q<yard>, [">= 0"])
55
+ s.add_development_dependency(%q<rspec>, [">= 0"])
56
+ s.add_development_dependency(%q<redcarpet>, [">= 0"])
57
+ s.add_development_dependency(%q<kramdown>, [">= 0"])
58
+ else
59
+ s.add_dependency(%q<colorize>, ["~> 0.5"])
60
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
61
+ s.add_dependency(%q<bundler>, [">= 1.0.0"])
62
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
63
+ s.add_dependency(%q<simplecov>, [">= 0"])
64
+ s.add_dependency(%q<yard>, [">= 0"])
65
+ s.add_dependency(%q<rspec>, [">= 0"])
66
+ s.add_dependency(%q<redcarpet>, [">= 0"])
67
+ s.add_dependency(%q<kramdown>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<colorize>, ["~> 0.5"])
71
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
72
+ s.add_dependency(%q<bundler>, [">= 1.0.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
74
+ s.add_dependency(%q<simplecov>, [">= 0"])
75
+ s.add_dependency(%q<yard>, [">= 0"])
76
+ s.add_dependency(%q<rspec>, [">= 0"])
77
+ s.add_dependency(%q<redcarpet>, [">= 0"])
78
+ s.add_dependency(%q<kramdown>, [">= 0"])
79
+ end
80
+ end
81
+