zzamboni-things2thl 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog CHANGED
@@ -1,6 +1,43 @@
1
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
2
+
3
+ * VERSION: Version bump to 0.8.0
4
+
5
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
6
+
7
+ * bin/things2thl, lib/Things2THL.rb: Added new mode of operation
8
+ --inbox (-I) to transfer only tasks from the Inbox.
9
+
10
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
11
+
12
+ * bin/things2thl, lib/Things2THL.rb: Added printing of some
13
+ statistics at the end - number of items created, total time elapsed.
14
+ Can be disabled by specifying -q twice (-qq).
15
+
16
+ 2009-05-25 Diego Zamboni <diego@zzamboni.org>
17
+
18
+ * bin/things2thl, lib/Things2THL.rb: Added option --sync, which
19
+ causes items (areas, projects and tasks) not to be created if they
20
+ already exist. This is useful if you add more items to Things after
21
+ you have transferred to THL, and want to re-transfer just the new
22
+ ones. This option is disabled by default because the duplicate checking is
23
+ done using only the item's name, so if you have multiple entries
24
+ with the same name in Things, they will only be transferred once
25
+ with --sync enabled.
26
+
27
+ 2009-05-24 Diego Zamboni <diego@zzamboni.org>
28
+
29
+ * bin/things2thl, lib/Things2THL.rb: Added options --areas-as-tags
30
+ and --areas-as-contexts, which specify that areas in Things should
31
+ be transferred to THL as tags/contexts respectively, instead of
32
+ created as separate entities. Thanks to @biomac101 for the suggestion.
33
+
34
+ 2009-05-21 Diego Zamboni <diego@zzamboni.org>
35
+
36
+ * things2thl.gemspec: Regenerated gemspec for version 0.7.0
37
+
1
38
  2009-05-21 Diego Zamboni <diego@zzamboni.org>
2
39
 
3
- * VERSION, lib/Things2THL.rb: Version bump to 0.7.0
40
+ * ChangeLog, VERSION, lib/Things2THL.rb: Version bump to 0.7.0
4
41
 
5
42
  2009-05-21 Diego Zamboni <diego@zzamboni.org>
6
43
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.0
1
+ 0.8.0
data/bin/things2thl CHANGED
@@ -4,6 +4,7 @@
4
4
  require File.join(File.dirname(__FILE__), *%w".. lib Things2THL")
5
5
  require "optparse"
6
6
  require "ostruct"
7
+ require "time"
7
8
 
8
9
  options=Things2THL.default_options
9
10
  opts = OptionParser.new do |opts|
@@ -14,6 +15,10 @@ opts = OptionParser.new do |opts|
14
15
  puts self
15
16
  exit
16
17
  end
18
+
19
+ def yesno(v)
20
+ "(default: #{v ? 'yes' : 'no'})"
21
+ end
17
22
 
18
23
  opts.separator ''
19
24
  opts.separator("Modes of operation (required):")
@@ -25,14 +30,20 @@ opts = OptionParser.new do |opts|
25
30
  "Convert both projects and areas in Things",
26
31
  " to lists in THL. This implies that",
27
32
  " projects are not nested inside areas.") { options.structure = :projects_areas_as_lists }
33
+ opts.on("-I", "--inbox", "Transfer only Inbox tasks." ) do
34
+ options.inboxonly = true
35
+ options.structure = :projects_as_lists
36
+ end
28
37
 
29
38
  opts.separator ''
30
39
  opts.separator("Options:")
31
- opts.on("--[no-]areas", "Transfer areas from Things (default: #{options.areas ? 'yes' : 'no'})") { |v| options.areas = v }
40
+ opts.on("--[no-]areas", "Transfer areas from Things #{yesno(options.areas)}") { |v| options.areas = v }
41
+ opts.on("--areas-as-tags", "Transfer areas as tags in THL.") { options.areas_as=:tags }
42
+ opts.on("--areas-as-contexts", "Transfer areas as contexts in THL.") { options.areas_as=:contexts }
32
43
  opts.on('--[no-]time-tags',
33
44
  'Consider tags of the form Xmin/Xsec/Xhr as',
34
45
  ' time estimates, set them in THL',
35
- " accordingly (default: #{options.timetags ? 'yes' : 'no'}).") do |value|
46
+ " accordingly #{yesno(options.timetags)}.") do |value|
36
47
  options.timetags = value
37
48
  end
38
49
  opts.on('--[no-]context-tags [REGEX]',
@@ -42,6 +53,7 @@ opts = OptionParser.new do |opts|
42
53
  " Use with no- to disable this feature.") do |regex|
43
54
  options.contexttagsregex = case regex; when false:nil; when "":options.contexttagsregex; else regex; end
44
55
  end
56
+ opts.on("--sync", "Only transfer new items #{yesno(options.sync)}") { options.sync = true }
45
57
  opts.on("--top-level-folder FOLDER", "Do the import inside the named folders,"," instead of the top level",
46
58
  " (Inbox, etc. will also be created there"," instead of their corresponding places)") do |toplevel|
47
59
  options.toplevel = toplevel
@@ -65,11 +77,13 @@ opts = OptionParser.new do |opts|
65
77
 
66
78
  opts.on("-c", "--completed",
67
79
  "Transfer also completed/canceled tasks",
68
- " and projects (default: #{options.completed ? 'yes' : 'no'})") { options.completed = true }
80
+ " and projects #{yesno(options.completed)}") { options.completed = true }
69
81
  opts.on("--[no-]archive-completed",
70
82
  "If transferring completed/canceled tasks,",
71
- " also mark them as archived (default: #{options.archivecompleted ? 'yes' : 'no'})") {|v| options.archivecompleted = v }
72
- opts.on("-q", "--quiet", "Do not print items as they are processed") { options.quiet = true }
83
+ " also mark them as archived #{yesno(options.archivecompleted)}") {|v| options.archivecompleted = v }
84
+ opts.on("-q", "--quiet",
85
+ "Do not print items as they are processed.",
86
+ "Twice: do not print stats at the end.") { options.quiet+=1 }
73
87
  # opts.on("-n", "--dry-run", "Do not create anything in THL, just print the items that would be created") { options.dryrun = true }
74
88
  # opts.on("-n", "--notes", "Shows only tasks with notes") { options[:tasks] = { :onlynotes => true } }
75
89
  # opts.on("-a", "--all", 'Shows all tasks in the focus') { |f| options[:tasks] = { :children => false } }
@@ -100,7 +114,7 @@ end
100
114
  ######################################################################
101
115
 
102
116
  opts.parse!
103
- unless options.structure
117
+ unless options.structure || options.inboxonly
104
118
  puts "Error: you didn't specify the mode of operation to use."
105
119
  puts ''
106
120
  opts.show_usage
@@ -110,27 +124,30 @@ converter = Things2THL.new(options, options.thingsapp, options.thlapp)
110
124
  things = converter.things
111
125
  thl = converter.thl
112
126
 
113
- # Create top-level containers if needed
127
+ # Start timing
128
+ start=Time.new
114
129
 
115
- # First, traverse all areas
116
- if options.areas
117
- puts "Processing Areas of Responsibility" unless options.quiet
118
- things.areas.get.each do |area|
119
- converter.process(Things2THL::ThingsNode.new(area))
130
+ unless options.inboxonly
131
+ # First, traverse all areas
132
+ if options.areas && !options.areas_as
133
+ puts "Processing Areas of Responsibility" unless options.quiet.nonzero?
134
+ things.areas.get.each do |area|
135
+ converter.process(Things2THL::ThingsNode.new(area))
136
+ end
120
137
  end
121
- end
122
138
 
123
- # Next, traverse all projects, putting each one inside its corresponding area
124
- puts "Processing Projects" unless options.quiet
125
- if options.completed
126
- puts " (fetching all projects - this may take a while)" unless options.quiet
127
- projlist=things.projects.get
128
- else
129
- puts " (fetching only open projects - this may take a while)" unless options.quiet
130
- projlist=things.projects[its.status.eq(:open)].get
131
- end
132
- projlist.each do |project|
133
- converter.process(Things2THL::ThingsNode.new(project))
139
+ # Next, traverse all projects, putting each one inside its corresponding area
140
+ puts "Processing Projects" unless options.quiet.nonzero?
141
+ if options.completed
142
+ puts " (fetching all projects - this may take a while)" unless options.quiet.nonzero?
143
+ projlist=things.projects.get
144
+ else
145
+ puts " (fetching only open projects - this may take a while)" unless options.quiet.nonzero?
146
+ projlist=things.projects[its.status.eq(:open)].get
147
+ end
148
+ projlist.each do |project|
149
+ converter.process(Things2THL::ThingsNode.new(project))
150
+ end
134
151
  end
135
152
 
136
153
  # Now do the tasks
@@ -138,13 +155,18 @@ end
138
155
  # - to_dos returns not only tasks, also projects (not areas)
139
156
  # - to-dos returns tasks from all the views: Inbox, Today, Scheduled, Someday, and Next, so we have
140
157
  # to separate them and create the appropriate containers as needed
141
- puts "Processing tasks" unless options.quiet
158
+ puts "Processing tasks" unless options.quiet.nonzero?
159
+ if options.inboxonly
160
+ queryobj=things.lists['Inbox']
161
+ else
162
+ queryobj=things
163
+ end
142
164
  if options.completed
143
- puts " (fetching all tasks - this may take a while)" unless options.quiet
144
- tasklist=things.to_dos.get
165
+ puts " (fetching all tasks - this may take a while)" unless options.quiet.nonzero?
166
+ tasklist=queryobj.to_dos.get
145
167
  else
146
- puts " (fetching only open tasks - this may take a while)" unless options.quiet
147
- tasklist=things.to_dos[its.status.eq(:open)].get
168
+ puts " (fetching only open tasks - this may take a while)" unless options.quiet.nonzero?
169
+ tasklist=queryobj.to_dos[its.status.eq(:open)].get
148
170
  end
149
171
  tasklist.each do |t|
150
172
  task=Things2THL::ThingsNode.new(t)
@@ -152,3 +174,11 @@ tasklist.each do |t|
152
174
  converter.process(task)
153
175
  end
154
176
 
177
+ # End timing
178
+ diff=Time.new-start
179
+
180
+ # Print stats at the end
181
+ unless options.quiet >= 2
182
+ puts "Conversion done in #{diff} seconds."
183
+ converter.created.each_pair { |key,val| puts " #{val} #{key}#{val!=1 ? 's' : ''} created" }
184
+ end
data/lib/Things2THL.rb CHANGED
@@ -263,7 +263,7 @@ module Things2THL
263
263
  ####################################################################
264
264
 
265
265
  class Converter
266
- attr_accessor :options, :things, :thl
266
+ attr_accessor :options, :things, :thl, :created
267
267
 
268
268
  def initialize(opt_struct = nil, things_location = nil, thl_location = nil)
269
269
  @options=opt_struct || Things2THL.default_options
@@ -295,6 +295,14 @@ module Things2THL
295
295
  # id_ of each node that belongs to that focus. Existence of the key
296
296
  # indicates existence in the focus.
297
297
  @cache_focus = {}
298
+
299
+ # Statistics
300
+ @created = {
301
+ :task => 0,
302
+ :list => 0,
303
+ :folder => 0
304
+ }
305
+
298
306
  end
299
307
 
300
308
  # Get the type of the THL node that corresponds to the given Things node,
@@ -360,24 +368,48 @@ module Things2THL
360
368
  end
361
369
  end
362
370
 
371
+ # Simplified version of find_or_create which simply takes :new and :name
372
+ def simple_find_or_create(what, name, parent = @thl.folders_group.get)
373
+ find_or_create({:new => what, :with_properties => { :name => name } }, parent)
374
+ end
375
+
363
376
  # Find or create a list or a folder inside the given parent (or the top-level folders group if not given)
364
- def find_or_create(what, name, parent = @thl.folders_group.get)
365
- unless what == :list || what == :folder
366
- raise "find_or_create: 'what' parameter has to be :list or :folder"
377
+ def find_or_create(props, parent = @thl.folders_group.get)
378
+ puts "find_or_create: props = #{props.inspect}" if $DEBUG
379
+ what=props[:new]
380
+ name=(what==:task) ? props[:with_properties][:title] : props[:with_properties][:name]
381
+ parentclass=parent.class_.get
382
+ unless what == :list || what == :folder || what == :task
383
+ raise "find_or_create: 'props[:new]' parameter has to be :list, :folder or :task"
367
384
  end
368
385
  puts "parent of #{name} = #{parent}" if $DEBUG
369
- if parent.class_.get != :folder
370
- raise "find_or_create: parent is not a folder, it's a #{parent.class_.get}"
386
+ if (what == :folder || what == :list) && parentclass != :folder
387
+ raise "find_or_create: parent is not a folder, it's a #{parentclass}"
388
+ elsif what == :task && parentclass != :list && parentclass != :task
389
+ raise "find_or_create: parent is not a list, it's a #{parentclass}"
371
390
  else
372
- if parent.groups[name].exists
373
- parent.groups[name].get
391
+ if what == :task
392
+ query = parent.tasks[its.title.eq(name)]
393
+ if ! query.get.empty?
394
+ query.get[0]
395
+ else
396
+ @created[what]+=1
397
+ parent.end.make(props)
398
+ end
374
399
  else
375
- parent.end.make(:new => what, :with_properties => {:name => name})
400
+ query = parent.groups[name]
401
+ if query.exists
402
+ query.get
403
+ else
404
+ @created[what]+=1
405
+ parent.end.make(props)
406
+ end
376
407
  end
377
408
  end
378
409
  end
379
410
 
380
411
  def new_folder(name, parent = @thl.folders_group.get)
412
+ @created[:folder]+=1
381
413
  parent.end.make(:new => :folder,
382
414
  :with_properties => { :name => name })
383
415
  end
@@ -388,7 +420,11 @@ module Things2THL
388
420
 
389
421
  unless @top_level_node
390
422
  # Create the top-level node if we don't have it cached yet
391
- @top_level_node=new_folder(options.toplevel)
423
+ if options.sync
424
+ @top_level_node=simple_find_or_create(:folder, options.toplevel)
425
+ else
426
+ @top_level_node=new_folder(options.toplevel)
427
+ end
392
428
  end
393
429
  @top_level_node
394
430
  end
@@ -416,17 +452,17 @@ module Things2THL
416
452
  when 'Trash', 'Today'
417
453
  nil
418
454
  when 'Inbox', 'Next'
419
- find_or_create(:list, focusname, top_level_node)
455
+ simple_find_or_create(:list, focusname, top_level_node)
420
456
  when 'Scheduled', 'Logbook'
421
- find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
457
+ simple_find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
422
458
  when 'Someday'
423
- find_or_create(:folder, focusname, top_level_node)
459
+ simple_find_or_create(:folder, focusname, top_level_node)
424
460
  when 'Projects'
425
461
  if thl_node_type(:project) == :task
426
- find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
462
+ simple_find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
427
463
  else
428
464
  if options.projectsfolder
429
- find_or_create(:folder, options.projectsfolder, top_level_node)
465
+ simple_find_or_create(:folder, options.projectsfolder, top_level_node)
430
466
  else
431
467
  top_level_node
432
468
  end
@@ -443,15 +479,15 @@ module Things2THL
443
479
  when 'Next'
444
480
  top_level_node
445
481
  when 'Scheduled', 'Logbook'
446
- find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
482
+ simple_find_or_create((thl_node_type(:project) == :task) ? :list : :folder, focusname, top_level_node)
447
483
  when 'Someday'
448
- find_or_create(:folder, focusname, top_level_node)
484
+ simple_find_or_create(:folder, focusname, top_level_node)
449
485
  when 'Projects'
450
486
  if thl_node_type(:project) == :task
451
- find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
487
+ simple_find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
452
488
  else
453
489
  if options.projectsfolder
454
- find_or_create(:folder, options.projectsfolder, top_level_node)
490
+ simple_find_or_create(:folder, options.projectsfolder, top_level_node)
455
491
  else
456
492
  top_level_node
457
493
  end
@@ -478,7 +514,7 @@ module Things2THL
478
514
  return top_level_for_focus('Someday')
479
515
  else
480
516
  if options.areasfolder
481
- return find_or_create(:folder, options.areasfolder || 'Areas', top_level_node)
517
+ return simple_find_or_create(:folder, options.areasfolder || 'Areas', top_level_node)
482
518
  else
483
519
  return top_level_node
484
520
  end
@@ -491,10 +527,10 @@ module Things2THL
491
527
  if foci.empty?
492
528
  if node.project?
493
529
  tl=top_level_for_node(node.project)
494
- if !tl && node.area?
530
+ if !tl && node.area? && !options.areas_as
495
531
  tl=top_level_for_node(node.area)
496
532
  end
497
- elsif node.area?
533
+ elsif node.area? && !options.areas_as
498
534
  tl=top_level_for_node(node.area)
499
535
  end
500
536
  return tl
@@ -530,7 +566,7 @@ module Things2THL
530
566
  when :area
531
567
  tlcontainer
532
568
  when :project
533
- if options.areas && (options.structure != :projects_areas_as_lists) && node.area?
569
+ if options.areas && !options.areas_as && (options.structure != :projects_areas_as_lists) && node.area?
534
570
  get_cached_or_process(node.area)
535
571
  else
536
572
  tlcontainer
@@ -538,7 +574,7 @@ module Things2THL
538
574
  when :selected_to_do
539
575
  if node.project?
540
576
  get_cached_or_process(node.project)
541
- elsif node.area? && options.areas
577
+ elsif node.area? && options.areas && !options.areas_as
542
578
  get_cached_or_process(node.area)
543
579
  else
544
580
  # It's a loose task
@@ -552,9 +588,9 @@ module Things2THL
552
588
  # so if the container is a folder, we have to create a list to hold the task
553
589
  if container && (container.class_.get == :folder) && (thl_node_type(node) == :task)
554
590
  if node.type == :project
555
- find_or_create(:list, options.projectsfolder || 'Projects', container)
591
+ simple_find_or_create(:list, options.projectsfolder || 'Projects', container)
556
592
  else
557
- find_or_create(:list, loose_tasks_name(container), container)
593
+ simple_find_or_create(:list, loose_tasks_name(container), container)
558
594
  end
559
595
  else
560
596
  container
@@ -566,8 +602,15 @@ module Things2THL
566
602
  new_node_type = thl_node_type(node)
567
603
  new_node_props = props_from_node(node)
568
604
  additional_nodes = new_node_props.delete(:__newnodes__)
569
- result=parent.end.make(:new => new_node_type,
570
- :with_properties => new_node_props )
605
+ new_node_spec = {
606
+ :new => new_node_type,
607
+ :with_properties => new_node_props }
608
+ if options.sync
609
+ result=find_or_create(new_node_spec, parent)
610
+ else
611
+ @created[new_node_type]+=1
612
+ result=parent.end.make(new_node_spec)
613
+ end
571
614
  if node.type == :area || node.type == :project
572
615
  @cache_nodes[node.id_]={}
573
616
  @cache_nodes[node.id_][:things_node] = node
@@ -576,7 +619,12 @@ module Things2THL
576
619
  # Add new nodes
577
620
  if additional_nodes
578
621
  additional_nodes.each do |n|
579
- result.end.make(n)
622
+ if options.sync
623
+ find_or_create(n, result)
624
+ else
625
+ @created[n[:new]]+=1
626
+ result.end.make(n)
627
+ end
580
628
  end
581
629
  end
582
630
  return result
@@ -598,11 +646,11 @@ module Things2THL
598
646
  container=container_for(node)
599
647
  puts "Container for #{node.name}: #{container}" if $DEBUG
600
648
  unless container
601
- puts "Skipping trashed task '#{node.name}'" unless options.quiet
649
+ puts "Skipping trashed task '#{node.name}'" unless options.quiet.nonzero?
602
650
  return
603
651
  end
604
652
 
605
- unless (options.quiet)
653
+ unless (options.quiet.nonzero?)
606
654
  bullet = (node.type == :area) ? "*" : ((node.status == :completed) ? "✓" : (node.status == :canceled) ? "×" : "-")
607
655
  puts bullet + " " + node.name
608
656
  end
@@ -689,17 +737,34 @@ module Things2THL
689
737
  # Process tags
690
738
  def process_tags(node, prop, inherit_project_tags, inherit_area_tags)
691
739
  tasktags = node.tags.map {|t| t.name }
740
+ taskcontexts = []
692
741
  if inherit_project_tags
693
742
  # Merge project and area tags
694
743
  if node.project?
695
744
  tasktags |= node.project.tags.map {|t| t.name }
696
745
  if options.areas && node.project.area?
697
746
  tasktags |= node.project.area.tags.map {|t| t.name }
747
+ if options.areas_as
748
+ case options.areas_as
749
+ when :tags
750
+ tasktags.push(node.project.area.name)
751
+ when :contexts
752
+ taskcontexts.push(node.project.area.name)
753
+ end
754
+ end
698
755
  end
699
756
  end
700
757
  end
701
758
  if options.areas && node.area? && inherit_area_tags
702
759
  tasktags |= node.area.tags.map {|t| t.name }
760
+ if options.areas_as
761
+ case options.areas_as
762
+ when :tags
763
+ tasktags.push(node.area.name)
764
+ when :contexts
765
+ taskcontexts.push(node.area.name)
766
+ end
767
+ end
703
768
  end
704
769
  unless tasktags.empty?
705
770
  # First process time-estimate tags if needed
@@ -742,6 +807,12 @@ module Things2THL
742
807
  end
743
808
  end].join(' ')
744
809
  end
810
+ unless taskcontexts.empty?
811
+ prop[:title] = [prop[:title], taskcontexts.map do |c|
812
+ # Contexts cannot have spaces, we also remove any initial @'s before adding our own.
813
+ "@" + c.gsub(/^@+/, "").gsub(/ /, '_')
814
+ end].join(' ')
815
+ end
745
816
  end
746
817
 
747
818
  # Check if node is in the Today list
@@ -835,13 +906,16 @@ module Things2THL
835
906
  options.database = nil
836
907
  options.structure = nil
837
908
  options.areas = true
838
- options.quiet = false
909
+ options.quiet = 0
839
910
  options.archivecompleted = true
840
911
  options.projectsfolder = nil
841
912
  options.areasfolder = nil
842
913
  options.contexttagsregex = '^@'
843
914
  options.timetagsregex = '^(\d+)(min|sec|hr)$'
844
915
  options.timetags = false
916
+ options.areas_as = nil
917
+ options.sync = false
918
+ options.inboxonly = false
845
919
  return options
846
920
  end
847
921
 
data/things2thl.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{things2thl}
5
- s.version = "0.7.0"
5
+ s.version = "0.8.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Diego Zamboni"]
9
- s.date = %q{2009-05-21}
9
+ s.date = %q{2009-05-25}
10
10
  s.default_executable = %q{things2thl}
11
11
  s.description = %q{Library and command-line tool for migrating Things data to The Hit List}
12
12
  s.email = %q{diego@zzamboni.org}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zzamboni-things2thl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Diego Zamboni
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-05-21 00:00:00 -07:00
12
+ date: 2009-05-25 00:00:00 -07:00
13
13
  default_executable: things2thl
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency