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/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