dayrb 2.0.2

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/lib/day/parser.rb ADDED
@@ -0,0 +1,168 @@
1
+ # DayRB Parser Module
2
+ #
3
+ # Parses and validates ARGV.
4
+ # After validation, we can confidently assume a specified task in opts exists.
5
+ #
6
+ # MIT License; See LICENSE file; Cameron Carroll 2014
7
+
8
+ require 'abbrev'
9
+
10
+ E_NO_SUCH_TASK = "I didn't find any task by that name."
11
+ E_MUST_SPECIFY_TASK = "I need you to specify which task to delete."
12
+
13
+ # DayRB Parser Module
14
+ #
15
+ # Scans and validates ARGV inputs and builds the opts hash.
16
+ # (Performs argument validation, and checks that a specified task really exists.)
17
+ module Parser
18
+ class << self
19
+
20
+ # Parse ARGV into opts hash
21
+ #
22
+ # @param config [Configuration] Entire configuration object, needed for task lookups.
23
+ def parse_options(config)
24
+ opts = {}
25
+ @config = config
26
+
27
+ opts[:operation] = case ARGV.first
28
+ when nil
29
+ :print
30
+ when "-a", "-A"
31
+ opts[:all] = true
32
+ :print
33
+ when "clear", "c"
34
+ :clear
35
+ when "delete", "rm"
36
+ :delete
37
+ when "info", "i"
38
+ :print_info
39
+ when "help"
40
+ :print_help
41
+ when "version"
42
+ :print_version
43
+ else
44
+ handle_non_command(ARGV.first) # could either be a new task or switch to an existing one
45
+ end
46
+
47
+ opts[:task] = case opts[:operation]
48
+ when :clear, :print_info
49
+ check_for_second_argument
50
+ when :delete
51
+ demand_second_argument
52
+ when :switch
53
+ task = lookup_task(ARGV.first)
54
+ if task && @config.data['context'] == task
55
+ opts[:operation] = :leave
56
+ nil
57
+ elsif task
58
+ task
59
+ else
60
+ raise ArgumentError, E_NO_SUCH_TASK
61
+ end
62
+ when :new
63
+ ARGV.first
64
+ end
65
+
66
+ if opts[:operation] == :new
67
+ opts = handle_new_task(opts)
68
+ end
69
+
70
+ opts.delete_if { |k, v| v.nil? }
71
+ return opts
72
+ end
73
+
74
+ private
75
+
76
+ # Determine if a non-command argument corresponds to a :switch or a :new task
77
+ def handle_non_command(argument)
78
+ if argument.number? || lookup_task(argument) # then we switch to that task index or name
79
+ :switch
80
+ else # then we assume it's a new task to be created.
81
+ :new
82
+ end
83
+ end
84
+
85
+ # Check for ARGV[1] but don't raise error if it doesn't exist.
86
+ # But if we find a task name/index, we make sure it is valid.
87
+ def check_for_second_argument
88
+ if ARGV[1]
89
+ task = lookup_task(ARGV[1])
90
+ if task
91
+ task
92
+ else
93
+ raise ArgumentError, E_NO_SUCH_TASK
94
+ end
95
+ end
96
+ end
97
+
98
+ # Checks for second argument, but raises error if it doesn't exist.
99
+ def demand_second_argument
100
+ argument = check_for_second_argument
101
+ if argument
102
+ argument
103
+ else
104
+ raise ArgumentError, E_MUST_SPECIFY_TASK
105
+ end
106
+ end
107
+
108
+ # Check config data either for a task name or index.
109
+ def lookup_task(name)
110
+ @config.lookup_task(name)
111
+ end
112
+
113
+ # Gather remaining options for a new task
114
+ # Checks for a description (or a mention of EDITOR),
115
+ # valid days, and a time estimate.
116
+ def handle_new_task(opts)
117
+ ARGV[1..-1].each do |arg|
118
+ if arg =~ /\(.+\)/
119
+ next if opts[:editor]
120
+ opts[:description] = arg
121
+ elsif arg.downcase == EDITOR
122
+ opts[:editor] = true
123
+ opts[:description] = ''
124
+ tempfile = 'dayrb_description.tmp'
125
+ system("#{EDITOR} #{tempfile}")
126
+ input = ""
127
+ begin
128
+ File.open(tempfile, 'r') do |tempfile|
129
+ while (line = tempfile.gets)
130
+ opts[:description] << line.chomp
131
+ end
132
+ end
133
+
134
+ File.delete tempfile
135
+ rescue => err
136
+ raise ArgumentError, err
137
+ end
138
+ elsif arg.downcase.nan?
139
+ opts[:days] ||= []
140
+ key = parse_day_argument(arg)
141
+ if opts[:days].include? key
142
+ raise ArgumentError, "You specified a single day (#{key}) more than once."
143
+ else
144
+ opts[:days] << key
145
+ end
146
+ else
147
+ if opts[:estimate]
148
+ raise ArgumentError, 'You specified more than one time estimate.'
149
+ else
150
+ opts[:estimate] = arg.to_i * 60# convert to seconds for storage
151
+ end
152
+ end
153
+ end
154
+
155
+ return opts
156
+ end
157
+
158
+ # Check a possible valid-day argument against abbreviation list.
159
+ def parse_day_argument(day)
160
+ abbreviations = Abbrev.abbrev(%w{sunday monday tuesday wednesday thursday friday saturday})
161
+ if abbreviations.has_key? day
162
+ return abbreviations[day].to_sym
163
+ else
164
+ raise ArgumentError, "Couldn't parse which days to enable task."
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,223 @@
1
+ # DayRB Presentation Module
2
+ #
3
+ # Handles printouts and error messages.
4
+ # Also adds colorization as specified in main file.
5
+ #
6
+ # MIT License; See LICENSE file; Cameron Carroll 2014
7
+
8
+ module Presenter
9
+ class << self
10
+
11
+ # Prints out task list and current context, if applicable.
12
+ #
13
+ # @param tasklist [Hash] Hash of task_name => task_object pairs
14
+ # @param context [String] Name of current task context
15
+ # @param time [String] Elapsed time since starting current task.
16
+ def print_list(tasklist, context, time)
17
+ if tasklist.empty?
18
+ print_error_empty
19
+ else
20
+ print_task_list(tasklist)
21
+ end
22
+ if context
23
+ print_current_context(context, time)
24
+ end
25
+ end
26
+
27
+ # Prints info for a specific task if provided.
28
+ # If not, prints out every description for tasks that have one.
29
+ #
30
+ # @param tasklist [Hash] Hash of task_name => task_object pairs
31
+ # @param task [String] Name of specific task to print info for.
32
+ def print_info(tasklist, task)
33
+ if task
34
+ task_object = tasklist[task]
35
+ if task_object.description
36
+ print_description task, task_object
37
+ else
38
+ puts "There was no description for #{task}."
39
+ end
40
+ else
41
+ tasklist.each do |task_name, task_object|
42
+ print_description task_name, task_object if task_object.description
43
+ end
44
+ end
45
+ end
46
+
47
+ # Prints out program help string
48
+ def print_help
49
+ puts <<-eos
50
+ Usage: day.rb <command> [<args>]
51
+
52
+ Commands:
53
+ (no command) Prints out task list for the day
54
+ (nonexisting task) Creates a new task
55
+ (existing task) Start tracking time for named task.
56
+ delete (task) Remove a task
57
+ info Print all descriptions
58
+ info (task) Print a specific description
59
+ clear Clear fulfillment for all tasks.
60
+ clear (task) Clear fulfillment for a specific task.
61
+
62
+ Refer to a task either by its name or index.
63
+ See readme.md for a more detailed overview.
64
+ eos
65
+ end
66
+
67
+ # Prints out the VERSION constant
68
+ def print_version
69
+ puts "Day.rb v#{VERSION}"
70
+ end
71
+
72
+ # Announces task has been deleted and prints its description if applicable.
73
+ #
74
+ # @param task [String] Name of task to be deleted
75
+ # @param description [String] Description of task (optional)
76
+ def announce_deletion(task, description)
77
+ puts "Deleted #{task}".color_text
78
+ puts "Description was: #{description}".color_text if description
79
+ end
80
+
81
+ # Announces that either a task or all tasks have had fulfillment cleared.
82
+ #
83
+ # @param task [String] Name of task to be cleared
84
+ def announce_clear(task)
85
+ if task
86
+ puts "Cleared fulfillment for #{task}".color_text
87
+ else
88
+ puts "Cleared fulfillment for all tasks".color_text
89
+ end
90
+ end
91
+
92
+ # Announces a switch to a new task...
93
+ # also prints the amount of time spent on the old one.
94
+ #
95
+ # @param task [String] Name of task to switch to
96
+ # @param old_task [String] Name of current context, before switching
97
+ # @param old_time [String] Time spent since starting old_task
98
+ def announce_switch(task, old_task, old_time)
99
+ puts "Switching to #{task}"
100
+ if old_task && old_time
101
+ puts "(Spent #{convert_time_with_suffix old_time} on #{old_task})"
102
+ end
103
+ end
104
+
105
+ # Announces that we leave current context, prints out time spent.
106
+ # Used when not starting a new task.
107
+ #
108
+ # @param old_task [String] Name of current context
109
+ # @param old_time [String] Time spent since starting old_task
110
+ def announce_leave_context(old_task, old_time)
111
+ puts "Stopping tracking for #{old_task}"
112
+ puts "(Spent #{convert_time_with_suffix old_time})"
113
+ end
114
+
115
+ # Announces the creation of a new task.
116
+ #
117
+ # @param task [String] Name of task to be added
118
+ def announce_new_task(task)
119
+ puts "Added new task, #{task}"
120
+ end
121
+
122
+ private
123
+
124
+ # Iterate through tasklist, printing index, name, description flag and fulfillment/estimate data.
125
+ #
126
+ # @param tasks [Hash] Collection of task_name => task_object pairs
127
+ def print_task_list(tasks)
128
+ ii = 0
129
+ # indexing the hash as an array
130
+ # task[0] contains key (task name)
131
+ # task[1] contains task object
132
+ tasks.each_with_index do |task, ii|
133
+ task_name = task[0]
134
+ task_object = task[1]
135
+ print ii.to_s.color_index + ': ' + task_name.color_task
136
+ print "*".color_star if task_object.description
137
+ print_fulfillment(task_object.fulfillment, task_object.time_estimate)
138
+ puts "\n"
139
+ end
140
+ end
141
+
142
+ # Print/format fulfillment and estimate data.
143
+ #
144
+ # @param fulfillment [Integer] Time spent on task in seconds
145
+ # @param estimate [Integer] Estimated time for task in seconds
146
+ def print_fulfillment(fulfillment, estimate)
147
+ if fulfillment
148
+ if estimate
149
+ diff = fulfillment.to_f / estimate.to_f * 100
150
+ print " [#{convert_time(fulfillment)}".color_completion + "/#{convert_time_with_suffix(estimate)}]".color_text
151
+ print " [#{'%2.1f' % diff}%]".color_completion
152
+ else
153
+ print " [#{convert_time_with_suffix(fulfillment)}]"
154
+ end
155
+ elsif estimate
156
+ print " (#{convert_time_with_suffix(estimate)} estimate)"
157
+ end
158
+ end
159
+
160
+ # Print task name and description.
161
+ #
162
+ # @param task_name [String] Name of task
163
+ # @param task_object [Task] Task object to print description for
164
+ def print_description(task_name, task_object)
165
+ print "Description for #{task_name}: "
166
+ puts task_object.description
167
+ end
168
+
169
+ # Print information about the current task.
170
+ #
171
+ # @param context [String] Name of current task
172
+ # @param time [Integer] Time spent on current task in seconds
173
+ def print_current_context(context, time)
174
+ puts "Current task: #{context} (#{convert_time_with_suffix(time)})"
175
+ end
176
+
177
+ # Convert seconds into a more appropriate amount.
178
+ # Hours and days also return the leftover minutes and hours, respectively.
179
+ #
180
+ # @param seconds [Integer] Time to be converted in seconds.
181
+ def convert_time(seconds)
182
+ if seconds < 60
183
+ return ('%1.0f' % seconds)
184
+ elsif seconds >= 60 && seconds < 3600
185
+ return ('%2.1f' % (seconds/60))
186
+ elsif seconds >= 3600 && seconds < 86400
187
+ hours = seconds / 3600
188
+ leftover_minutes = seconds % 3600 / 60
189
+ return ('%1.0f' % hours), ('%2.1f' % leftover_minutes)
190
+ elsif seconds >= 86400
191
+ days = seconds / 86400
192
+ leftover_hours = seconds % 86400 / 3600
193
+ return ('%1.0f' % days), ('%2.1f' % leftover_hours)
194
+ end
195
+ end
196
+
197
+ # Formats the results of convert_time into a human-readable string.
198
+ #
199
+ # @param seconds [Integer] Time to be converted in seconds.
200
+ def convert_time_with_suffix(seconds)
201
+ first_result, second_result = convert_time(seconds)
202
+ if seconds < 60
203
+ "#{first_result} seconds"
204
+ elsif seconds >= 60 && seconds < 3600
205
+ "#{first_result} minutes"
206
+ elsif seconds >= 3600 && seconds < 86400
207
+ "#{first_result} hours and #{second_result} minutes"
208
+ elsif seconds >= 86400
209
+ "#{first_result} days and #{second_result} hours"
210
+ end
211
+ end
212
+
213
+ # Print empty-tasklist error.
214
+ def print_error_empty()
215
+ puts "The task list is empty!"
216
+ end
217
+
218
+ # Print unknown error.
219
+ def print_error_unknown()
220
+ puts "Sorry, that command is not known. Try 'help'."
221
+ end
222
+ end
223
+ end
data/lib/day/task.rb ADDED
@@ -0,0 +1,44 @@
1
+ # DayRB Task Class
2
+ #
3
+ # Has essentially only one function, which is to perform valid_today? checks.
4
+ # But maybe we'll just keep it and give it more responsibility later.
5
+ #
6
+ # MIT License; See LICENSE file; Cameron Carroll 2014
7
+
8
+ class Task
9
+
10
+ attr_reader :name, :valid_days, :description, :time_estimate, :fulfillment
11
+
12
+ def initialize(name, description, valid_days, time_estimate, fulfillment)
13
+ @name = name
14
+ @valid_days = valid_days
15
+ @description = description
16
+ @time_estimate = time_estimate
17
+ @fulfillment = fulfillment
18
+ end
19
+
20
+ # Determine whether the task is valid today.
21
+ def valid_today?
22
+ if @valid_days
23
+ today = Time.new.wday #0 is sunday, 6 saturday
24
+
25
+ weekday_key = case today
26
+ when 0 then :sunday
27
+ when 1 then :monday
28
+ when 2 then :tuesday
29
+ when 3 then :wednesday
30
+ when 4 then :thursday
31
+ when 5 then :friday
32
+ when 6 then :saturday
33
+ end
34
+
35
+ if (@valid_days.include?(weekday_key) || @valid_days.empty?)
36
+ return true
37
+ else
38
+ return false
39
+ end
40
+ else
41
+ return true # valid everyday
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # DayRB Tasklist Module
2
+ #
3
+ # MIT License; See LICENSE file; Cameron Carroll 2014
4
+
5
+ require_relative 'task'
6
+
7
+ # DayRB Tasklist Module
8
+ #
9
+ # Responsible for loading tasks.
10
+ # Mainly to manage a list of tasks which are valid today,
11
+ # but also allow us to use the '-a' option.
12
+ class Tasklist
13
+ attr_reader :all_tasks, :valid_tasks
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ @all_tasks = load_tasks(config.data['tasks'])
18
+ today = Time.new.strftime("%A").downcase.to_sym
19
+ @valid_tasks = @all_tasks.select do |task_name, task_object|
20
+ if task_object.valid_days
21
+ task_object.valid_days.include? today
22
+ else
23
+ true
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Build array of task objects from their DB records.
31
+ #
32
+ # @param tasks [Hash] Collection of task_name => task_hash pairs
33
+ def load_tasks(tasks)
34
+ task_objects = {}
35
+ unless tasks.empty?
36
+ tasks.each do |task_name, task|
37
+ task_objects[task_name] = Task.new(task_name, task['description'], task['valid_days'],
38
+ task['estimate'], task['fulfillment'])
39
+ end
40
+ end
41
+
42
+ return task_objects
43
+ end
44
+ end
data/readme.md ADDED
@@ -0,0 +1,53 @@
1
+ day.rb
2
+ ======
3
+ (Version 2.0.1 -- August 2014)
4
+
5
+ A command-line to-do & time-tracking application.
6
+
7
+ * Define & describe tasks, and set time estimates for yourself.
8
+ * Check in or out of tasks to track time spent.
9
+
10
+ Requirements:
11
+ -------------
12
+ * Ruby (Tested with 2.1.2)
13
+
14
+ Installation:
15
+ -------------
16
+
17
+ ### Method 1: Download a Release (One File)
18
+
19
+ * Head on over to the [Releases Page](https://github.com/sanarothe/day/releases)
20
+ * Download the latest "one-file distributable" version of day.rb
21
+ * Stick it in your favorite bin folder. (~/bin)
22
+ * Chmod it to be executable (chmod +x ~/bin/day.rb)
23
+
24
+ ### Method 2: Clone the Repository (Entire Folder)
25
+
26
+ * Clone the repository to your favorite apps folder. (git clone https://github.com/sanarothe/day.git ~/apps)
27
+ * Symlink day.rb into your favorite bin folder. (ln -s ~/apps/day/day.rb ~/bin/day)
28
+ * Chmod it to be executable (chmod +x ~/bin/day)
29
+
30
+ Usage Overview:
31
+ ---------------
32
+
33
+ Usage: day.rb <command> [<args>]
34
+
35
+ Commands:
36
+ (no command) Prints out task list for the day
37
+ (nonexisting task) Creates a new task
38
+ (existing task) Start tracking time for named task
39
+ delete (task) Remove a task
40
+ info Print all descriptions
41
+ info (task) Print a specific description
42
+ clear Clear fulfillment for all tasks.
43
+ clear (task) Clear fulfillment for a specific task.
44
+
45
+ (From 'day.rb help')
46
+
47
+ * Use the '-a' flag (with no command) to print out tasks that aren't enabled for the day
48
+ * Jump directly from task to task
49
+ * Stores data by default in ~/.config/day/ -- Edit the constant at top of script to change this.
50
+
51
+ Copyright 2014 - Cameron Carroll
52
+
53
+ License: MIT
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+ describe Configuration do
4
+
5
+ describe "#initialize" do
6
+ before :each do
7
+ bootstrap
8
+ end
9
+
10
+ it "accepts a file path and creates a DB file." do
11
+ expect(File.exists?(FULL_FILE_PATH)).to be(true)
12
+ end
13
+
14
+ it "bootstraps the DB" do
15
+ expect(@config.data['tasks'].class).to be(Hash)
16
+ end
17
+ end
18
+
19
+ describe "#new_task" do
20
+ before :each do
21
+ bootstrap
22
+ end
23
+
24
+ it "adds a task to the database" do
25
+ test_name = 'test_task_name'
26
+ opts = {:task => test_name, :description => 'some description'}
27
+ expect(@config.data['tasks'][test_name]).to be(nil)
28
+ @config.new_task(opts)
29
+ @config.reload
30
+ expect(@config.data['tasks'][test_name]).to be_truthy
31
+ end
32
+ end
33
+
34
+ describe "#switch_to" do
35
+ before :each do
36
+ bootstrap
37
+ end
38
+
39
+ it "should enter a new context (no current)" do
40
+ expect(@config.data['context']).to eq(nil)
41
+ @config.switch_to("test")
42
+ expect(@config.data['context']).to eq("test")
43
+ end
44
+
45
+ it "should enter a new context (given a current one)" do
46
+ bootstrap_task("test2")
47
+ @config.switch_to("test")
48
+ expect(@config.data['context']).to eq("test")
49
+ @config.switch_to("test2")
50
+ expect(@config.data['context']).to eq("test2")
51
+ end
52
+ end
53
+
54
+ describe "#delete" do
55
+ before :each do
56
+ bootstrap
57
+ end
58
+
59
+ it "should remove a task from data" do
60
+ expect(@config.data['tasks']['test']).to be_truthy
61
+ @config.delete("test")
62
+ expect(@config.data['tasks']['test']).not_to be_truthy
63
+ end
64
+ end
65
+
66
+ describe "#clear_context" do
67
+ before :each do
68
+ bootstrap
69
+ end
70
+
71
+ it "should clear context given a current one" do
72
+ @config.switch_to("test")
73
+ expect(@config.data['context']).to eq("test")
74
+ @config.reload
75
+ @config.clear_context
76
+ expect(@config.data['context']).to eq(nil)
77
+ end
78
+
79
+ it "should add time to the fulfillment" do
80
+ @config.switch_to("test")
81
+ @config.reload
82
+ @config.clear_context
83
+ expect(@config.data['tasks']['test']['fulfillment']).to be_truthy
84
+ end
85
+ end
86
+
87
+ describe "#clear_fulfillment" do
88
+ before :each do
89
+ bootstrap
90
+ end
91
+
92
+ it "should clear fulfillment for a specified task" do
93
+ @config.switch_to("test")
94
+ @config.reload
95
+ @config.clear_context
96
+ @config.reload
97
+ expect(@config.data['tasks']['test']['fulfillment']).to be_truthy
98
+ @config.clear_fulfillment("test")
99
+ @config.reload
100
+ expect(@config.data['tasks']['test']['fulfillment']).not_to be_truthy
101
+ end
102
+
103
+ it "should clear fulfillment for all tasks otherwise" do
104
+ @config.switch_to("test")
105
+ @config.reload
106
+ @config.clear_context
107
+ @config.reload
108
+ expect(@config.data['tasks']['test']['fulfillment']).to be_truthy
109
+ @config.clear_fulfillment(nil)
110
+ @config.reload
111
+ expect(@config.data['tasks']['test']['fulfillment']).not_to be_truthy
112
+ end
113
+ end
114
+
115
+ describe "#lookup_task" do
116
+ before :each do
117
+ bootstrap
118
+ end
119
+
120
+ it "should return a task name given a corresponding name" do
121
+ input = "test"
122
+ expect(@config.lookup_task(input)).to eq(input)
123
+ end
124
+
125
+ it "should return a task name given a corresponding index" do
126
+ input = "0"
127
+ expected = "test"
128
+ expect(@config.lookup_task(input)).to eq(expected)
129
+ end
130
+
131
+ it "should return nil given a non-task name" do
132
+ input = "non-task"
133
+ expect(@config.lookup_task(input)).to eq(nil)
134
+ end
135
+
136
+ it "should return nil given an out-of-bound index" do
137
+ input = "1"
138
+ expect(@config.lookup_task(input)).to eq(nil)
139
+ end
140
+ end
141
+
142
+ end