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/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 = "1.0"
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
- @todo_default_action = "list"
39
- @todo_file_path = @options[:file] || "TODO2.txt"
40
- #@todo_serial_path = File.expand_path("~/serial_numbers")
41
- @todo_serial_path = "serial_numbers"
42
- @archive_path = "todo_archive.txt" # should take path of todo and put there TODO:
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["add"] = "Add a task. \n\t #{$0} add <TEXT>\n\t --component C --project P --priority X add <TEXT>"
53
- @actions["pri"] = "Add priority to task. \n\t #{$0} pri <ITEM> [A-Z]"
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 #{$0} depri <ITEM>"
56
- @actions["delete"] = "Delete a task. \n\t #{$0} delete <ITEM>"
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 #{$0} status <STAT> <ITEM>\n\t<STAT> are closed started pending unstarted hold next"
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 #{$0} note <ITEM> <TEXT>"
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
- # TODO config
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 1
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
- print "item no is:#{paditem}:\n" if @verbose
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
- # reads serial_number file, returns serialno for this app
117
- # and increments the serial number and writes back.
118
- def _get_serial_number
119
- require 'fileutils'
120
- appname = @appname
121
- filename = @todo_serial_path
122
- h = {}
123
- if File.exists?(filename)
124
- File.open(filename).each { |line|
125
- #sn = $1 if line.match regex
126
- x = line.split ":"
127
- h[x[0]] = x[1].chomp
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
- sn = h[appname] || 1
131
- # update the sn in file
132
- nsn = sn.to_i + 1
133
- # this will create if not exists in addition to storing if it does
134
- h[appname] = nsn
135
- # write back to file
136
- File.open(filename, "w") do |f|
137
- h.each_pair {|k,v| f.print "#{k}:#{v}\n"}
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
- return sn
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 #{@todo_file_path} "
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
- # @ example:
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
- populate
296
- changeon = nil
297
- items = []
298
- prior = nil
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
- if args[0] =~ /^[A-Z]$/
303
- changeon = :ITEM
304
- elsif args[0] =~ /^[0-9]+$/
305
- changeon = :PRI
306
- else
307
- puts "ERROR! "
308
- exit 1
309
- end
310
- puts "args 0 is #{args[0]} "
311
- args.each do |arg|
312
- if arg =~ /^[A-Z]$/
313
- prior = arg #$1
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
- elsif arg =~ /^[0-9]+$/
320
- item = arg #$1
321
- if changeon == :ITEM
322
- puts " changing #{item} to #{prior} "
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
- items << item
383
+ die "Error in sub(): #{row}.\nNothing saved. "
326
384
  end
327
385
  else
328
- puts "ERROR in arg :#{arg}:"
386
+ errors += 1
387
+ warning "#{item} not found."
329
388
  end
330
389
  end
331
- save_array
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
- # Reove the priority of a task
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
- populate
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
- puts "tags args #{args} "
388
- items_first = items_first? args
389
- items = []
390
- tag = nil
391
- args.each do |arg|
392
- if arg =~ /^[a-zA-Z]/
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
- puts "delete with #{args} "
416
- delete_row @todo_file_path do |line|
417
- #puts "line #{line} "
418
- item = line.match(/^ *([0-9]+)/)
419
- #puts "item #{item} "
420
- if args.include? item[1]
421
- if @options[:force]
422
- true
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
- puts line
425
- print "Do you wish to delete (Y/N): "
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 delete
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
- print_red "Status #{stat} is invalid!"
444
- exit 1
489
+ die "Status #{stat} is invalid!"
445
490
  end
446
- ctr = change_items(items, /(\[.\])/, "[#{newstatus}]")
447
- puts "Changed status of #{ctr} items"
448
- #change_items items do |item, line|
449
- #puts line if @verbose
450
- #line.sub!(/(\[.\])/, "[#{newstatus}]")
451
- #puts "#{RED}#{line}#{CLEAR}" if @verbose
452
- #end
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 =~ /^[a-zA-Z]/
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
- paditem = _paditem(i+1)
479
- row[0] = paditem
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, line|
492
- m = line.match(/^ */)
564
+ change_items args do |item, row|
565
+ m = row[0].match(/^ */)
493
566
  indent = m[0]
494
- puts line if @verbose
495
- # we place the text before the date, adding a C-a and indent
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 @todo_file_path do |line|
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
- # For given items, ...
623
+ # Get row for given item or nil.
524
624
  #
525
- # @param [Array, #include?] items to delete
526
- # @return [true, false] success or fail
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 CHANGEME(args)
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
- # yields lines from file that match the given item
532
- # We do not need to now parse and match the item in each method
533
- def change_items args, pattern=nil, replacement=nil
534
- changed_ctr = 0
535
- change_file @todo_file_path do |line|
536
- item = line.match(/^ *([0-9]+)/)
537
- if args.include? item[1]
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 line if @verbose
540
- ret = line.sub!(pattern, replacement)
541
- changed_ctr += 1 if ret
542
- print_red line if @verbose
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
- yield item[1], line
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
- return changed_ctr
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
- ctr = 1
555
- require 'fileutils'
556
- FileUtils.cp @todo_file_path, "#{@todo_file_path}.org"
557
- puts "Saved #{@todo_file_path} as #{@todo_file_path}.org"
558
- change_file @todo_file_path do |line|
559
- paditem = _paditem ctr
560
- line.sub!(/^ *[0-9]+/, paditem)
561
- ctr += 1
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: #{$0} [options] action"
840
+ opts.banner = "Usage: #{$APPNAME} [options] action"
615
841
 
616
842
  opts.separator ""
617
- opts.separator "Specific options:"
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.on_tail("-d DIR", "--dir DIR", "Use TODO file in this directory") do |v|
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
- puts "#{RED}#{v}: no such directory #{CLEAR}"
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.on_tail("-h", "--help", "Show this message") do
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.on_tail("--show-actions", "show actions ") do |v|
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.on_tail("--version", "Show version") do
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
- Todo.main(ARGV) if __FILE__ == $0
953
+ if __FILE__ == $0
954
+ exit Todo.main(ARGV)
955
+ end