syc-task 0.0.1
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/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: []
|