tracking 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|