todorb 0.2.1 → 1.0.0
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/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
|