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.
- data/.travis.yml +5 -2
- data/CHANGELOG.md +9 -0
- data/Gemfile +14 -7
- data/README.md +24 -9
- data/Rakefile +10 -10
- data/VERSION +1 -1
- data/bin/tracking +1 -1
- data/lib/tracking.rb +9 -20
- data/lib/tracking/cli.rb +117 -109
- data/lib/tracking/config.rb +34 -15
- data/lib/tracking/list.rb +77 -57
- data/lib/tracking/task.rb +99 -0
- data/spec/cli_spec.rb +36 -0
- data/spec/helper.rb +21 -0
- data/tracking.gemspec +81 -0
- metadata +103 -22
- data/test/helper.rb +0 -39
- data/test/test_cli.rb +0 -47
data/lib/tracking/config.rb
CHANGED
@@ -1,29 +1,35 @@
|
|
1
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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 =>
|
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
|
-
#
|
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
|
-
#
|
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,
|
63
|
+
File.open(PATH, 'w') do |fh|
|
46
64
|
fh.puts(configs.to_yaml)
|
47
65
|
end
|
48
66
|
end
|
49
67
|
|
50
|
-
#
|
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
|
-
|
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
|
data/lib/tracking/list.rb
CHANGED
@@ -1,76 +1,96 @@
|
|
1
|
-
|
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
|
-
|
15
|
-
|
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
|
-
#
|
19
|
-
|
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
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
data/spec/cli_spec.rb
ADDED
@@ -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
|
data/spec/helper.rb
ADDED
@@ -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
|
data/tracking.gemspec
ADDED
@@ -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
|
+
|