todorb 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +12 -0
- data/Makefile +29 -0
- data/README.markdown +48 -3
- data/Rakefile +54 -0
- data/VERSION +1 -1
- data/bin/todorb +1 -1
- data/lib/common/cmdapp.rb +170 -0
- data/lib/common/sed.rb +84 -44
- data/lib/todorb.rb +508 -266
- data/tests/Makefile +2 -0
- data/tests/README +313 -0
- data/tests/aggregate-results.sh +34 -0
- data/tests/clean.sh +3 -0
- data/tests/dataset1.txt +14 -0
- data/tests/recreate.sh +33 -0
- data/tests/rtest2.sh +124 -0
- data/tests/t0001-add.sh +22 -0
- data/tests/t0002-listing.sh +58 -0
- data/tests/test-lib.sh +614 -0
- data/todorb.gemspec +62 -0
- metadata +22 -6
data/lib/todorb.rb
CHANGED
@@ -10,23 +10,56 @@
|
|
10
10
|
require 'rubygems'
|
11
11
|
#require 'csv'
|
12
12
|
require 'common/colorconstants'
|
13
|
-
include ColorConstants
|
14
13
|
require 'common/sed'
|
14
|
+
require 'common/cmdapp'
|
15
|
+
include ColorConstants
|
15
16
|
include Sed
|
17
|
+
include Cmdapp
|
16
18
|
|
17
19
|
PRI_A = YELLOW + BOLD
|
18
20
|
PRI_B = WHITE + BOLD
|
19
21
|
PRI_C = GREEN + BOLD
|
20
22
|
PRI_D = CYAN + BOLD
|
21
|
-
VERSION = "
|
23
|
+
VERSION = "2.0"
|
22
24
|
DATE = "2010-06-10"
|
23
|
-
APPNAME = $0
|
25
|
+
APPNAME = File.basename($0)
|
24
26
|
AUTHOR = "rkumar"
|
27
|
+
TABSTOP = 4 # indentation of subtasks
|
25
28
|
|
26
29
|
class Todo
|
30
|
+
# This class is responsible for all todo task related functionality.
|
31
|
+
#
|
32
|
+
# == Create a file
|
33
|
+
# Adding a task is the first operation.
|
34
|
+
# $ todorb add "Create a project in rubyforge"
|
35
|
+
# $ todorb add "Update Rakefile with project name"
|
36
|
+
# The above creates a TODO2.txt file and a serial_number file.
|
37
|
+
#
|
38
|
+
# == List tasks
|
39
|
+
# To list open/unstarted tasks:
|
40
|
+
# $ todorb
|
41
|
+
# To list closed tasks also:
|
42
|
+
# $ todorb --show-all
|
43
|
+
#
|
44
|
+
# If you are located elsewhere, give directory name:
|
45
|
+
# $ todorb -d ~/
|
46
|
+
#
|
47
|
+
# == Close a task (mark as done)
|
48
|
+
# $ todorb status close 1
|
49
|
+
#
|
50
|
+
# == Add priority
|
51
|
+
# $ todorb pri A 2
|
52
|
+
#
|
53
|
+
# For more:
|
54
|
+
# $ todorb --help
|
55
|
+
# $ todorb --show-actions
|
56
|
+
#
|
57
|
+
# == TODO:
|
58
|
+
#
|
27
59
|
def initialize options, argv
|
28
60
|
|
29
61
|
@options = options
|
62
|
+
@aliases = {}
|
30
63
|
@argv = argv
|
31
64
|
@file = options[:file]
|
32
65
|
## data is a 2 dim array: rows and fields. It contains each row of the file
|
@@ -35,62 +68,59 @@ class Todo
|
|
35
68
|
init_vars
|
36
69
|
end
|
37
70
|
def init_vars
|
38
|
-
@
|
39
|
-
@
|
40
|
-
#@
|
41
|
-
@
|
42
|
-
@archive_path = "todo_archive.txt"
|
71
|
+
@app_default_action = "list"
|
72
|
+
@app_file_path = @options[:file] || "TODO2.txt"
|
73
|
+
#@app_serial_path = File.expand_path("~/serial_numbers")
|
74
|
+
@app_serial_path = "serial_numbers"
|
75
|
+
@archive_path = "todo_archive.txt"
|
76
|
+
@deleted_path = "todo_deleted.txt"
|
43
77
|
@todo_delim = "\t"
|
44
78
|
@appname = File.basename( Dir.getwd ) #+ ".#{$0}"
|
79
|
+
# in order to support the testing framework
|
45
80
|
t = Time.now
|
81
|
+
ut = ENV["TODO_TEST_TIME"]
|
82
|
+
t = Time.at(ut.to_i) if ut
|
46
83
|
@now = t.strftime("%Y-%m-%d %H:%M:%S")
|
47
84
|
@today = t.strftime("%Y-%m-%d")
|
48
85
|
@verbose = @options[:verbose]
|
86
|
+
$valid_array = false
|
87
|
+
# menu MENU
|
49
88
|
#@actions = %w[ list add pri priority depri tag del delete status redo note archive help]
|
50
89
|
@actions = {}
|
51
90
|
@actions["list"] = "List all tasks.\n\t --hide-numbering --renumber"
|
52
|
-
@actions["
|
53
|
-
@actions["
|
91
|
+
@actions["listsub"] = "List all tasks.\n\t --hide-numbering --renumber"
|
92
|
+
@actions["add"] = "Add a task. \n\t #{$APPNAME} add <TEXT>\n\t --component C --project P --priority X add <TEXT>"
|
93
|
+
@actions["pri"] = "Add priority to task. \n\t #{$APPNAME} pri <ITEM> [A-Z]"
|
54
94
|
@actions["priority"] = "Same as pri"
|
55
|
-
@actions["depri"] = "Remove priority of task. \n\t #{$
|
56
|
-
@actions["delete"] = "Delete a task. \n\t #{$
|
95
|
+
@actions["depri"] = "Remove priority of task. \n\t #{$APPNAME} depri <ITEM>"
|
96
|
+
@actions["delete"] = "Delete a task. \n\t #{$APPNAME} delete <ITEM>"
|
57
97
|
@actions["del"] = "Same as delete"
|
58
|
-
@actions["status"] = "Change the status of a task. \n\t #{$
|
98
|
+
@actions["status"] = "Change the status of a task. \n\t #{$APPNAME} status <STAT> <ITEM>\n\t<STAT> are open closed started pending hold next"
|
59
99
|
@actions["redo"] = "Renumbers the todo file starting 1"
|
60
|
-
@actions["note"] = "Add a note to an item. \n\t #{$
|
100
|
+
@actions["note"] = "Add a note to an item. \n\t #{$APPNAME} note <ITEM> <TEXT>"
|
101
|
+
@actions["tag"] = "Add a tag to an item/s. \n\t #{$APPNAME} tag <ITEMS> <TEXT>"
|
61
102
|
@actions["archive"] = "archive closed tasks to archive.txt"
|
103
|
+
@actions["copyunder"] = "Move first item under second (as a subtask). aka cu"
|
104
|
+
|
62
105
|
@actions["help"] = "Display help"
|
106
|
+
@actions["addsub"] = "Add a task under another . \n\t #{$APPNAME} add <TEXT>\n\t --component C --project P --priority X add <TEXT>"
|
63
107
|
|
108
|
+
# adding some sort of aliases so shortcuts can be defined
|
109
|
+
@aliases["open"] = ["status","open"]
|
110
|
+
@aliases["close"] = ["status","closed"]
|
111
|
+
@aliases["cu"] = ["copyunder"]
|
64
112
|
|
65
|
-
#
|
113
|
+
@copying = false # used by copyunder when it calls addsub
|
114
|
+
# TODO: config
|
66
115
|
# we need to read up from config file and update
|
67
116
|
end
|
68
|
-
# menu MENU
|
69
|
-
def run
|
70
|
-
@action = @argv[0] || @todo_default_action
|
71
|
-
@action = @action.downcase
|
72
|
-
@action.sub!('priority', 'pri')
|
73
|
-
@action.sub!(/^del$/, 'delete')
|
74
|
-
|
75
|
-
|
76
|
-
@argv.shift
|
77
|
-
if @actions.include? @action
|
78
|
-
send(@action, @argv)
|
79
|
-
else
|
80
|
-
help @argv
|
81
|
-
end
|
82
|
-
end
|
83
|
-
def help args
|
84
|
-
#puts "Actions are #{@actions.join(", ")} "
|
85
|
-
@actions.each_pair { |name, val| puts "#{name}\t#{val}" }
|
86
|
-
end
|
87
117
|
def add args
|
88
118
|
if args.empty?
|
89
119
|
print "Enter todo: "
|
90
120
|
STDOUT.flush
|
91
121
|
text = gets.chomp
|
92
122
|
if text.empty?
|
93
|
-
exit
|
123
|
+
exit ERRCODE
|
94
124
|
end
|
95
125
|
Kernel.print("You gave me '#{text}'") if @verbose
|
96
126
|
else
|
@@ -101,70 +131,103 @@ class Todo
|
|
101
131
|
text.tr! "\n", ''
|
102
132
|
Kernel.print("Got '#{text}'\n") if @verbose
|
103
133
|
item = _get_serial_number
|
134
|
+
die "Could not get a new item number" if item.nil?
|
104
135
|
paditem = _paditem(item)
|
105
|
-
|
136
|
+
verbose "item no is:#{paditem}:\n"
|
106
137
|
priority = @options[:priority] ? " (#{@options[:priority]})" : ""
|
107
138
|
project = @options[:project] ? " +#{@options[:project]}" : ""
|
108
139
|
component = @options[:component] ? " @#{@options[:component]}" : ""
|
109
140
|
newtext="#{paditem}#{@todo_delim}[ ]#{priority}#{project}#{component} #{text} (#{@today})"
|
141
|
+
File.open(@app_file_path, "a") { | file| file.puts newtext }
|
110
142
|
puts "Adding:"
|
111
143
|
puts newtext
|
112
|
-
File.open(@todo_file_path, "a") { | file| file.puts newtext }
|
113
144
|
|
145
|
+
0
|
114
146
|
end
|
115
147
|
##
|
116
|
-
#
|
117
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
148
|
+
# add a subtask
|
149
|
+
# @param [Array] 1. item under which to place, 2. text
|
150
|
+
# @return
|
151
|
+
# @example:
|
152
|
+
# addsub 1 "A task"
|
153
|
+
# => will get added as 1.1 or 1.2 etc
|
154
|
+
# addsub 1.3 "a task"
|
155
|
+
# => will get added as 1.3.x
|
156
|
+
def addsub args
|
157
|
+
under = args.shift
|
158
|
+
text = args.join " "
|
159
|
+
exit unless text
|
160
|
+
#puts "under #{under} text: #{text} "
|
161
|
+
lastlinect = nil
|
162
|
+
lastlinetext = nil
|
163
|
+
# look for last item below given task (if there is)
|
164
|
+
Sed::egrep( [@app_file_path], Regexp.new("#{under}\.[0-9]+ ")) do |fn,ln,line|
|
165
|
+
lastlinect = ln
|
166
|
+
lastlinetext = line
|
167
|
+
puts line if @verbose
|
129
168
|
end
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
169
|
+
if lastlinect
|
170
|
+
verbose "Last line found #{lastlinetext} "
|
171
|
+
m = lastlinetext.match(/\.([0-9]+) /)
|
172
|
+
lastindex = m[1].to_i
|
173
|
+
# check if it has subitems, find last one only for linecount
|
174
|
+
Sed::egrep( [@app_file_path], Regexp.new("#{under}\.#{lastindex}\.[0-9]+ ")) do |fn,ln,line|
|
175
|
+
lastlinect = ln
|
176
|
+
end
|
177
|
+
lastindex += 1
|
178
|
+
item = "#{under}.#{lastindex}"
|
179
|
+
else
|
180
|
+
# no subitem found, so this is first
|
181
|
+
item = "#{under}.1"
|
182
|
+
# get line of parent
|
183
|
+
found = nil
|
184
|
+
Sed::egrep( [@app_file_path], Regexp.new("#{under} ")) do |fn,ln,line|
|
185
|
+
lastlinect = ln
|
186
|
+
found = true
|
187
|
+
end
|
188
|
+
die "Task #{under} not found" unless found
|
138
189
|
end
|
139
|
-
|
190
|
+
die "Could not determine which line to insert under" unless lastlinect
|
191
|
+
verbose "item is #{item} ::: line #{lastlinect} "
|
192
|
+
|
193
|
+
# convert actual newline to C-a. slash n's are escapes so echo -e does not muck up.
|
194
|
+
text.tr! "\n", ''
|
195
|
+
Kernel.print("Got '#{text}'\n") if @verbose
|
196
|
+
paditem = _paditem(item)
|
197
|
+
print "item no is:#{paditem}:\n" if @verbose
|
198
|
+
priority = @options[:priority] ? " (#{@options[:priority]})" : ""
|
199
|
+
project = @options[:project] ? " +#{@options[:project]}" : ""
|
200
|
+
component = @options[:component] ? " @#{@options[:component]}" : ""
|
201
|
+
level = (item.split '.').length
|
202
|
+
indent = " " * (TABSTOP * (level-1))
|
203
|
+
newtext=nil
|
204
|
+
if @copying
|
205
|
+
newtext="#{indent}#{item}#{@todo_delim}#{text}"
|
206
|
+
else
|
207
|
+
newtext="#{indent}#{paditem}#{@todo_delim}[ ]#{priority}#{project}#{component} #{text} (#{@today})"
|
208
|
+
end
|
209
|
+
raise "Cannot insert blank text. Programmer error!" unless newtext
|
210
|
+
#_backup
|
211
|
+
puts "Adding:"
|
212
|
+
puts newtext
|
213
|
+
Sed::insert_row(@app_file_path, lastlinect, newtext)
|
214
|
+
return 0
|
140
215
|
end
|
141
|
-
|
142
|
-
# After doing a redo of the numbering, we need to reset the numbers for that app
|
143
|
-
def _set_serial_number number
|
144
|
-
appname = @appname
|
145
|
-
pattern = Regexp.new "^#{appname}:.*$"
|
146
|
-
filename = @todo_serial_path
|
147
|
-
_backup filename
|
148
|
-
change_row filename, pattern, "#{appname}:#{number}"
|
149
|
-
end
|
150
|
-
def _backup filename=@todo_file_path
|
151
|
-
require 'fileutils'
|
152
|
-
FileUtils.cp filename, "#{filename}.org"
|
153
|
-
end
|
154
|
-
def check_file filename=@todo_file_path
|
216
|
+
def check_file filename=@app_file_path
|
155
217
|
File.exists?(filename) or die "#{filename} does not exist in this dir. Use 'add' to create an item first."
|
156
218
|
end
|
157
|
-
def die text
|
158
|
-
$stderr.puts text
|
159
|
-
exit 1
|
160
|
-
end
|
161
219
|
##
|
162
220
|
# for historical reasons, I pad item to 3 spaces in text file.
|
163
221
|
# It used to help me in printing straight off without any formatting in unix shell
|
164
222
|
def _paditem item
|
165
223
|
return sprintf("%3s", item)
|
166
224
|
end
|
225
|
+
##
|
226
|
+
# populates array with open tasks (or all if --show-all)
|
227
|
+
# DO NOT USE in conjunction with save_array since this is only open tasks
|
228
|
+
# Use load_array with save_array
|
167
229
|
def populate
|
230
|
+
$valid_array = false # this array object should not be saved
|
168
231
|
check_file
|
169
232
|
@ctr = 0
|
170
233
|
@total = 0
|
@@ -209,9 +272,9 @@ class Todo
|
|
209
272
|
sort if @options[:sort]
|
210
273
|
renumber if @options[:renumber]
|
211
274
|
colorize # << currently this is where I print !! Since i colorize the whole line
|
212
|
-
puts
|
213
|
-
puts " #{@data.length} of #{@total} rows displayed from #{@
|
214
|
-
|
275
|
+
puts " "
|
276
|
+
puts " #{@data.length} of #{@total} rows displayed from #{@app_file_path} "
|
277
|
+
return 0
|
215
278
|
end
|
216
279
|
def print_todo
|
217
280
|
@ctr = 0
|
@@ -285,198 +348,208 @@ class Todo
|
|
285
348
|
#
|
286
349
|
# @param [Array] priority, single char A-Z, item or items
|
287
350
|
# @return
|
288
|
-
# @
|
351
|
+
# @example:
|
289
352
|
# pri A 5 6 7
|
290
353
|
# pri 5 6 7 A
|
291
|
-
# pri A 5 6 7 B 1 2 3
|
292
|
-
# pri 5 6 7 A 1 2 3 B
|
354
|
+
# -- NO LONGER this complicated system pri A 5 6 7 B 1 2 3
|
355
|
+
# -- NO LONGER this complicated system pri 5 6 7 A 1 2 3 B
|
293
356
|
|
357
|
+
# 2010-06-19 15:21 total rewrite, so we fetch item from array and warn if absent.
|
294
358
|
def pri args
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
item = nil
|
359
|
+
errors = 0
|
360
|
+
ctr = 0
|
361
|
+
#populate # populate removed closed task so later saving will lose tasks
|
362
|
+
load_array
|
300
363
|
## if the first arg is priority then following items all have that priority
|
301
364
|
## if the first arg is item/s then wait for priority and use that
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
if changeon == :PRI
|
315
|
-
puts " changing previous items #{items} to #{prior} "
|
316
|
-
items.each { |i| _pri(i, prior) }
|
317
|
-
items = []
|
365
|
+
prior, items = _separate args, /^[A-Z]$/
|
366
|
+
total = items.count
|
367
|
+
die "#{@action}: priority expected [A-Z]" unless prior
|
368
|
+
die "#{@action}: items expected" unless items
|
369
|
+
verbose "args 0 is #{args[0]}. pri #{prior} items #{items} "
|
370
|
+
items.each do |item|
|
371
|
+
row = get_item(item)
|
372
|
+
if row
|
373
|
+
puts " #{row[0]} : #{row[1]} "
|
374
|
+
# remove existing priority if there
|
375
|
+
if row[1] =~ /\] (\([A-Z]\) )/
|
376
|
+
row[1].sub!(/\([A-Z]\) /,"")
|
318
377
|
end
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
_pri(item, prior)
|
378
|
+
ret = row[1].sub!(/\] /,"] (#{prior}) ")
|
379
|
+
if ret
|
380
|
+
puts " #{GREEN}#{row[0]} : #{row[1]} #{CLEAR}"
|
381
|
+
ctr += 1
|
324
382
|
else
|
325
|
-
|
383
|
+
die "Error in sub(): #{row}.\nNothing saved. "
|
326
384
|
end
|
327
385
|
else
|
328
|
-
|
386
|
+
errors += 1
|
387
|
+
warning "#{item} not found."
|
329
388
|
end
|
330
389
|
end
|
331
|
-
|
390
|
+
|
391
|
+
message "#{errors} error/s" if errors > 0
|
392
|
+
if ctr > 0
|
393
|
+
puts "Changed priority of #{ctr} task/s"
|
394
|
+
save_array
|
395
|
+
return 0
|
396
|
+
end
|
397
|
+
return ERRCODE
|
332
398
|
end
|
333
399
|
##
|
334
|
-
#
|
400
|
+
# Remove the priority of a task
|
335
401
|
#
|
336
402
|
# @param [Array] items to deprioritize
|
337
403
|
# @return
|
338
|
-
public
|
339
404
|
def depri(args)
|
340
|
-
|
341
|
-
puts "depri got #{args} "
|
342
|
-
each do |row|
|
343
|
-
item = row[0].sub(/^[ -]*/,'')
|
344
|
-
if args.include? item
|
345
|
-
if row[1] =~ /\] (\([A-Z]\) )/
|
346
|
-
puts row[1]
|
347
|
-
row[1].sub!(/\([A-Z]\) /,"")
|
348
|
-
puts "#{RED}#{row[1]}#{CLEAR}"
|
349
|
-
end
|
350
|
-
end
|
351
|
-
end
|
352
|
-
end
|
353
|
-
##
|
354
|
-
# saves the task array to disk
|
355
|
-
def save_array
|
356
|
-
File.open(@todo_file_path, "w") do |file|
|
357
|
-
@data.each { |row| file.puts "#{row[0]}\t#{row[1]}" }
|
358
|
-
end
|
359
|
-
end
|
360
|
-
##
|
361
|
-
# change priority of given item to priority in array
|
362
|
-
private
|
363
|
-
def _pri item, pri
|
364
|
-
paditem = _paditem(item)
|
365
|
-
@data.each { |row|
|
366
|
-
if row[0] == paditem
|
367
|
-
puts " #{row[0]} : #{row[1]} "
|
368
|
-
if row[1] =~ /\] (\([A-Z]\) )/
|
369
|
-
row[1].sub!(/\([A-Z]\) /,"")
|
370
|
-
end
|
371
|
-
row[1].sub!(/\] /,"] (#{pri}) ")
|
372
|
-
puts " #{RED}#{row[0]} : #{row[1]} #{CLEAR}"
|
373
|
-
return true
|
374
|
-
end
|
375
|
-
}
|
376
|
-
puts " #{RED} no such item #{item} #{CLEAR} "
|
377
|
-
return false
|
378
|
-
|
405
|
+
change_items args, /\([A-Z]\) /,""
|
379
406
|
end
|
380
407
|
##
|
381
408
|
# Appends a tag to task
|
409
|
+
# FIXME: check with subtasks
|
382
410
|
#
|
383
411
|
# @param [Array] items and tag, or tag and items
|
384
412
|
# @return
|
385
|
-
public
|
386
413
|
def tag(args)
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
tag = arg
|
394
|
-
elsif arg =~ /^[0-9]+$/
|
395
|
-
items << arg
|
396
|
-
end
|
397
|
-
end
|
398
|
-
#items.each { |i| change_row }
|
399
|
-
change_file @todo_file_path do |line|
|
400
|
-
item = line.match(/^ *([0-9]+)/)
|
401
|
-
if items.include? item[1]
|
402
|
-
puts "#{line}" if @verbose
|
403
|
-
line.sub!(/$/, " @#{tag}")
|
404
|
-
puts "#{RED}#{line}#{CLEAR} " if @verbose
|
405
|
-
end
|
406
|
-
end
|
414
|
+
tag, items = _separate args
|
415
|
+
#change_items items do |item, row|
|
416
|
+
#ret = row[1].sub!(/ (\([0-9]{4})/, " @#{tag} "+'\1')
|
417
|
+
#ret
|
418
|
+
#end
|
419
|
+
change_items(items, / (\([0-9]{4})/, " @#{tag} "+'\1')
|
407
420
|
end
|
408
421
|
##
|
409
422
|
# deletes one or more items
|
410
423
|
#
|
411
424
|
# @param [Array, #include?] items to delete
|
412
425
|
# @return
|
426
|
+
# FIXME: if invalid item passed I have no way of giving error
|
413
427
|
public
|
414
428
|
def delete(args)
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
429
|
+
ctr = errors = 0
|
430
|
+
items = args
|
431
|
+
die "#{@action}: items expected" unless items
|
432
|
+
total = items.count
|
433
|
+
totalitems = []
|
434
|
+
load_array
|
435
|
+
items.each do |item|
|
436
|
+
if @options[:recursive]
|
437
|
+
a = get_item_subs item
|
438
|
+
if a
|
439
|
+
a.each { |e|
|
440
|
+
totalitems << e; #e[0].strip
|
441
|
+
}
|
442
|
+
else
|
443
|
+
errors += 1
|
444
|
+
warning "#{item} not found."
|
445
|
+
end
|
446
|
+
else
|
447
|
+
row = get_item(item)
|
448
|
+
if row
|
449
|
+
totalitems << row
|
423
450
|
else
|
424
|
-
|
425
|
-
|
426
|
-
STDOUT.flush
|
427
|
-
ans = STDIN.gets.chomp
|
428
|
-
ans =~ /[Yy]/
|
451
|
+
errors += 1
|
452
|
+
warning "#{item} not found."
|
429
453
|
end
|
430
454
|
end
|
431
455
|
end
|
456
|
+
totalitems.each { |item|
|
457
|
+
puts "#{item[0]} #{item[1]}"
|
458
|
+
print "Do you wish to delete (Y/N): "
|
459
|
+
STDOUT.flush
|
460
|
+
ans = STDIN.gets.chomp
|
461
|
+
if ans =~ /[Yy]/
|
462
|
+
@data.delete item
|
463
|
+
# put deleted row into deleted file, so one can undo
|
464
|
+
File.open(@deleted_path, "a") { | file| file.puts "#{item[0]}#{@todo_delim}#{item[1]}" }
|
465
|
+
ctr += 1
|
466
|
+
else
|
467
|
+
puts "Delete canceled #{item[0]}"
|
468
|
+
end
|
469
|
+
}
|
470
|
+
message "#{errors} error/s" if errors > 0
|
471
|
+
if ctr > 0
|
472
|
+
puts "Deleted #{ctr} task/s"
|
473
|
+
save_array
|
474
|
+
return 0
|
475
|
+
end
|
476
|
+
return ERRCODE
|
432
477
|
end
|
433
478
|
##
|
434
479
|
# Change status of given items
|
435
480
|
#
|
436
|
-
# @param [Array, #include?] items to
|
481
|
+
# @param [Array, #include?] items to change status of
|
437
482
|
# @return [true, false] success or fail
|
438
483
|
public
|
439
484
|
def status(args)
|
440
|
-
stat, items = _separate args
|
485
|
+
stat, items = _separate args #, /^[a-zA-Z]/
|
486
|
+
verbose "Items: #{items} : stat #{stat} "
|
441
487
|
status, newstatus = _resolve_status stat
|
442
488
|
if status.nil?
|
443
|
-
|
444
|
-
exit 1
|
489
|
+
die "Status #{stat} is invalid!"
|
445
490
|
end
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
491
|
+
# this worked fine for single items, but not for recursive changes
|
492
|
+
#ctr = change_items(items, /(\[.\])/, "[#{newstatus}]")
|
493
|
+
totalitems = []
|
494
|
+
#ret = line.sub!(/(\[.\])/, "[#{newstatus}]")
|
495
|
+
load_array
|
496
|
+
items.each { |item|
|
497
|
+
a = get_item_subs item
|
498
|
+
if a
|
499
|
+
a.each { |e|
|
500
|
+
totalitems << e[0].strip
|
501
|
+
}
|
502
|
+
else
|
503
|
+
# perhaps I should pass item into total and let c_i handle error message
|
504
|
+
warning "No tasks found for #{item}"
|
505
|
+
end
|
506
|
+
}
|
507
|
+
change_items(totalitems, /(\[.\])/, "[#{newstatus}]")
|
508
|
+
0
|
453
509
|
end
|
454
510
|
##
|
455
511
|
# separates args into tag or subcommand and items
|
456
512
|
# This allows user to pass e.g. a priority first and then item list
|
457
513
|
# or item list first and then priority.
|
458
514
|
# This can only be used if the tag or pri or status is non-numeric and the item is numeric.
|
459
|
-
def _separate args
|
515
|
+
def _separate args, pattern=nil #/^[a-zA-Z]/
|
460
516
|
tag = nil
|
461
517
|
items = []
|
462
518
|
args.each do |arg|
|
463
|
-
if arg =~ /^[
|
464
|
-
tag = arg
|
465
|
-
elsif arg =~ /^[0-9]+$/
|
519
|
+
if arg =~ /^[0-9\.]+$/
|
466
520
|
items << arg
|
521
|
+
else
|
522
|
+
tag = arg
|
523
|
+
if pattern
|
524
|
+
die "#{@action}: #{arg} appears invalid." if arg !~ pattern
|
525
|
+
end
|
467
526
|
end
|
468
527
|
end
|
528
|
+
items = nil if items.empty?
|
469
529
|
return tag, items
|
470
530
|
end
|
471
531
|
##
|
472
532
|
# Renumber while displaying
|
473
|
-
#
|
474
533
|
# @return [true, false] success or fail
|
475
534
|
private
|
476
535
|
def renumber
|
536
|
+
## this did not account for subtasks
|
537
|
+
#@data.each_with_index { |row, i|
|
538
|
+
#paditem = _paditem(i+1)
|
539
|
+
#row[0] = paditem
|
540
|
+
#}
|
541
|
+
## this accounts for subtasks
|
542
|
+
ctr = 0
|
477
543
|
@data.each_with_index { |row, i|
|
478
|
-
|
479
|
-
row[0]
|
544
|
+
# main task, increment counter
|
545
|
+
if row[0] =~ /^ *[0-9]+$/
|
546
|
+
ctr += 1
|
547
|
+
paditem = _paditem(ctr)
|
548
|
+
row[0] = paditem
|
549
|
+
else
|
550
|
+
# assume its a subtask, just change the outer number
|
551
|
+
row[0].sub!(/[0-9]+\./, "#{ctr}.")
|
552
|
+
end
|
480
553
|
}
|
481
554
|
end
|
482
555
|
##
|
@@ -488,14 +561,11 @@ class Todo
|
|
488
561
|
def note(args)
|
489
562
|
_backup
|
490
563
|
text = args.pop
|
491
|
-
change_items args do |item,
|
492
|
-
m =
|
564
|
+
change_items args do |item, row|
|
565
|
+
m = row[0].match(/^ */)
|
493
566
|
indent = m[0]
|
494
|
-
|
495
|
-
|
496
|
-
# At printing the C-a is replaced with a newline and some spaces
|
497
|
-
ret = line.sub!(/ (\([0-9]{4})/," #{indent}* #{text} "+'\1')
|
498
|
-
print_red line if @verbose
|
567
|
+
ret = row[1].sub!(/ (\([0-9]{4})/," #{indent}* #{text} "+'\1')
|
568
|
+
ret
|
499
569
|
end
|
500
570
|
end
|
501
571
|
##
|
@@ -508,7 +578,7 @@ class Todo
|
|
508
578
|
filename = @archive_path
|
509
579
|
file = File.open(filename, "a")
|
510
580
|
ctr = 0
|
511
|
-
delete_row @
|
581
|
+
Sed::delete_row @app_file_path do |line|
|
512
582
|
if line =~ /\[x\]/
|
513
583
|
file.puts line
|
514
584
|
ctr += 1
|
@@ -519,60 +589,192 @@ class Todo
|
|
519
589
|
file.close
|
520
590
|
puts "Archived #{ctr} tasks."
|
521
591
|
end
|
592
|
+
# Copy given item under second item
|
593
|
+
#
|
594
|
+
# @param [Array] 2 items, move first under second
|
595
|
+
# @return [Boolean] success or fail
|
596
|
+
public
|
597
|
+
def copyunder(args)
|
598
|
+
if args.nil? or args.count != 2
|
599
|
+
die "copyunder expects only 2 args: from and to item, both existing"
|
600
|
+
end
|
601
|
+
from = args[0]
|
602
|
+
to = args[1]
|
603
|
+
# extract item from
|
604
|
+
lastlinetext = nil
|
605
|
+
rx = regexp_item from
|
606
|
+
Sed::egrep( [@app_file_path], rx) do |fn,ln,line|
|
607
|
+
lastlinect = ln
|
608
|
+
lastlinetext = line
|
609
|
+
puts line
|
610
|
+
end
|
611
|
+
# removing everything from start to status inclusive
|
612
|
+
lastlinetext.sub!(/^.*\[/,'[').chomp!
|
613
|
+
puts lastlinetext
|
614
|
+
@copying = true
|
615
|
+
addsub [to, lastlinetext]
|
616
|
+
# remove item number and status ?
|
617
|
+
# give to addsub to add.
|
618
|
+
# delete from
|
619
|
+
delete_item from
|
620
|
+
# take care of data in addsub (if existing, and also /
|
621
|
+
end
|
522
622
|
##
|
523
|
-
#
|
623
|
+
# Get row for given item or nil.
|
524
624
|
#
|
525
|
-
# @param [
|
526
|
-
# @return [
|
625
|
+
# @param [String] item to retrieve
|
626
|
+
# @return [Array, nil] success or fail
|
627
|
+
# Returns row from @data as String[2] comprising item and rest of line.
|
527
628
|
public
|
528
|
-
def
|
629
|
+
def get_item(item)
|
630
|
+
raise "Please load array first!" if @data.empty?
|
631
|
+
puts "get_item got #{item}." if @verbose
|
632
|
+
#rx = regexp_item(item)
|
633
|
+
rx = Regexp.new("^ +#{item}$")
|
634
|
+
@data.each { |row|
|
635
|
+
puts " get_item read #{row[0]}." if @verbose
|
636
|
+
return row if row[0] =~ rx
|
637
|
+
}
|
638
|
+
# not found
|
639
|
+
return nil
|
640
|
+
end
|
641
|
+
##
|
642
|
+
# list task and its subtasks
|
643
|
+
# just testing this out
|
644
|
+
def listsub(args)
|
645
|
+
load_array
|
646
|
+
args.each { |item|
|
647
|
+
a = get_item_subs item
|
648
|
+
puts "for #{item} "
|
649
|
+
a.each { |e| puts " #{e[0]} #{e[1]} " }
|
650
|
+
}
|
651
|
+
0
|
652
|
+
end
|
653
|
+
# get item and its subtasks
|
654
|
+
# (in an attempt to make recursive changes cleaner)
|
655
|
+
# @param item (taken from command line)
|
656
|
+
# @return [Array, nil] row[] objects
|
657
|
+
def get_item_subs(item)
|
658
|
+
raise "Please load array first!" if @data.empty?
|
659
|
+
verbose "get_item got #{item}."
|
660
|
+
#rx = regexp_item(item)
|
661
|
+
rx = Regexp.new("^ +#{item}$")
|
662
|
+
rx2 = Regexp.new("^ +#{item}\.")
|
663
|
+
rows = []
|
664
|
+
@data.each { |row|
|
665
|
+
verbose " get_item read #{row[0]}."
|
666
|
+
if row[0] =~ rx
|
667
|
+
rows << row
|
668
|
+
rx = rx2
|
669
|
+
end
|
670
|
+
}
|
671
|
+
return nil if rows.empty?
|
672
|
+
return rows
|
529
673
|
end
|
530
674
|
##
|
531
|
-
#
|
532
|
-
#
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
675
|
+
# For given items, search replace or yield item and row[]
|
676
|
+
# (earlier started as new_change_items)
|
677
|
+
#
|
678
|
+
# @param [Array, #each] items to change
|
679
|
+
# @yield item, row[] - split of line on tab.
|
680
|
+
# @return [0, ERRCODE] success or fail
|
681
|
+
public
|
682
|
+
def change_items items, pattern=nil, replacement=nil
|
683
|
+
ctr = errors = 0
|
684
|
+
#tag, items = _separate args
|
685
|
+
# or items = args
|
686
|
+
die "#{@action}: items expected" unless items
|
687
|
+
total = items.count
|
688
|
+
load_array
|
689
|
+
items.each do |item|
|
690
|
+
row = get_item(item)
|
691
|
+
if row
|
538
692
|
if pattern
|
539
|
-
puts
|
540
|
-
ret =
|
541
|
-
|
542
|
-
|
693
|
+
puts " #{row[0]} : #{row[1]} " if @verbose
|
694
|
+
ret = row[1].sub!(pattern, replacement)
|
695
|
+
if ret
|
696
|
+
puts " #{GREEN}#{row[0]} : #{row[1]} #{CLEAR}"
|
697
|
+
ctr += 1
|
698
|
+
else
|
699
|
+
# this is since there could be a programmer error.
|
700
|
+
die "Possible error in sub() - No replacement: #{row[0]} : #{row[1]}.\nNothing saved. "
|
701
|
+
end
|
543
702
|
else
|
544
|
-
|
703
|
+
puts " #{row[0]} : #{row[1]} " if @verbose
|
704
|
+
ret = yield item, row
|
705
|
+
if ret
|
706
|
+
ctr += 1
|
707
|
+
puts " #{GREEN}#{row[0]} : #{row[1]} #{CLEAR}"
|
708
|
+
end
|
545
709
|
end
|
710
|
+
else
|
711
|
+
errors += 1
|
712
|
+
warning "#{item} not found."
|
546
713
|
end
|
547
714
|
end
|
548
|
-
|
715
|
+
message "#{errors} error/s" if errors > 0
|
716
|
+
if ctr > 0
|
717
|
+
puts "Changed #{ctr} task/s"
|
718
|
+
save_array
|
719
|
+
return 0
|
720
|
+
end
|
721
|
+
return ERRCODE
|
722
|
+
end
|
723
|
+
## does a straight delete of an item, no questions asked
|
724
|
+
# internal use only.
|
725
|
+
def delete_item item
|
726
|
+
filename=@app_file_path
|
727
|
+
d = Sed::_read filename
|
728
|
+
d.delete_if { |row| line_contains_item?(row, item) }
|
729
|
+
Sed::_write filename, d
|
730
|
+
end
|
731
|
+
def line_contains_item? line, item
|
732
|
+
rx = regexp_item item
|
733
|
+
return line.match rx
|
734
|
+
end
|
735
|
+
def row_contains_item? row, item
|
736
|
+
rx = Regexp.new("^ +#{item}")
|
737
|
+
return row[0].match rx
|
738
|
+
end
|
739
|
+
# return a regexp for an item to do matches on - WARNING INCLUDES TAB
|
740
|
+
def regexp_item item
|
741
|
+
Regexp.new("^ +#{item}#{@todo_delim}")
|
742
|
+
end
|
743
|
+
# @unused - wrote so i could use it refactoring - i should be using this TODO:
|
744
|
+
def extract_item line
|
745
|
+
item = line.match(/^ *([0-9\.]+)/)
|
746
|
+
return nil if item.nil?
|
747
|
+
return item[1]
|
549
748
|
end
|
550
749
|
##
|
551
750
|
# Redoes the numbering in the file.
|
552
751
|
# Useful if the numbers have gone high and you want to start over.
|
553
752
|
def redo args
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
puts "Saved #{@
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
753
|
+
#require 'fileutils'
|
754
|
+
#FileUtils.cp @app_file_path, "#{@app_file_path}.org"
|
755
|
+
_backup
|
756
|
+
puts "Saved #{@app_file_path} as #{@app_file_path}.org"
|
757
|
+
#ctr = 1
|
758
|
+
#change_file @app_file_path do |line|
|
759
|
+
#paditem = _paditem ctr
|
760
|
+
#line.sub!(/^ *[0-9]+/, paditem)
|
761
|
+
#ctr += 1
|
762
|
+
#end
|
763
|
+
ctr = 0
|
764
|
+
Sed::change_file @app_file_path do |line|
|
765
|
+
if line =~ /^ *[0-9]+\t/
|
766
|
+
ctr += 1
|
767
|
+
paditem = _paditem ctr
|
768
|
+
line.sub!(/^ *[0-9]+\t/, "#{paditem}#{@todo_delim}")
|
769
|
+
else
|
770
|
+
# assume its a subtask, just change the outer number
|
771
|
+
line.sub!(/[0-9]+\./, "#{ctr}.")
|
772
|
+
end
|
562
773
|
end
|
563
774
|
_set_serial_number ctr
|
564
775
|
puts "Redone numbering"
|
565
776
|
end
|
566
777
|
##
|
567
|
-
# does this command start with items or something else
|
568
|
-
private
|
569
|
-
def items_first?(args)
|
570
|
-
return true if args[0] =~ /^[0-9]+$/
|
571
|
-
return false
|
572
|
-
end
|
573
|
-
def print_red line
|
574
|
-
puts "#{RED}#{line}#{CLEAR}"
|
575
|
-
end
|
576
778
|
private
|
577
779
|
def _resolve_status stat
|
578
780
|
status = nil
|
@@ -601,28 +803,45 @@ class Todo
|
|
601
803
|
#newstatus=$( echo $status | sed 's/^start/@/;s/^pend/P/;s/^close/x/;s/hold/H/;s/next/1/;s/^unstarted/ /' )
|
602
804
|
return status, newstatus
|
603
805
|
end
|
806
|
+
##
|
807
|
+
# given some items, checks given line to see if it contains subtasks of given items
|
808
|
+
# if item is 3.1, does line contain 3.1.x 3.1.x.x etc or not
|
809
|
+
# @example
|
810
|
+
# [1, 2, 3, 3.1, 3.1.1, 3.2, 3.3 ... ], "3.1.1"
|
811
|
+
def item_matches_subtask? items, item
|
812
|
+
items.each { |e|
|
813
|
+
rx = Regexp.new "#{e}\."
|
814
|
+
m = item.match rx
|
815
|
+
return true if m
|
816
|
+
}
|
817
|
+
return false
|
818
|
+
end
|
819
|
+
## [1, 2, 3, 3.1, 3.1.1, 3.2, 3.3 ... ], " 3.1.1\t[ ] some task"
|
604
820
|
|
605
821
|
def self.main args
|
822
|
+
ret = nil
|
606
823
|
begin
|
607
824
|
# http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html
|
608
825
|
require 'optparse'
|
609
826
|
options = {}
|
610
827
|
options[:verbose] = false
|
611
828
|
options[:colorize] = true
|
829
|
+
# adding some env variable pickups, so you don't have to keep passing.
|
830
|
+
showall = ENV["TODO_SHOW_ALL"]
|
831
|
+
if showall
|
832
|
+
options[:show_all] = (showall == "0") ? false:true
|
833
|
+
end
|
834
|
+
plain = ENV["TODO_PLAIN"]
|
835
|
+
if plain
|
836
|
+
options[:colorize] = (plain == "0") ? false:true
|
837
|
+
end
|
612
838
|
|
613
839
|
OptionParser.new do |opts|
|
614
|
-
opts.banner = "Usage: #{$
|
840
|
+
opts.banner = "Usage: #{$APPNAME} [options] action"
|
615
841
|
|
616
842
|
opts.separator ""
|
617
|
-
opts.separator "
|
618
|
-
|
843
|
+
opts.separator "Options for list and add:"
|
619
844
|
|
620
|
-
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
621
|
-
options[:verbose] = v
|
622
|
-
end
|
623
|
-
opts.on("-f", "--file FILENAME", "CSV filename") do |v|
|
624
|
-
options[:file] = v
|
625
|
-
end
|
626
845
|
opts.on("-P", "--project PROJECTNAME", "name of project for add or list") { |v|
|
627
846
|
options[:project] = v
|
628
847
|
options[:filter] = true
|
@@ -635,9 +854,14 @@ class Todo
|
|
635
854
|
options[:component] = v
|
636
855
|
options[:filter] = true
|
637
856
|
}
|
857
|
+
opts.separator ""
|
858
|
+
opts.separator "Specific options:"
|
638
859
|
opts.on("--force", "force delete or add without prompting") do |v|
|
639
860
|
options[:force] = v
|
640
861
|
end
|
862
|
+
opts.on("--recursive", "operate on subtasks also for delete, status") do |v|
|
863
|
+
options[:recursive] = v
|
864
|
+
end
|
641
865
|
opts.separator ""
|
642
866
|
opts.separator "List options:"
|
643
867
|
|
@@ -657,42 +881,57 @@ class Todo
|
|
657
881
|
opts.on("--hide-numbering", "hide-numbering while listing ") do |v|
|
658
882
|
options[:hide_numbering] = v
|
659
883
|
end
|
660
|
-
opts.on("--show-all", "show all tasks (incl closed)") do |v|
|
884
|
+
opts.on("--[no-]show-all", "show all tasks (incl closed)") do |v|
|
661
885
|
options[:show_all] = v
|
662
886
|
end
|
663
887
|
|
664
888
|
opts.separator ""
|
665
889
|
opts.separator "Common options:"
|
666
890
|
|
667
|
-
opts.
|
891
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
892
|
+
options[:verbose] = v
|
893
|
+
end
|
894
|
+
opts.on("-f", "--file FILENAME", "CSV filename") do |v|
|
895
|
+
options[:file] = v
|
896
|
+
end
|
897
|
+
opts.on("-d DIR", "--dir DIR", "Use TODO file in this directory") do |v|
|
668
898
|
require 'FileUtils'
|
669
899
|
dir = File.expand_path v
|
670
900
|
if File.directory? dir
|
671
901
|
options[:dir] = dir
|
902
|
+
# changing dir is important so that serial_number file is the current one.
|
672
903
|
FileUtils.cd dir
|
673
904
|
else
|
674
|
-
|
675
|
-
exit 1
|
905
|
+
die "#{RED}#{v}: no such directory #{CLEAR}"
|
676
906
|
end
|
677
907
|
end
|
678
908
|
# No argument, shows at tail. This will print an options summary.
|
679
909
|
# Try it and see!
|
680
|
-
opts.
|
910
|
+
opts.on("-h", "--help", "Show this message") do
|
681
911
|
puts opts
|
682
|
-
exit
|
912
|
+
exit 0
|
683
913
|
end
|
684
914
|
|
685
|
-
opts.
|
915
|
+
opts.on("--show-actions", "show actions ") do |v|
|
686
916
|
todo = Todo.new(options, ARGV)
|
687
917
|
todo.help nil
|
688
918
|
exit 0
|
689
919
|
end
|
690
920
|
|
691
|
-
opts.
|
921
|
+
opts.on("--version", "Show version") do
|
692
922
|
puts "#{APPNAME} version #{VERSION}, #{DATE}"
|
693
923
|
puts "by #{AUTHOR}. This software is under the GPL License."
|
694
924
|
exit 0
|
695
925
|
end
|
926
|
+
opts.separator ""
|
927
|
+
opts.separator "Common Usage:"
|
928
|
+
opts.separator <<TEXT
|
929
|
+
#{APPNAME} list
|
930
|
+
#{APPNAME} add "TEXT ...."
|
931
|
+
#{APPNAME} pri 1 A
|
932
|
+
#{APPNAME} close 1
|
933
|
+
TEXT
|
934
|
+
|
696
935
|
end.parse!(args)
|
697
936
|
|
698
937
|
options[:file] ||= "TODO2.txt"
|
@@ -704,10 +943,13 @@ class Todo
|
|
704
943
|
#raise "-f FILENAME is mandatory" unless options[:file]
|
705
944
|
|
706
945
|
todo = Todo.new(options, args)
|
707
|
-
todo.run
|
946
|
+
ret = todo.run
|
708
947
|
ensure
|
709
948
|
end
|
949
|
+
return ret
|
710
950
|
end # main
|
711
951
|
end # class Todo
|
712
952
|
|
713
|
-
|
953
|
+
if __FILE__ == $0
|
954
|
+
exit Todo.main(ARGV)
|
955
|
+
end
|