syc-task 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +64 -0
- data/bin/syctask +258 -0
- data/lib/syctask.rb +7 -0
- data/lib/syctask/evaluator.rb +80 -0
- data/lib/syctask/task.rb +229 -0
- data/lib/syctask/task_service.rb +108 -0
- data/lib/syctask/version.rb +5 -0
- data/syctask.rdoc +5 -0
- metadata +168 -0
data/README.rdoc
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
= Simple task organizer
|
2
|
+
With syctask you can organize your tasks (inspired by David Bryant Copland's book <b>Build Awesome Command-Line Applications in Ruby</b>.
|
3
|
+
|
4
|
+
==Install
|
5
|
+
The application can be installed with
|
6
|
+
$ gem install syc-task
|
7
|
+
|
8
|
+
== Usage
|
9
|
+
syctask provides basic task organizer functions as create, update, list und complete a task. Addtional functions are to plan tasks you want to accomplish today. If you are not sure in which sequence to conduct the task you can prioritize them with a pair wise comparisson. You can time tasks with start and stop and you can finally extract tasks from a minutes of meetings file.
|
10
|
+
|
11
|
+
===Create tasks with new
|
12
|
+
Create a new task in the default task directory ~/.tasks
|
13
|
+
$ syctask "My first task"
|
14
|
+
|
15
|
+
Provide a description
|
16
|
+
$ syctask "My first task" --description "Explanation of my first task"
|
17
|
+
|
18
|
+
Schedule a task with a follow-up and due date
|
19
|
+
$ syctask "My first task" --follow-up "2013-02-25" --due "2013-03-11"
|
20
|
+
|
21
|
+
Set a proirity for a task
|
22
|
+
$ syctask "My first task" --prio 3
|
23
|
+
|
24
|
+
Except for --description you can also provide short forms for the options.
|
25
|
+
|
26
|
+
===List tasks
|
27
|
+
List tasks that are not marked as done in short form
|
28
|
+
$ syctask list
|
29
|
+
|
30
|
+
List all tasks in long form
|
31
|
+
$ syctask list --all --complete
|
32
|
+
|
33
|
+
Search tasks that match a pattern
|
34
|
+
$ syctask list --id "<10" --follow_up ">2013-02-25" --title "My \w task"
|
35
|
+
|
36
|
+
===Update tasks
|
37
|
+
Except for title and id all values can be updated. Note and tags are not
|
38
|
+
overridden rather supplemented with the update value.
|
39
|
+
|
40
|
+
Update task with ID 1 and provide some informative note
|
41
|
+
$ syctask update 1 --note "Some explanation about the progress on the task"
|
42
|
+
|
43
|
+
===Complete tasks
|
44
|
+
Complete the task with ID 1 and provide a final note
|
45
|
+
$ syctask done 1 --note "Finalize my first task"
|
46
|
+
|
47
|
+
==Supported platform
|
48
|
+
syc-task has been tested with 1.9.3
|
49
|
+
|
50
|
+
==Notes
|
51
|
+
As with version 0.0.1 only new, update, list and done is implemented.
|
52
|
+
|
53
|
+
The test files live in the folder test and start with test_.
|
54
|
+
|
55
|
+
There is a rake file available to run all tests
|
56
|
+
$ rake test
|
57
|
+
|
58
|
+
==License
|
59
|
+
syc-task is released under the {MIT License}[http://opensource.org/licenses/MIT]
|
60
|
+
|
61
|
+
==Links
|
62
|
+
* [http://www.github.com/sugaryourcoffee/syc-task] - Source code on GitHub
|
63
|
+
* [http://syc.dyndns.org/drupal/wiki/syc-task] - Development notebook
|
64
|
+
* [https://rubygems.org/gems/syc-backup] - RubyGems
|
data/bin/syctask
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'gli'
|
3
|
+
require 'syctask'
|
4
|
+
include GLI::App
|
5
|
+
|
6
|
+
program_desc 'A simple task manager'
|
7
|
+
|
8
|
+
version Syctask::VERSION
|
9
|
+
|
10
|
+
desc 'The directory where tasks are saved to'
|
11
|
+
default_value File.expand_path('~/.tasks')
|
12
|
+
arg_name 'TASK_DIR'
|
13
|
+
flag [:t,:taskdir]
|
14
|
+
|
15
|
+
desc 'Project name where tasks are saved'
|
16
|
+
arg_name 'PROJECT'
|
17
|
+
flag [:p, :project]
|
18
|
+
|
19
|
+
desc 'Create a new task'
|
20
|
+
arg_name 'TASK_TITLE'
|
21
|
+
command :new do |c|
|
22
|
+
c.desc 'Priority of the task, 1 highes priority'
|
23
|
+
c.default_value 3
|
24
|
+
c.arg_name 'PRIO'
|
25
|
+
c.flag [:p, :prio], :type => Integer
|
26
|
+
|
27
|
+
c.desc 'Follow-up date'
|
28
|
+
c.arg_name 'FOLLOW-UP'
|
29
|
+
c.flag [:f, :follow_up], :must_match => /\d{4}-\d{2}-\d{2}/
|
30
|
+
|
31
|
+
c.desc 'Due date'
|
32
|
+
c.arg_name 'DUE'
|
33
|
+
c.flag [:d, :due_date], :must_match => /\d{4}-\d{2}-\d{2}/
|
34
|
+
|
35
|
+
c.desc 'Description of the task'
|
36
|
+
c.arg_name 'DESCRIPTION'
|
37
|
+
c.flag :description
|
38
|
+
|
39
|
+
c.desc 'Note on progress of the task'
|
40
|
+
c.arg_name 'NOTE'
|
41
|
+
c.flag [:n, :note]
|
42
|
+
|
43
|
+
c.desc 'Tags that describe the task'
|
44
|
+
c.arg_name 'TAG1,TAG2,TAG3'
|
45
|
+
c.flag [:t, :tags], :must_match => /^\w+(?:,\w+)*/
|
46
|
+
|
47
|
+
c.action do |global_options,options,args|
|
48
|
+
filter = [:tags, :description, :prio, :due_date, :follow_up,
|
49
|
+
:note]
|
50
|
+
options.keep_if {|key, value| filter.find_index(key)}
|
51
|
+
if args.empty?
|
52
|
+
STDOUT.puts "Reading new tasks from STDIN now... (end with CTRL-D)"
|
53
|
+
args = STDIN.readlines.map {|t| t.chomp}
|
54
|
+
end
|
55
|
+
task_numbers = nil
|
56
|
+
args.each do |title|
|
57
|
+
task_number = @service.create(global_options[:t], options, title)
|
58
|
+
if task_numbers.nil?
|
59
|
+
task_numbers = task_number.to_s
|
60
|
+
else
|
61
|
+
task_numbers += ", #{task_number}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
if args.empty?
|
65
|
+
puts
|
66
|
+
help_now! "You have to provide at least a title to create a task"
|
67
|
+
else
|
68
|
+
STDOUT.puts sprintf("%s %s", "--> created tasks with task numbers",
|
69
|
+
" #{task_numbers}").color(:green)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'Extract tasks from a file'
|
75
|
+
arg_name 'TASK_FILE'
|
76
|
+
command :scan do |c|
|
77
|
+
c.action do |global_options,options,args|
|
78
|
+
puts "scan command not implemented yet"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
desc 'List tasks'
|
83
|
+
command :list do |c|
|
84
|
+
|
85
|
+
c.desc 'List all tasks done and open'
|
86
|
+
c.switch [:a, :all]
|
87
|
+
|
88
|
+
c.desc 'Print complete task'
|
89
|
+
c.switch [:c, :complete]
|
90
|
+
|
91
|
+
c.desc 'Filter for ID'
|
92
|
+
c.arg_name 'ID1,ID2,ID3|[<|=|>]ID'
|
93
|
+
c.flag [:i, :id], :must_match => /^\d+(?:,\d+)*|^[<|=|>]\d+/
|
94
|
+
|
95
|
+
c.desc 'REGEXP as filter for title'
|
96
|
+
c.arg_name 'REGEXP'
|
97
|
+
c.flag [:title]
|
98
|
+
|
99
|
+
c.desc 'Filter for priority'
|
100
|
+
c.arg_name '[<|=|>]PRIO'
|
101
|
+
c.flag [:p, :prio], :must_match => /^\d+|^[<|=|>]\d+/
|
102
|
+
|
103
|
+
c.desc 'Filter for follow-up date'
|
104
|
+
c.arg_name '[<|=|>]DATE'
|
105
|
+
c.flag [:f, :follow_up], :must_match => /^(?:[<|=|>])?\d{4}-\d{2}-\d{2}/
|
106
|
+
|
107
|
+
c.desc 'Filter for due date'
|
108
|
+
c.arg_name '[<|=|>]DATE'
|
109
|
+
c.flag [:d, :due_date], :must_match => /^(?:[<|=|>])?\d{4}-\d{2}-\d{2}/
|
110
|
+
|
111
|
+
c.desc 'REGEXP as filter for description'
|
112
|
+
c.arg_name 'REGEXP'
|
113
|
+
c.flag :description
|
114
|
+
|
115
|
+
c.desc 'REGEXP as filter for note'
|
116
|
+
c.arg_name 'REGEXP'
|
117
|
+
c.flag [:n, :note]
|
118
|
+
|
119
|
+
c.desc 'Tags or REGEXP as filter for tags'
|
120
|
+
c.arg_name 'TAG1,TAG2,TAG3|REGEXP'
|
121
|
+
c.flag [:t, :tags], :must_match => /^\w+(?:,\w+)*|\/.*\//
|
122
|
+
|
123
|
+
c.action do |global_options,options,args|
|
124
|
+
filter = [:id, :tags, :description, :prio, :due_date, :follow_up,
|
125
|
+
:note, :title]
|
126
|
+
all = options[:all]
|
127
|
+
complete = options[:complete]
|
128
|
+
options.keep_if {|key, value| filter.find_index(key) and value != nil}
|
129
|
+
count = 0
|
130
|
+
@service.find(global_options[:t], options, all).each do |task|
|
131
|
+
task.print_pretty(complete)
|
132
|
+
count += 1
|
133
|
+
end
|
134
|
+
STDOUT.puts sprintf("--> found %d tasks", count).color(:green)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
desc 'Plan tasks for today'
|
139
|
+
command :plan do |c|
|
140
|
+
c.action do |global_options,options,args|
|
141
|
+
puts "plan command not implemented yet"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
desc 'Prioritize tasks'
|
146
|
+
command :prio do |c|
|
147
|
+
c.action do |global_options,options,args|
|
148
|
+
puts "prio command not implemented yet"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
desc 'Start and time a task'
|
153
|
+
arg_name 'TASK_NUMBER'
|
154
|
+
command :start do |c|
|
155
|
+
c.action do |global_options,options,args|
|
156
|
+
puts "start command not implemented yet"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
desc 'Stop the running task'
|
161
|
+
command :stop do |c|
|
162
|
+
c.action do |global_options,options,args|
|
163
|
+
puts "stop command not implemented yet"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
desc 'Update the task'
|
168
|
+
arg_name 'TASK_NUMBER'
|
169
|
+
command :update do |c|
|
170
|
+
c.desc 'Priority of the task, 1 highes priority'
|
171
|
+
c.arg_name 'PRIO'
|
172
|
+
c.flag [:p, :prio], :type => Integer
|
173
|
+
|
174
|
+
c.desc 'Follow-up date'
|
175
|
+
c.arg_name 'FOLLOW-UP'
|
176
|
+
c.flag [:f, :follow_up], :must_match => /\d{4}-\d{2}-\d{2}/
|
177
|
+
|
178
|
+
c.desc 'Due date'
|
179
|
+
c.arg_name 'DUE'
|
180
|
+
c.flag [:d, :due_date], :must_match => /\d{4}-\d{2}-\d{2}/
|
181
|
+
|
182
|
+
c.desc 'Description of the task'
|
183
|
+
c.arg_name 'DESCRIPTION'
|
184
|
+
c.flag :description
|
185
|
+
|
186
|
+
c.desc 'Note on progress of the task'
|
187
|
+
c.arg_name 'NOTE'
|
188
|
+
c.flag [:n, :note]
|
189
|
+
|
190
|
+
c.desc 'Tags that describe the task'
|
191
|
+
c.arg_name 'TAG1,TAG2,TAG3'
|
192
|
+
c.flag [:t, :tags], :must_match => /^\w+(?:,\w+)*/
|
193
|
+
|
194
|
+
c.action do |global_options,options,args|
|
195
|
+
help_now! "TASK_NUMBER required" if args.empty?
|
196
|
+
filter = [:tags, :description, :prio, :due_date, :follow_up,
|
197
|
+
:note]
|
198
|
+
options.keep_if {|key, value| filter.find_index(key) and value != nil}
|
199
|
+
|
200
|
+
success = @service.update(global_options[:t], args[0], options)
|
201
|
+
STDOUT.puts sprintf("sucessfully updated task with TASK_NUMBER ", args[0]).color(:green) if success
|
202
|
+
STDOUT.puts sprintf("could not update task with TASK_NUMBER ", args[0]).color(:red) unless success
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
desc 'Mark task as done'
|
207
|
+
arg_name 'TASK_NUMBER'
|
208
|
+
command :done do |c|
|
209
|
+
c.desc 'Print task after marked as done'
|
210
|
+
c.switch [:p, :print]
|
211
|
+
c.desc 'Print complete task'
|
212
|
+
c.switch [:c, :complete]
|
213
|
+
|
214
|
+
c.desc 'Final note for the task'
|
215
|
+
c.arg_name 'NOTE'
|
216
|
+
c.flag [:n, :note]
|
217
|
+
|
218
|
+
c.action do |global_options,options,args|
|
219
|
+
help_now!('TASK_NUMBER is required') if args.empty?
|
220
|
+
task = @service.read(global_options[:t], args[0])
|
221
|
+
exit_now!("Task with TASK_NUMBER #{args[0]} does not exist") unless task
|
222
|
+
task.done(options[:note])
|
223
|
+
@service.save(global_options[:t], task)
|
224
|
+
STDOUT.puts sprintf("Marked task with TASK_NUMBER %d as done", args[0]).color(:green)
|
225
|
+
task.print_pretty(options[:c]) if options[:p]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
pre do |global,command,options,args|
|
230
|
+
# Pre logic here
|
231
|
+
# Return true to proceed; false to abort and not call the
|
232
|
+
# chosen command
|
233
|
+
# Use skips_pre before a command to skip this block
|
234
|
+
# on that command only
|
235
|
+
|
236
|
+
@service = Syctask::TaskService.new
|
237
|
+
|
238
|
+
dir = File.expand_path(global[:t])
|
239
|
+
dir += "/" + global[:p] if global[:p]
|
240
|
+
global[:taskdir] = global[:t] = dir.squeeze("/")
|
241
|
+
|
242
|
+
|
243
|
+
true
|
244
|
+
end
|
245
|
+
|
246
|
+
post do |global,command,options,args|
|
247
|
+
# Post logic here
|
248
|
+
# Use skips_post before a command to skip this
|
249
|
+
# block on that command only
|
250
|
+
end
|
251
|
+
|
252
|
+
on_error do |exception|
|
253
|
+
# Error logic here
|
254
|
+
# return false to skip default error handling
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
exit run(ARGV)
|
data/lib/syctask.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Syctask provides functions for managing tasks in a task list.
|
2
|
+
module Syctask
|
3
|
+
|
4
|
+
# Evaluator provides different evaluatons for comparing numbers,
|
5
|
+
# dates and strings. Also provides methods to check whether a value is part
|
6
|
+
# of a list
|
7
|
+
class Evaluator
|
8
|
+
# Pattern to match operands <|=|> followed by a number or a single number
|
9
|
+
NUMBER_COMPARE_PATTERN = /^(<|=|>)(\d+)|^(\d+)/
|
10
|
+
# Pattern to match comma separated values
|
11
|
+
CSV_PATTERN = /\w+(?=,)|(?<!<|=|>)\w+$/
|
12
|
+
# Pattern to match comma separated numbers
|
13
|
+
NUMBER_CSV_PATTERN = /\d+(?=,)|\d+$/
|
14
|
+
# Pattern to match a date prepended by <|=|> or a single date
|
15
|
+
DATE_COMPARE_PATTERN = /^(<|=|>)(\d{4}-\d{2}-\d{2})|(\d{4}-\d{2}-\d{2})/
|
16
|
+
# Pattern to match a date in the form of yyyy-mm-dd
|
17
|
+
DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/
|
18
|
+
# Pattern that matches anything that is not prepended with <|=|>
|
19
|
+
NON_COMPARE_PATTERN = /[^<=>]*/
|
20
|
+
|
21
|
+
# Compares two numbers regarding <|=|>. Returns true if the comparisson
|
22
|
+
# succeeds otherwise false. If eather value or pattern is nil false is
|
23
|
+
# returned. If value is empty false is returned.
|
24
|
+
def compare_numbers(value, pattern)
|
25
|
+
return false if value.nil? or pattern.nil?
|
26
|
+
return false if value.class == String and value.empty?
|
27
|
+
result = pattern.match(NUMBER_COMPARE_PATTERN)
|
28
|
+
return false unless result
|
29
|
+
compare(value, result.captures)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Compares two dates regarding <|=|>. Returns true if the comparisson
|
33
|
+
# succeeds otherwise false. If eather value or pattern is nil false is
|
34
|
+
# returned.
|
35
|
+
def compare_dates(value, pattern)
|
36
|
+
return false if value.nil? or pattern.nil?
|
37
|
+
result = pattern.match(DATE_COMPARE_PATTERN)
|
38
|
+
return false unless result
|
39
|
+
value = "'#{value}'"
|
40
|
+
captures = result.captures.collect! do |c|
|
41
|
+
c and c.match(DATE_PATTERN) ? "'#{c}'" : c
|
42
|
+
end
|
43
|
+
compare(value, captures)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Compares two values regarding <|=|>. Returns true if the comparisson
|
47
|
+
# succeeds otherwise false. If eather value or operand is nil false is
|
48
|
+
# returned.
|
49
|
+
def compare(value, operands)
|
50
|
+
|
51
|
+
if operands[2]
|
52
|
+
operands[0] = "=="
|
53
|
+
operands[1] = operands[2]
|
54
|
+
elsif operands[0] == "="
|
55
|
+
operands[0] = "=="
|
56
|
+
end
|
57
|
+
|
58
|
+
expression = "#{value} #{operands[0]} #{operands[1]}"
|
59
|
+
eval(expression)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Evaluates whether value is part of the provided csv pattern. Returns true
|
63
|
+
# if it evaluates to true otherwise false. If value or pattern is nil false
|
64
|
+
# is returned.
|
65
|
+
def includes?(value, pattern)
|
66
|
+
return false if value.nil? or pattern.nil?
|
67
|
+
captures = pattern.scan(CSV_PATTERN)
|
68
|
+
!captures.find_index(value.to_s).nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Evaluates if value matches the provided regex. Returns true if a match is
|
72
|
+
# found. If value or regex is nil false is returned.
|
73
|
+
def matches?(value, regex)
|
74
|
+
return false if value.nil? or regex.nil?
|
75
|
+
!value.match(Regexp.new(regex, true)).nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/lib/syctask/task.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rainbow'
|
3
|
+
require_relative 'evaluator'
|
4
|
+
|
5
|
+
# Syctask provides functions for managing tasks in a task list
|
6
|
+
module Syctask
|
7
|
+
|
8
|
+
# A Task is the basic element of the task list and holds all information
|
9
|
+
# about a task.
|
10
|
+
class Task
|
11
|
+
|
12
|
+
# Holds the options of the task.
|
13
|
+
# Options are
|
14
|
+
# * description - additional information about the task
|
15
|
+
# * follow_up - follow-up date of the task
|
16
|
+
# * due - due date of the task
|
17
|
+
# * prio - priority of the task
|
18
|
+
# * note - information about the progress or state of the task
|
19
|
+
# * tags - can be used to search for tasks that belong to a certain category
|
20
|
+
attr_accessor :options
|
21
|
+
# Title of the class
|
22
|
+
attr_reader :title
|
23
|
+
# ID of the task
|
24
|
+
attr_reader :id
|
25
|
+
# Creation date
|
26
|
+
attr_reader :creation_date
|
27
|
+
# Update date
|
28
|
+
attr_reader :update_date
|
29
|
+
# Done date
|
30
|
+
attr_reader :done_date
|
31
|
+
# Directory where the file of the task is located
|
32
|
+
attr_reader :dir
|
33
|
+
|
34
|
+
# Creates a new task. If the options contain a note than the current date
|
35
|
+
# and time is added.
|
36
|
+
def initialize(options={}, title, id)
|
37
|
+
@creation_date = Time.now.strftime("%Y-%m-%d - %H:%M:%S")
|
38
|
+
@title = title
|
39
|
+
@options = options
|
40
|
+
@options[:note] =
|
41
|
+
"#{@creation_date}\n#{@options[:note]}\n" if @options[:note]
|
42
|
+
@id = id
|
43
|
+
end
|
44
|
+
|
45
|
+
# Updates the task with new values. Except for note and tags which are
|
46
|
+
# supplemented with the new values and not overridden.
|
47
|
+
def update(options)
|
48
|
+
@update_date = Time.now.strftime("%Y-%m-%d - %H:%M:%S")
|
49
|
+
options.keys.each do |key|
|
50
|
+
new_value = options[key]
|
51
|
+
|
52
|
+
case key
|
53
|
+
when :note
|
54
|
+
new_value = "#{@update_date}\n#{new_value}\n#{@options[key]}"
|
55
|
+
when :tags
|
56
|
+
if @options[key].include? new_value
|
57
|
+
new_value = @options[key]
|
58
|
+
else
|
59
|
+
new_value = "#{@options[key]},#{new_value}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@options[key] = new_value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks whether this task has been updated. Returns true if updated
|
68
|
+
# otherwise false
|
69
|
+
def update?
|
70
|
+
!@updated_date.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Marks the task as done. When done than the done date is set. Optionally a
|
74
|
+
# note can be provided.
|
75
|
+
def done(note="")
|
76
|
+
@done_date = Time.now.strftime("%Y-%m-%d - %H:%M:%S")
|
77
|
+
if note
|
78
|
+
options[:note] = "#{@done_date}\n#{note}\n#{@options[:note]}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Checks if this task is done. Returns true if done otherwise false
|
83
|
+
def done?
|
84
|
+
!@done_date.nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Compares the provided elements in the filter with the correspondent
|
88
|
+
# elements in the task. When all comparissons match than true is returned.
|
89
|
+
# If one comparisson does not match false is returned. If filter is empty
|
90
|
+
# than true is returned. The values can be compared regarding <, =, > or
|
91
|
+
# whether the task's value is part of a list of provided values. It is also
|
92
|
+
# possible to provide a regex as a filter. Following comparissons are
|
93
|
+
# available
|
94
|
+
# Value Compare
|
95
|
+
# :title regex
|
96
|
+
# :description regex
|
97
|
+
# :id contains, <|=|> no operator same as =
|
98
|
+
# :prio contains, <|=|> no operator same as =
|
99
|
+
# :tags contains, regex
|
100
|
+
# :follow_up <|=|>
|
101
|
+
# :due <|=|>
|
102
|
+
def matches?(filter = {})
|
103
|
+
return true if filter.empty?
|
104
|
+
evaluator = Evaluator.new
|
105
|
+
filter.each do |key, value|
|
106
|
+
matches = false
|
107
|
+
case key
|
108
|
+
when :title, :t
|
109
|
+
matches = evaluator.matches?(@title, value)
|
110
|
+
when :description
|
111
|
+
matches = evaluator.matches?(@options[:description], value)
|
112
|
+
when :id, :i, "id", "i"
|
113
|
+
matches = (evaluator.includes?(@id, value) or
|
114
|
+
evaluator.compare_numbers(@id, value))
|
115
|
+
when :prio, :p
|
116
|
+
matches = (evaluator.includes?(@options[:prio], value) or
|
117
|
+
evaluator.compare(@options[:prio], value))
|
118
|
+
when :tags
|
119
|
+
matches = evaluator.matches?(@options[:tags], value)
|
120
|
+
when :follow_up, :f, :d, :due_date
|
121
|
+
matches = evaluator.compare_dates(@options[key], value)
|
122
|
+
end
|
123
|
+
return false unless matches
|
124
|
+
end
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
# Prints the task in a formatted way eather all values when long is true
|
129
|
+
# or only id, title, prio, follow-up and due date.
|
130
|
+
def print_pretty(long=false)
|
131
|
+
pretty_string(long)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Prints the task as a CSV
|
135
|
+
def print_csv
|
136
|
+
STDOUT.puts(csv_string)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# Creates the directory if it does not exist
|
142
|
+
def create_dir(dir)
|
143
|
+
fileutils.mkdir_p dir unless file.exists? dir
|
144
|
+
end
|
145
|
+
|
146
|
+
# creates the task's id based on the tasks available in the task directory.
|
147
|
+
# the task's file name is in the form id.task. create_task_id determines
|
148
|
+
# the biggest number and adds one to create the task's id.
|
149
|
+
def create_task_id
|
150
|
+
tasks = dir.glob("#{@dir}/*")
|
151
|
+
ids = []
|
152
|
+
tasks.each {|task| ids << task.scan(/^\d+(?=\.task)/)[0].to_i }
|
153
|
+
if ids.empty?
|
154
|
+
@id = 1
|
155
|
+
elsif
|
156
|
+
@id = ids[ids.size-1] + 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Prints the task formatted. Values that are nil are not printed. A type all
|
161
|
+
# will print all available values. Otherwise only ID, title, description,
|
162
|
+
# prio, follow-up and due date are printed.
|
163
|
+
def pretty_string(long)
|
164
|
+
color = :default
|
165
|
+
color = :green if self.done?
|
166
|
+
|
167
|
+
puts sprintf("%04d - %s", @id, @title.bright).color(color)
|
168
|
+
puts sprintf("%6s %s", " ", @options[:description]).color(color) if @options[:description]
|
169
|
+
puts sprintf("%6s Prio: %s", " ", @options[:prio]).color(color) if @options[:prio]
|
170
|
+
puts sprintf("%6s Follow-up: %s", " ", @options[:follow_up]).color(color) if @options[:follow_up]
|
171
|
+
puts sprintf("%6s Due: %s", " ", @options[:due]).color(color) if @options[:due]
|
172
|
+
if long
|
173
|
+
if @options[:note]
|
174
|
+
note = split_lines(@options[:note].chomp, 70)
|
175
|
+
note = note.chomp.
|
176
|
+
gsub(/\n(?!\d{4}-\d{2}-\d{2} - \d{2}:\d{2}:\d{2})/, "\n#{' '*9}")
|
177
|
+
note = note.
|
178
|
+
gsub(/\n(?=\d{4}-\d{2}-\d{2} - \d{2}:\d{2}:\d{2})/, "\n#{' '*7}")
|
179
|
+
puts sprintf("%6s %s", " ", note.chomp).color(color)
|
180
|
+
end
|
181
|
+
puts sprintf("%6s Tags: %s", " ", @options[:tags]).color(color) if @options[:tags]
|
182
|
+
puts sprintf("%6s Created: %s", " ", @creation_date).color(color)
|
183
|
+
puts sprintf("%6s Updated: %s", " ", @update_date).color(color) if @update_date
|
184
|
+
puts sprintf("%6s Closed: %s", " ", @done_date).color(color) if @done_date
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Prints all values as a csv separated with ";". This string can be read by
|
189
|
+
# another application. The values are
|
190
|
+
# id;title;description;prio;follow-up;due;note;tags;created;
|
191
|
+
# updated|UNCHANGED;DONE|OPEN
|
192
|
+
def csv_string
|
193
|
+
string = "\n#{@id};#{@title};"
|
194
|
+
string +" #{@options[:description]};#{@options[:prio]};"
|
195
|
+
string += "#{@options[:follow_up]};#{@options[:due]};"
|
196
|
+
string += "#{@options[:note].gsub(/\n/, '\\n')};"
|
197
|
+
string += "#{@options[:tags]};"
|
198
|
+
string += "#{@creation_date};"
|
199
|
+
string += "#{@udpate_date ? "UPDATED" : "UNCHANGED"};"
|
200
|
+
string += "#{@done_date ? "DONE" : "OPEN"}\n"
|
201
|
+
string
|
202
|
+
end
|
203
|
+
|
204
|
+
# Splits a string to size (chars) less or equal to length
|
205
|
+
def split_lines(string, length)
|
206
|
+
lines = string.squeeze(" ").split("\n")
|
207
|
+
i = 0
|
208
|
+
new_lines = []
|
209
|
+
new_lines[i] = ""
|
210
|
+
lines.each do |line|
|
211
|
+
line.squeeze(" ").split.each do |w|
|
212
|
+
if new_lines[i].length + w.length < length
|
213
|
+
new_lines[i] += "#{w} "
|
214
|
+
else
|
215
|
+
i += 1
|
216
|
+
new_lines[i] = "#{w} "
|
217
|
+
end
|
218
|
+
end
|
219
|
+
i += 1
|
220
|
+
new_lines[i] = ""
|
221
|
+
end
|
222
|
+
text = ""
|
223
|
+
new_lines.each {|l| text << "#{l}\n"}
|
224
|
+
text.chomp
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# Syctask provides functions for managing tasks in a task list
|
4
|
+
module Syctask
|
5
|
+
|
6
|
+
# Provides services to operate tasks as create, read, find, update and save
|
7
|
+
# Task objects
|
8
|
+
class TaskService
|
9
|
+
|
10
|
+
# Creates a new task in the specified directory, with the specified options
|
11
|
+
# and the specified title. If the directory doesn't exist it is created.
|
12
|
+
# When the task is created it is assigned a unique ID within the directory.
|
13
|
+
# Options are
|
14
|
+
# * description - additional information about the task
|
15
|
+
# * follow_up - follow-up date of the task
|
16
|
+
# * due - due date of the task
|
17
|
+
# * prio - priority of the task
|
18
|
+
# * note - information about the progress or state of the task
|
19
|
+
# * tags - can be used to searching tasks that belong to a certain category
|
20
|
+
def create(dir, options, title)
|
21
|
+
create_dir(dir)
|
22
|
+
task = Task.new(options, title, create_id(dir))
|
23
|
+
save(dir, task)
|
24
|
+
task.id
|
25
|
+
end
|
26
|
+
|
27
|
+
# Reads the task with given ID id located in given directory dir. If task
|
28
|
+
# does not exist nil is returned otherwise the task is returned
|
29
|
+
def read(dir, id)
|
30
|
+
task = nil
|
31
|
+
Dir.glob("#{dir}/*").each do |file|
|
32
|
+
task = YAML.load_file(file) if File.file? file
|
33
|
+
return task if task and task.id == id.to_i
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Finds all tasks that match the given filter. The filter can be provided
|
39
|
+
# for :id, :title, :description, :follow_up, :due, :tags and :prio.
|
40
|
+
# id can be eather a selection of IDs ID1,ID2,ID3 or a comparison <|=|>ID.
|
41
|
+
# title and :description can be a REGEX as /look for \d+ examples/
|
42
|
+
# follow-up and :due can be <|=|>DATE
|
43
|
+
# tags can be eather a selection TAG1,TAG2,TAG3 or a REGEX /[Ll]ecture/
|
44
|
+
# prio can be <|=|>PRIO
|
45
|
+
def find(dir, filter={}, all=true)
|
46
|
+
tasks = []
|
47
|
+
Dir.glob("#{dir}/*").sort.each do |file|
|
48
|
+
File.file?(file) ? task = YAML.load_file(file) : next
|
49
|
+
next if task and not all and task.done?
|
50
|
+
next if not task
|
51
|
+
tasks << task if task.matches?(filter)
|
52
|
+
end
|
53
|
+
tasks
|
54
|
+
end
|
55
|
+
|
56
|
+
# Updates the task with the given id in the given directory dir with the
|
57
|
+
# provided options.
|
58
|
+
# Options are
|
59
|
+
# * description - additional information about the task
|
60
|
+
# * follow_up - follow-up date of the task
|
61
|
+
# * due - due date of the task
|
62
|
+
# * prio - priority of the task
|
63
|
+
# * note - information about the progress or state of the task
|
64
|
+
# * tags - can be used to searching tasks that belong to a certain category
|
65
|
+
# Except for note and tags the values of the task are overridden with the
|
66
|
+
# new value. If note and tags are provided these are added to the existing
|
67
|
+
# values.
|
68
|
+
def update(dir, id, options)
|
69
|
+
task_file = Dir.glob("#{dir}/#{id}.task")[0]
|
70
|
+
task = YAML.load_file(task_file) if task_file
|
71
|
+
updated = false
|
72
|
+
if task
|
73
|
+
task.update(options)
|
74
|
+
save(dir, task)
|
75
|
+
updated = true
|
76
|
+
end
|
77
|
+
updated
|
78
|
+
end
|
79
|
+
|
80
|
+
# Saves the task to the task directory
|
81
|
+
def save(dir, task)
|
82
|
+
File.open("#{dir}/#{task.id}.task", 'w') {|f| YAML.dump(task, f)}
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Creates the task directory if it does not exist
|
88
|
+
def create_dir(dir)
|
89
|
+
FileUtils.mkdir_p dir unless File.exists? dir
|
90
|
+
end
|
91
|
+
|
92
|
+
# Creates the task's ID based on the tasks available in the task directory.
|
93
|
+
# The task's file name is in the form ID.task. create_id determines
|
94
|
+
# the biggest number and adds one to create the task's ID.
|
95
|
+
def create_id(dir)
|
96
|
+
tasks = Dir.glob("#{dir}/*")
|
97
|
+
ids = []
|
98
|
+
tasks.each do |task|
|
99
|
+
id = File.basename(task).scan(/^\d+(?=\.task)/)[0]
|
100
|
+
ids << id.to_i if id
|
101
|
+
end
|
102
|
+
ids.empty? ? 1 : ids.sort[ids.size-1] + 1
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
data/syctask.rdoc
ADDED
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: syc-task
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Pierre Sugar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rdoc
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: aruba
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: gli
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.5.4
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - '='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.5.4
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rainbow
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: ! "= Simple task organizer\nWith syctask you can organize your tasks
|
95
|
+
(inspired by David Bryant Copland's book <b>Build Awesome Command-Line Applications
|
96
|
+
in Ruby</b>.\n\n==Install\nThe application can be installed with\n $ gem install
|
97
|
+
syc-task\n\n== Usage\nsyctask provides basic task organizer functions as create,
|
98
|
+
update, list und complete a task. Addtional functions are to plan tasks you want
|
99
|
+
to accomplish today. If you are not sure in which sequence to conduct the task you
|
100
|
+
can prioritize them with a pair wise comparisson. You can time tasks with start
|
101
|
+
and stop and you can finally extract tasks from a minutes of meetings file.\n\n===Create
|
102
|
+
tasks with new\nCreate a new task in the default task directory ~/.tasks\n $
|
103
|
+
syctask \"My first task\"\n\nProvide a description\n $ syctask \"My first task\"
|
104
|
+
--description \"Explanation of my first task\"\n\nSchedule a task with a follow-up
|
105
|
+
and due date\n $ syctask \"My first task\" --follow-up \"2013-02-25\" --due \"2013-03-11\"\n\nSet
|
106
|
+
a proirity for a task\n $ syctask \"My first task\" --prio 3\n\nExcept for --description
|
107
|
+
you can also provide short forms for the options.\n\n===List tasks\nList tasks that
|
108
|
+
are not marked as done in short form\n $ syctask list\n\nList all tasks in long
|
109
|
+
form\n $ syctask list --all --complete\n\nSearch tasks that match a pattern\n
|
110
|
+
\ $ syctask list --id \"<10\" --follow_up \">2013-02-25\" --title \"My \\w task\"\n\n===Update
|
111
|
+
tasks\nExcept for title and id all values can be updated. Note and tags are not\noverridden
|
112
|
+
rather supplemented with the update value.\n\nUpdate task with ID 1 and provide
|
113
|
+
some informative note\n $ syctask update 1 --note \"Some explanation about the
|
114
|
+
progress on the task\"\n\n===Complete tasks\nComplete the task with ID 1 and provide
|
115
|
+
a final note\n $ syctask done 1 --note \"Finalize my first task\"\n\n==Supported
|
116
|
+
platform\nsyc-task has been tested with 1.9.3\n\n==Notes\nAs with version 0.0.1
|
117
|
+
only new, update, list and done is implemented.\n\nThe test files live in the folder
|
118
|
+
test and start with test_.\n\nThere is a rake file available to run all tests\n
|
119
|
+
\ $ rake test\n\n==License\nsyc-task is released under the {MIT License}[http://opensource.org/licenses/MIT]\n\n==Links\n*
|
120
|
+
[http://www.github.com/sugaryourcoffee/syc-task] - Source code on GitHub\n* [http://syc.dyndns.org/drupal/wiki/syc-task]
|
121
|
+
- Development notebook\n* [https://rubygems.org/gems/syc-backup] - RubyGems\n"
|
122
|
+
email: pierre@sugaryourcoffee.de
|
123
|
+
executables:
|
124
|
+
- syctask
|
125
|
+
extensions: []
|
126
|
+
extra_rdoc_files:
|
127
|
+
- README.rdoc
|
128
|
+
- syctask.rdoc
|
129
|
+
files:
|
130
|
+
- bin/syctask
|
131
|
+
- lib/syctask/version.rb
|
132
|
+
- lib/syctask/task.rb
|
133
|
+
- lib/syctask/task_service.rb
|
134
|
+
- lib/syctask/evaluator.rb
|
135
|
+
- lib/syctask.rb
|
136
|
+
- README.rdoc
|
137
|
+
- syctask.rdoc
|
138
|
+
homepage: http://syc.dyndns.org/drupal/syc-task
|
139
|
+
licenses: []
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options:
|
142
|
+
- --title
|
143
|
+
- syctask
|
144
|
+
- --main
|
145
|
+
- README.rdoc
|
146
|
+
- -ri
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
- lib
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
151
|
+
none: false
|
152
|
+
requirements:
|
153
|
+
- - ! '>='
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
|
+
none: false
|
158
|
+
requirements:
|
159
|
+
- - ! '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubyforge_project:
|
164
|
+
rubygems_version: 1.8.25
|
165
|
+
signing_key:
|
166
|
+
specification_version: 3
|
167
|
+
summary: Simple task organizer
|
168
|
+
test_files: []
|