zzamboni-things2thl 0.2.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/Manifest +5 -0
- data/README +60 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/bin/things2thl +123 -0
- data/lib/Things2THL.rb +698 -0
- data/things2thl.gemspec +42 -0
- metadata +59 -0
data/Manifest
ADDED
data/README
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Things2THL
|
|
2
|
+
|
|
3
|
+
Conversion program to transfer all tasks from Things
|
|
4
|
+
(http://culturedcode.com/things/ ) to The Hit List
|
|
5
|
+
(http://www.potionfactory.com/thehitlist/ ).
|
|
6
|
+
|
|
7
|
+
Written by Diego Zamboni <diego@zzamboni.org>
|
|
8
|
+
|
|
9
|
+
INSTALL:
|
|
10
|
+
--------
|
|
11
|
+
|
|
12
|
+
You need Things 1.1.1 or later, since things2thl requires Applescript
|
|
13
|
+
support.
|
|
14
|
+
|
|
15
|
+
You need to install rb-appscript from
|
|
16
|
+
http://appscript.sourceforge.net/rb-appscript/install.html
|
|
17
|
+
|
|
18
|
+
Run things2thl by changing into the base directory of this
|
|
19
|
+
distribution and running:
|
|
20
|
+
./bin/things2thl [options]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
USAGE:
|
|
24
|
+
-----
|
|
25
|
+
|
|
26
|
+
To see a usage message:
|
|
27
|
+
|
|
28
|
+
./bin/things2thl -h
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Functionality still missing:
|
|
33
|
+
---------------------------
|
|
34
|
+
|
|
35
|
+
- Handling contexts vs tags
|
|
36
|
+
|
|
37
|
+
Plan: make it a user option which Things tasks should be considered
|
|
38
|
+
as contexts.
|
|
39
|
+
|
|
40
|
+
- Handle rich-text notes (with attachments, links, etc.) properly
|
|
41
|
+
|
|
42
|
+
Plan: not sure yet. Need to investigate how notes are stored in
|
|
43
|
+
THL (Things uses an XML format)
|
|
44
|
+
|
|
45
|
+
- Handling delegation ("People" feature in Things)
|
|
46
|
+
|
|
47
|
+
Not sure how to transfer this to THL. Ideas are welcome.
|
|
48
|
+
|
|
49
|
+
Known issues:
|
|
50
|
+
-------------
|
|
51
|
+
|
|
52
|
+
- Cancellation/completion dates are not transferred, because THL
|
|
53
|
+
handles those attributes as read-only. So if you choose to transfer
|
|
54
|
+
completed/canceled tasks, they will all appear in your "completed
|
|
55
|
+
today" view.
|
|
56
|
+
|
|
57
|
+
- Tasks in the Things "Scheduled" focus are transferred, but the
|
|
58
|
+
scheduling itself is not transferred, because this information is
|
|
59
|
+
not accessible through AS from either Things not THL.
|
|
60
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'jeweler'
|
|
6
|
+
Jeweler::Tasks.new do |gemspec|
|
|
7
|
+
gemspec.name = "things2thl"
|
|
8
|
+
gemspec.summary = "Library and command-line tool for migrating Things data to The Hit List"
|
|
9
|
+
gemspec.email = "diego@zzamboni.org"
|
|
10
|
+
gemspec.homepage = "http://zzamboni.github.com/things2thl/"
|
|
11
|
+
gemspec.description = "Library and command-line tool for migrating Things data to The Hit List"
|
|
12
|
+
gemspec.authors = ["Diego Zamboni"]
|
|
13
|
+
end
|
|
14
|
+
rescue LoadError
|
|
15
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.0
|
data/bin/things2thl
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
require File.join(File.dirname(__FILE__), *%w".. lib Things2THL")
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "ostruct"
|
|
7
|
+
|
|
8
|
+
# Command line options and defaults
|
|
9
|
+
options=OpenStruct.new
|
|
10
|
+
options.completed = false
|
|
11
|
+
options.database = nil
|
|
12
|
+
options.structure = :projects_as_lists
|
|
13
|
+
options.areas = true
|
|
14
|
+
options.quiet = false
|
|
15
|
+
options.archivecompleted = true
|
|
16
|
+
options.projectsfolder = nil
|
|
17
|
+
opts = OptionParser.new do |opts|
|
|
18
|
+
opts.separator ''
|
|
19
|
+
opts.separator 'Options:'
|
|
20
|
+
|
|
21
|
+
opts.banner = "Usage: things2thl [options]"
|
|
22
|
+
|
|
23
|
+
def opts.show_usage
|
|
24
|
+
puts self
|
|
25
|
+
exit
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
opts.on("--projects-as-lists", "Convert projects in Things to lists in THL (default)") { options.structure = :projects_as_lists }
|
|
29
|
+
opts.on("--projects-as-tasks", "Convert projects in Things to tasks in THL") { options.structure = :projects_as_tasks }
|
|
30
|
+
opts.on("--no-areas", "Ignore areas in Things") { options.areas = false }
|
|
31
|
+
|
|
32
|
+
opts.on("--top-level-folder FOLDER", "If specified, do the import inside the named folders, instead of the top level",
|
|
33
|
+
" (Inbox, etc. will also be created there instead of their corresponding places)") do |toplevel|
|
|
34
|
+
options.toplevel = toplevel
|
|
35
|
+
end
|
|
36
|
+
opts.on("--projects-folder FOLDER", "If specified, the named folder will be created to contain all projects when",
|
|
37
|
+
" --projects-as-lists is used (otherwise they will be put in the top folders group).",
|
|
38
|
+
" If --projects-as-tasks is used, a 'Projects' list is always created, but this option",
|
|
39
|
+
" can be used to specify its name.") do |projfolder|
|
|
40
|
+
options.projectsfolder = projfolder
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on("-c", "--completed", 'Transfer also completed/canceled tasks and projects (default: no)') { options.completed = true }
|
|
44
|
+
opts.on("--no-archive-completed", 'If transferring completed/canceled tasks, also mark them as archived (default: yes)') {options.archivecompleted = false }
|
|
45
|
+
opts.on("-q", "--quiet", "Do not print items as they are processed") { options.quiet = true }
|
|
46
|
+
# opts.on("-n", "--dry-run", "Do not create anything in THL, just print the items that would be created") { options.dryrun = true }
|
|
47
|
+
# opts.on("-n", "--notes", "Shows only tasks with notes") { options[:tasks] = { :onlynotes => true } }
|
|
48
|
+
# opts.on("-a", "--all", 'Shows all tasks in the focus') { |f| options[:tasks] = { :children => false } }
|
|
49
|
+
|
|
50
|
+
opts.on("-h", "--help", "Shows this help message") { opts.show_usage }
|
|
51
|
+
opts.on("-v", "--version", "Shows version") do
|
|
52
|
+
puts Things2THL::Version::STRING
|
|
53
|
+
exit
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.separator("")
|
|
57
|
+
opts.separator("Options you should seldom need:")
|
|
58
|
+
opts.on("--things THINGSAPP", "Location of the Things application (default: /Applications/Things.app)") do |things|
|
|
59
|
+
options.thingsapp = things
|
|
60
|
+
end
|
|
61
|
+
opts.on("--thl THLAPP", "Location of the The Hit List application (default: /Applications/The Hit List.app)") do |thl|
|
|
62
|
+
options.thlapp = thl
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
######################################################################
|
|
68
|
+
# Main program
|
|
69
|
+
######################################################################
|
|
70
|
+
|
|
71
|
+
opts.parse!
|
|
72
|
+
#opts.show_usage unless options.key?(:focus)
|
|
73
|
+
|
|
74
|
+
converter = Things2THL.new(options, options.thingsapp, options.thlapp)
|
|
75
|
+
things = converter.things
|
|
76
|
+
thl = converter.thl
|
|
77
|
+
|
|
78
|
+
# Create top-level containers if needed
|
|
79
|
+
|
|
80
|
+
# First, traverse all areas
|
|
81
|
+
things.areas.get.each { |area| converter.process(Things2THL::ThingsNode.new(area)) } if options.areas
|
|
82
|
+
|
|
83
|
+
# Next, traverse all projects, putting each one inside its corresponding area
|
|
84
|
+
things.projects.get.each { |project| converter.process(Things2THL::ThingsNode.new(project)) }
|
|
85
|
+
|
|
86
|
+
# Now do the tasks
|
|
87
|
+
# This is more complicated because:
|
|
88
|
+
# - to_dos returns not only tasks, also projects (not areas)
|
|
89
|
+
# - to-dos returns tasks from all the views: Inbox, Scheduled, Someday, and Next, so we have
|
|
90
|
+
# to separate them and create the appropriate containers as needed
|
|
91
|
+
things.to_dos.get.each do |t|
|
|
92
|
+
task=Things2THL::ThingsNode.new(t)
|
|
93
|
+
next if task.type != :selected_to_do
|
|
94
|
+
converter.process(task)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# # First traverse areas
|
|
98
|
+
# puts "Areas:"
|
|
99
|
+
# things.areas.each { |n| converter.traverse(n, 0, thl.folders_group.get) }
|
|
100
|
+
# # Then traverse area-less projects
|
|
101
|
+
# puts "\nProjects without an area:"
|
|
102
|
+
# parent = thl.folders_group.get
|
|
103
|
+
# if (options.structure == :projects_as_tasks)
|
|
104
|
+
# parent = converter.find_or_create_list(parent, "Projects")
|
|
105
|
+
# end
|
|
106
|
+
# things.projects.each { |n| converter.traverse(n, 0, parent) unless n.parent_area? }
|
|
107
|
+
# Then tasks without a project
|
|
108
|
+
#puts "\nTasks without a project:"
|
|
109
|
+
#things.focus(:next).children.each { |n| traverse(n, 0, options) unless n.parent? }
|
|
110
|
+
|
|
111
|
+
#############
|
|
112
|
+
|
|
113
|
+
# thl.folders_group.end.make(:new => :folder, :with_properties => {:name => 'test folder'})
|
|
114
|
+
# app("/Applications/The Hit List.app").folders_group.folders.ID("1B98A3855A3C79DE").end.make(:new => :list, :with_properties => {:name => 'test list'})
|
|
115
|
+
# task=app("/Applications/The Hit List.app").folders_group.folders.ID("1B98A3855A3C79DE").lists.ID("43B3187E6D90AB8D").end.make(:new => :task, :with_properties => {:title => 'test
|
|
116
|
+
# task', :notes => 'test notes' })
|
|
117
|
+
# task.end.make(:new => :task, :with_properties => {:title => 'test subtask'})
|
|
118
|
+
# task.set(:title => 'new title')
|
|
119
|
+
# task.set(title => 'new title')
|
|
120
|
+
# task.title = 'new title'
|
|
121
|
+
# thl.set(task.title, :to => 'new title')
|
|
122
|
+
# task.title.set('another title')
|
|
123
|
+
# task.title.get
|
data/lib/Things2THL.rb
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
#require File.join(File.dirname(__FILE__), *%w".. .. things-rb lib things")
|
|
4
|
+
require "ostruct"
|
|
5
|
+
require 'time'
|
|
6
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
7
|
+
require 'appscript'; include Appscript
|
|
8
|
+
|
|
9
|
+
######################################################################
|
|
10
|
+
|
|
11
|
+
module Things2THL
|
|
12
|
+
module Version
|
|
13
|
+
MAJOR = 0
|
|
14
|
+
MINOR = 1
|
|
15
|
+
|
|
16
|
+
STRING = [MAJOR, MINOR].join(".")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
####################################################################
|
|
20
|
+
|
|
21
|
+
module Constants
|
|
22
|
+
# Ways in which the items can be structured on the THL side.
|
|
23
|
+
# Elements are: <Things item> => [<THL item>, {prop mapping}, postblock],
|
|
24
|
+
# where {prop mapping} is a map containing <Things prop> => <THL prop>.
|
|
25
|
+
# <Things prop> is a symbol representing the name of a Node property.
|
|
26
|
+
# <THL prop> can be any of the following
|
|
27
|
+
# :symbol => value of <Things prop> is assigned to this prop
|
|
28
|
+
# [:symbol, {hash}] => value of <Things prop> is looked up in {hash}
|
|
29
|
+
# and the corresponding value is assigned to :symbol
|
|
30
|
+
# {value1 => :symbol1, value2 => :symbol2} => if value of <Things prop>
|
|
31
|
+
# is value1, then :symbol1 prop in THL is set to true, etc.
|
|
32
|
+
# postblock, if given, should be a code block that will receive
|
|
33
|
+
# the original node, the produced new properties hash and the
|
|
34
|
+
# Things2THL object as parameters, and do any necessary
|
|
35
|
+
# postprocessing on the new properties
|
|
36
|
+
# If postblock inserts in the properties hash an item with the key
|
|
37
|
+
# :__newnodes__, it should be an array with items of the form
|
|
38
|
+
# { :new => :what,
|
|
39
|
+
# :with_properties => { properties }
|
|
40
|
+
# }
|
|
41
|
+
# Those items will be added to the new THL node immediately
|
|
42
|
+
# after its creation.
|
|
43
|
+
STRUCTURES = {
|
|
44
|
+
:projects_as_lists => {
|
|
45
|
+
:area => [:folder,
|
|
46
|
+
{
|
|
47
|
+
:name => :name,
|
|
48
|
+
}],
|
|
49
|
+
:project => [:list,
|
|
50
|
+
{
|
|
51
|
+
:name => :name,
|
|
52
|
+
:creation_date => :created_date,
|
|
53
|
+
},
|
|
54
|
+
Proc.new {|node,prop,obj|
|
|
55
|
+
obj.add_list_notes(node,prop)
|
|
56
|
+
obj.add_project_duedate(node,prop)
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
:selected_to_do => [:task,
|
|
60
|
+
{
|
|
61
|
+
:name => :title,
|
|
62
|
+
:creation_date => :created_date,
|
|
63
|
+
:due_date => :due_date,
|
|
64
|
+
:completion_date => :completed_date,
|
|
65
|
+
:cancellation_date => :canceled_date,
|
|
66
|
+
:status => {
|
|
67
|
+
:completed => :completed,
|
|
68
|
+
:canceled => :canceled,
|
|
69
|
+
},
|
|
70
|
+
:notes => :notes,
|
|
71
|
+
},
|
|
72
|
+
Proc.new {|node,prop,obj|
|
|
73
|
+
obj.fix_completed_canceled(node, prop)
|
|
74
|
+
obj.archive_completed(prop)
|
|
75
|
+
obj.add_tags(node, prop, true, true)
|
|
76
|
+
obj.check_today(node, prop)
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
:projects_as_tasks => {
|
|
81
|
+
:area => [:list,
|
|
82
|
+
{
|
|
83
|
+
:name => :name,
|
|
84
|
+
}],
|
|
85
|
+
:project => [:task,
|
|
86
|
+
{
|
|
87
|
+
:name => :title,
|
|
88
|
+
:creation_date => :created_date,
|
|
89
|
+
:due_date => :due_date,
|
|
90
|
+
:completion_date => :completed_date,
|
|
91
|
+
:cancellation_date => :canceled_date,
|
|
92
|
+
:status => {
|
|
93
|
+
:completed => :completed,
|
|
94
|
+
:canceled => :canceled,
|
|
95
|
+
},
|
|
96
|
+
:notes => :notes,
|
|
97
|
+
},
|
|
98
|
+
Proc.new {|node,prop,obj|
|
|
99
|
+
obj.fix_completed_canceled(node, prop)
|
|
100
|
+
obj.archive_completed(prop)
|
|
101
|
+
obj.add_tags(node, prop, false, true)
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
:selected_to_do => [:task,
|
|
105
|
+
{
|
|
106
|
+
:name => :title,
|
|
107
|
+
:creation_date => :created_date,
|
|
108
|
+
:due_date => :due_date,
|
|
109
|
+
:completion_date => :completed_date,
|
|
110
|
+
:cancellation_date => :canceled_date,
|
|
111
|
+
:status => {
|
|
112
|
+
:completed => :completed,
|
|
113
|
+
:canceled => :canceled,
|
|
114
|
+
},
|
|
115
|
+
:notes => :notes,
|
|
116
|
+
},
|
|
117
|
+
Proc.new {|node,prop,obj|
|
|
118
|
+
obj.fix_completed_canceled(node, prop)
|
|
119
|
+
obj.archive_completed(prop)
|
|
120
|
+
obj.add_tags(node, prop, false, true)
|
|
121
|
+
obj.check_today(node, prop)
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# For some reason some entities in THL use "title", others use "name"
|
|
128
|
+
TITLEPROP = {
|
|
129
|
+
:folder => :name,
|
|
130
|
+
:list => :name,
|
|
131
|
+
:task => :title
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
end ### module Constants
|
|
135
|
+
|
|
136
|
+
####################################################################
|
|
137
|
+
|
|
138
|
+
# Wrapper around an AS Things node to provide easier access to its props
|
|
139
|
+
# For each property 'prop' of the node, it defines two methods:
|
|
140
|
+
# prop() returns the value, converting :missing_value to nil
|
|
141
|
+
# prop?() returns true/false to indicate if the value is set and is not :missing_value
|
|
142
|
+
# Not all the AS properties respond properly to get(), so not all of the
|
|
143
|
+
# auto-defined methods will work properly.
|
|
144
|
+
class ThingsNode
|
|
145
|
+
attr_accessor :node
|
|
146
|
+
|
|
147
|
+
@@defined={}
|
|
148
|
+
|
|
149
|
+
def initialize(node)
|
|
150
|
+
@node = node
|
|
151
|
+
@values = {}
|
|
152
|
+
return unless @@defined.empty?
|
|
153
|
+
(node.properties + node.elements).each do |prop|
|
|
154
|
+
next if @@defined.has_key?(prop)
|
|
155
|
+
puts "Defining ThingsNode.#{prop}" if $DEBUG
|
|
156
|
+
@@defined[prop]=true
|
|
157
|
+
case prop
|
|
158
|
+
# For area, project and some others, convert the result to a ThingsNode as well
|
|
159
|
+
when 'area', 'project', 'areas', 'projects', 'parent_tag', 'delegate'
|
|
160
|
+
ThingsNode.class_eval <<-EOF
|
|
161
|
+
def #{prop}
|
|
162
|
+
return @values['#{prop}'] if @values.has_key?('#{prop}')
|
|
163
|
+
value=node.#{prop}.get
|
|
164
|
+
@values['#{prop}']=case value
|
|
165
|
+
when nil, :missing_value
|
|
166
|
+
nil
|
|
167
|
+
else
|
|
168
|
+
ThingsNode.new(value)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
def #{prop}?
|
|
172
|
+
!!#{prop}
|
|
173
|
+
end
|
|
174
|
+
EOF
|
|
175
|
+
when 'to_dos', 'tags'
|
|
176
|
+
# For to_dos and tags, map the returned array to ThingsNode as well
|
|
177
|
+
ThingsNode.class_eval <<-EOF
|
|
178
|
+
def #{prop}
|
|
179
|
+
return @values['#{prop}'] if @values.has_key?('#{prop}')
|
|
180
|
+
value=node.#{prop}.get
|
|
181
|
+
@values['#{prop}']=case value
|
|
182
|
+
when nil, :missing_value
|
|
183
|
+
nil
|
|
184
|
+
else
|
|
185
|
+
value.map { |n| ThingsNode.new(n) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
def #{prop}?
|
|
189
|
+
!!#{prop}
|
|
190
|
+
end
|
|
191
|
+
EOF
|
|
192
|
+
else
|
|
193
|
+
ThingsNode.class_eval <<-EOF
|
|
194
|
+
def #{prop}
|
|
195
|
+
return @values['#{prop}'] if @values.has_key?('#{prop}')
|
|
196
|
+
value=node.#{prop}.get
|
|
197
|
+
value=nil if value==:missing_value
|
|
198
|
+
@values['#{prop}']=value
|
|
199
|
+
end
|
|
200
|
+
def #{prop}?
|
|
201
|
+
!!#{prop}
|
|
202
|
+
end
|
|
203
|
+
EOF
|
|
204
|
+
# Some smarts for specific attributes
|
|
205
|
+
if prop == 'class_'
|
|
206
|
+
ThingsNode.class_eval 'alias_method :type, :class_'
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end ### ThingsNode
|
|
212
|
+
|
|
213
|
+
####################################################################
|
|
214
|
+
|
|
215
|
+
class Converter
|
|
216
|
+
attr_accessor :options, :things, :thl
|
|
217
|
+
|
|
218
|
+
def initialize(opt_struct = nil, things_location = nil, thl_location = nil)
|
|
219
|
+
@options=opt_struct || OpenStruct.new
|
|
220
|
+
thingsappname=things_location || 'Things'
|
|
221
|
+
thlappname=thl_location || 'The Hit List'
|
|
222
|
+
begin
|
|
223
|
+
@things = Appscript.app(thingsappname)
|
|
224
|
+
@thl = Appscript.app(thlappname)
|
|
225
|
+
rescue ApplicationNotFoundError => e
|
|
226
|
+
puts "I could not open one of the needed applications: #{e.message}"
|
|
227
|
+
exit(1)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Structure to keep track of already create items
|
|
231
|
+
# These hashes are indexed by Things node ID (node.id_). Each element
|
|
232
|
+
# contains a hash with two elements:
|
|
233
|
+
# :things_node - pointer to the corresponding AS node in Things
|
|
234
|
+
# :thl_node - pointer to the corresponding AS node in THL
|
|
235
|
+
@cache_nodes = {}
|
|
236
|
+
|
|
237
|
+
# Cache of which items are contained in each focus (Inbox, etc.)
|
|
238
|
+
# Indexed by focus name, value is a hash with elements keyed by the
|
|
239
|
+
# id_ of each node that belongs to that focus. Existence of the key
|
|
240
|
+
# indicates existence in the focus.
|
|
241
|
+
@cache_focus = {}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Get the type of the THL node that corresponds to the given Things node,
|
|
245
|
+
# depending on the options specified
|
|
246
|
+
def thl_node_type(node)
|
|
247
|
+
Constants::STRUCTURES[options.structure][node.type][0]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Get the name/title for a THL node.
|
|
251
|
+
def thl_node_name(node)
|
|
252
|
+
node.properties_.get[Constants::TITLEPROP[node.class_.get]]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def props_from_node(node)
|
|
256
|
+
newprops={}
|
|
257
|
+
# Process the properties, according to how the mapping is specified in STRUCTURES
|
|
258
|
+
pm=Constants::STRUCTURES[options.structure][node.type][1]
|
|
259
|
+
proptoset=nil
|
|
260
|
+
pm.keys.each do |k|
|
|
261
|
+
case pm[k]
|
|
262
|
+
when Symbol
|
|
263
|
+
proptoset=pm[k]
|
|
264
|
+
value=node.node.properties_.get[k]
|
|
265
|
+
newprops[proptoset] = value if value && value != :missing_value
|
|
266
|
+
when Array
|
|
267
|
+
proptoset=pm[k][0]
|
|
268
|
+
value=pm[k][1][node.node.properties_.get[k]]
|
|
269
|
+
newprops[proptoset] = value if value && value != :missing_value
|
|
270
|
+
when Hash
|
|
271
|
+
value = node.node.properties_.get[k]
|
|
272
|
+
if value && value != :missing_value
|
|
273
|
+
proptoset = pm[k][value]
|
|
274
|
+
if proptoset
|
|
275
|
+
newprops[proptoset] = true
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
else
|
|
279
|
+
puts "Invalid class for pm[k]=#{pm[k]} (#{pm[k].class})"
|
|
280
|
+
end
|
|
281
|
+
puts "Mapping node.#{k} (#{node.node.properties_.get[k]}) to newprops[#{proptoset}]=#{newprops[proptoset]}" if $DEBUG
|
|
282
|
+
end
|
|
283
|
+
# Do any necesary postprocessing
|
|
284
|
+
postproc=Constants::STRUCTURES[options.structure][node.type][2]
|
|
285
|
+
if postproc
|
|
286
|
+
puts "Calling post-processor #{postproc.to_s} with newprops=#{newprops.inspect}" if $DEBUG
|
|
287
|
+
postproc.call(node, newprops, self)
|
|
288
|
+
puts "After post-processor: #{newprops.inspect}" if $DEBUG
|
|
289
|
+
end
|
|
290
|
+
return newprops
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def loose_tasks_name(parent)
|
|
294
|
+
if parent == top_level_node
|
|
295
|
+
"Loose tasks"
|
|
296
|
+
else
|
|
297
|
+
thl_node_name(parent) + " - loose tasks"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Find or create a list or a folder inside the given parent (or the top-level folders group if not given)
|
|
302
|
+
def find_or_create(what, name, parent = @thl.folders_group.get)
|
|
303
|
+
unless what == :list || what == :folder
|
|
304
|
+
raise "find_or_create: 'what' parameter has to be :list or :folder"
|
|
305
|
+
end
|
|
306
|
+
puts "parent of #{name} = #{parent}" if $DEBUG
|
|
307
|
+
if parent.class_.get != :folder
|
|
308
|
+
raise "find_or_create: parent is not a folder, it's a #{parent.class_.get}"
|
|
309
|
+
else
|
|
310
|
+
if parent.groups[name].exists
|
|
311
|
+
parent.groups[name].get
|
|
312
|
+
else
|
|
313
|
+
parent.end.make(:new => what, :with_properties => {:name => name})
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def new_folder(name, parent = @thl.folders_group.get)
|
|
319
|
+
parent.end.make(:new => :folder,
|
|
320
|
+
:with_properties => { :name => name })
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Return the provided top level node, or the folders group if the option is not specified
|
|
324
|
+
def top_level_node
|
|
325
|
+
return @thl.folders_group.get unless options.toplevel
|
|
326
|
+
|
|
327
|
+
unless @top_level_node
|
|
328
|
+
# Create the top-level node if we don't have it cached yet
|
|
329
|
+
@top_level_node=new_folder(options.toplevel)
|
|
330
|
+
end
|
|
331
|
+
@top_level_node
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Create (if necessary) and return an appropriate THL container
|
|
335
|
+
# for a given top-level Things focus.
|
|
336
|
+
# If --top-level-folder is specified, all of them are simply
|
|
337
|
+
# folders inside that folder.
|
|
338
|
+
# Otherwise:
|
|
339
|
+
# Inbox => Inbox
|
|
340
|
+
# Next => default top level node
|
|
341
|
+
# Scheduled, Someday => correspondingly-named top-level folders
|
|
342
|
+
# Logbook => 'Completed' top-level folder
|
|
343
|
+
# Projects => 'Projects' list if --projects-as-tasks
|
|
344
|
+
# => 'Projects' folder if --projects-folder was specified
|
|
345
|
+
# => default top level node otherwise
|
|
346
|
+
# Trash => ignore
|
|
347
|
+
# Today => ignore
|
|
348
|
+
def top_level_for_focus(focusnames)
|
|
349
|
+
# We loop through all the focus names given, and return the first
|
|
350
|
+
# one that produces a non-nil container
|
|
351
|
+
focusnames.each do |focusname|
|
|
352
|
+
result = if options.toplevel
|
|
353
|
+
case focusname
|
|
354
|
+
when 'Trash', 'Today'
|
|
355
|
+
nil
|
|
356
|
+
when 'Inbox', 'Next'
|
|
357
|
+
find_or_create(:list, focusname, top_level_node)
|
|
358
|
+
when 'Scheduled', 'Someday', 'Logbook'
|
|
359
|
+
find_or_create(:folder, focusname, top_level_node)
|
|
360
|
+
when 'Projects'
|
|
361
|
+
if options.structure == :projects_as_tasks
|
|
362
|
+
find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
|
|
363
|
+
else
|
|
364
|
+
if options.projectsfolder
|
|
365
|
+
find_or_create(:folder, options.projectsfolder, top_level_node)
|
|
366
|
+
else
|
|
367
|
+
top_level_node
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
else
|
|
371
|
+
puts "Invalid focus name: #{focusname}"
|
|
372
|
+
top_level_node
|
|
373
|
+
end
|
|
374
|
+
else
|
|
375
|
+
# That was easy. Now for the more complicated part.
|
|
376
|
+
case focusname
|
|
377
|
+
when 'Inbox'
|
|
378
|
+
thl.inbox.get
|
|
379
|
+
when 'Next'
|
|
380
|
+
top_level_node
|
|
381
|
+
when 'Scheduled', 'Someday', 'Logbook'
|
|
382
|
+
find_or_create(:folder, focusname, top_level_node)
|
|
383
|
+
when 'Projects'
|
|
384
|
+
if options.structure == :projects_as_tasks
|
|
385
|
+
find_or_create(:list, options.projectsfolder || 'Projects', top_level_node)
|
|
386
|
+
else
|
|
387
|
+
if options.projectsfolder
|
|
388
|
+
find_or_create(:folder, options.projectsfolder, top_level_node)
|
|
389
|
+
else
|
|
390
|
+
top_level_node
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
when 'Trash', 'Today'
|
|
394
|
+
nil
|
|
395
|
+
else
|
|
396
|
+
puts "Invalid focus name: #{focusname}"
|
|
397
|
+
top_level_node
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
return result if result
|
|
401
|
+
end
|
|
402
|
+
nil
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Get the top-level focus for a node. If it's not directly contained
|
|
406
|
+
# in a focus, check its project and area, if any.
|
|
407
|
+
def top_level_for_node(node)
|
|
408
|
+
# Areas are always contained at the top level, unless they
|
|
409
|
+
# are suspended
|
|
410
|
+
if node.type == :area
|
|
411
|
+
if node.suspended
|
|
412
|
+
return top_level_for_focus('Someday')
|
|
413
|
+
else
|
|
414
|
+
return top_level_node
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Else, find if the node is contained in a top-level focus...
|
|
419
|
+
foci=focus_of(node)
|
|
420
|
+
# ...if not, look at its project and area
|
|
421
|
+
if foci.empty?
|
|
422
|
+
if node.project?
|
|
423
|
+
tl=top_level_for_node(node.project)
|
|
424
|
+
if !tl && node.area?
|
|
425
|
+
tl=top_level_for_node(node.area)
|
|
426
|
+
end
|
|
427
|
+
elsif node.area?
|
|
428
|
+
tl=top_level_for_node(node.area)
|
|
429
|
+
end
|
|
430
|
+
return tl
|
|
431
|
+
end
|
|
432
|
+
top_level_for_focus(foci)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# See if we have processed a node already - in that case return
|
|
436
|
+
# the cached THL node. Otherwise, invoke the process() function
|
|
437
|
+
# on it, which will put it in the cache.
|
|
438
|
+
def get_cached_or_process(node)
|
|
439
|
+
node_id=node.id_
|
|
440
|
+
if @cache_nodes.has_key?(node_id)
|
|
441
|
+
@cache_nodes[node_id][:thl_node]
|
|
442
|
+
else
|
|
443
|
+
# If we don't have the corresponding node cached yet, do it now
|
|
444
|
+
process(node)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Create if necessary and return an appropriate THL container
|
|
449
|
+
# object for the new node, according to the node's class and
|
|
450
|
+
# options selected. A task in THL can only be contained in a list
|
|
451
|
+
# or in another task. So if parent is a folder and note is a task,
|
|
452
|
+
# we need to find or create an auxiliary list to contain it.
|
|
453
|
+
def container_for(node)
|
|
454
|
+
# If its top-level container is nil, it means we need to skip this node
|
|
455
|
+
# unless it's an area, areas don't have a focus
|
|
456
|
+
tlcontainer=top_level_for_node(node)
|
|
457
|
+
return nil unless tlcontainer
|
|
458
|
+
|
|
459
|
+
# Otherwise, run through the process
|
|
460
|
+
container = case node.type
|
|
461
|
+
when :area
|
|
462
|
+
tlcontainer
|
|
463
|
+
when :project
|
|
464
|
+
if options.areas && node.area?
|
|
465
|
+
get_cached_or_process(node.area)
|
|
466
|
+
else
|
|
467
|
+
tlcontainer
|
|
468
|
+
end
|
|
469
|
+
when :selected_to_do
|
|
470
|
+
if node.project?
|
|
471
|
+
get_cached_or_process(node.project)
|
|
472
|
+
elsif node.area? && options.areas
|
|
473
|
+
get_cached_or_process(node.area)
|
|
474
|
+
else
|
|
475
|
+
# It's a loose task
|
|
476
|
+
tlcontainer
|
|
477
|
+
end
|
|
478
|
+
else
|
|
479
|
+
raise "Invalid Things node type: #{node.type}"
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Now we check the container type. Tasks can only be contained in lists,
|
|
483
|
+
# so if the container is a folder, we have to create a list to hold the task
|
|
484
|
+
if container && (container.class_.get == :folder) && (thl_node_type(node) == :task)
|
|
485
|
+
if node.type == :project
|
|
486
|
+
find_or_create(:list, options.projectsfolder || 'Projects', container)
|
|
487
|
+
else
|
|
488
|
+
find_or_create(:list, loose_tasks_name(container), container)
|
|
489
|
+
end
|
|
490
|
+
else
|
|
491
|
+
container
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def create_in_thl(node, parent)
|
|
496
|
+
if (parent)
|
|
497
|
+
new_node_type = thl_node_type(node)
|
|
498
|
+
new_node_props = props_from_node(node)
|
|
499
|
+
additional_nodes = new_node_props.delete(:__newnodes__)
|
|
500
|
+
result=parent.end.make(:new => new_node_type,
|
|
501
|
+
:with_properties => new_node_props )
|
|
502
|
+
if node.type == :area || node.type == :project
|
|
503
|
+
@cache_nodes[node.id_]={}
|
|
504
|
+
@cache_nodes[node.id_][:things_node] = node
|
|
505
|
+
@cache_nodes[node.id_][:thl_node] = result
|
|
506
|
+
end
|
|
507
|
+
# Add new nodes
|
|
508
|
+
if additional_nodes
|
|
509
|
+
additional_nodes.each do |n|
|
|
510
|
+
result.end.make(n)
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
return result
|
|
514
|
+
else
|
|
515
|
+
parent
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Process a single node. Returns the new THL node.
|
|
520
|
+
def process(node)
|
|
521
|
+
return unless node
|
|
522
|
+
# Skip if we have already processed it
|
|
523
|
+
return if @cache_nodes.has_key?(node.id_)
|
|
524
|
+
# Areas don't have status, so we skip the check
|
|
525
|
+
unless node.type == :area
|
|
526
|
+
return if ( node.status == :completed || node.status == :canceled ) && !(options.completed)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
container=container_for(node)
|
|
530
|
+
puts "Container for #{node.name}: #{container}" if $DEBUG
|
|
531
|
+
unless container
|
|
532
|
+
puts "Skipping trashed task '#{node.name}'" unless options.quiet
|
|
533
|
+
return
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
unless (options.quiet)
|
|
537
|
+
bullet = (node.type == :area) ? "*" : ((node.status == :completed) ? "✓" : (node.status == :canceled) ? "×" : "-")
|
|
538
|
+
puts bullet + " " + node.name + " (#{node.type})"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
newnode=create_in_thl(node, container)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Get all the focus names
|
|
545
|
+
def get_focusnames(all=false)
|
|
546
|
+
# Get only top-level items of type :list (areas are also there, but with type :area) unless all==true
|
|
547
|
+
@cached_focusnames||=things.lists.get.select {|l| all || l.class_.get == :list }.map { |focus| focus.name.get }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Create the focus caches
|
|
551
|
+
def create_focuscaches
|
|
552
|
+
get_focusnames.each { |focus|
|
|
553
|
+
puts "Creating focus cache for #{focus}..." if $DEBUG
|
|
554
|
+
@cache_focus[focus] = {}
|
|
555
|
+
next if focus == "Logbook" && !options.completed
|
|
556
|
+
things.lists[focus].to_dos.get.each { |t|
|
|
557
|
+
@cache_focus[focus][t.id_.get] = true
|
|
558
|
+
}
|
|
559
|
+
puts " Cache: #{@cache_focus[focus].inspect}" if $DEBUG
|
|
560
|
+
}
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Get the focuses in which a task is visible
|
|
564
|
+
def focus_of(node)
|
|
565
|
+
result=[]
|
|
566
|
+
get_focusnames.each { |focus|
|
|
567
|
+
if in_focus?(focus, node)
|
|
568
|
+
result.push(focus)
|
|
569
|
+
end
|
|
570
|
+
}
|
|
571
|
+
if node.type == :area && node.suspended
|
|
572
|
+
result.push('Someday')
|
|
573
|
+
end
|
|
574
|
+
result
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Check if a node is in a certain focus
|
|
578
|
+
# Node can be a ThingsNode, and AS node, or a node ID
|
|
579
|
+
def in_focus?(focus, node)
|
|
580
|
+
unless @cache_focus[focus]
|
|
581
|
+
create_focuscaches
|
|
582
|
+
end
|
|
583
|
+
case node
|
|
584
|
+
when ThingsNode
|
|
585
|
+
key = node.id_
|
|
586
|
+
when Appscript::Reference
|
|
587
|
+
key = node.id_.get
|
|
588
|
+
when String
|
|
589
|
+
key = node
|
|
590
|
+
else
|
|
591
|
+
puts "Unknown node object type: #{node.class}"
|
|
592
|
+
return nil
|
|
593
|
+
end
|
|
594
|
+
return @cache_focus[focus].has_key?(key)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
###-------------------------------------------------------------------
|
|
598
|
+
### Methods to fix new nodes - called from the postproc block in STRUCTURES
|
|
599
|
+
|
|
600
|
+
# Things sets both 'completion_date' and 'cancellation_date'
|
|
601
|
+
# for both completed and canceled tasks, which confuses THL,
|
|
602
|
+
# so we delete the one that should not be there.
|
|
603
|
+
def fix_completed_canceled(node,prop)
|
|
604
|
+
if prop[:completed_date] && prop[:canceled_date]
|
|
605
|
+
if prop[:canceled]
|
|
606
|
+
prop.delete(:completed)
|
|
607
|
+
prop.delete(:completed_date)
|
|
608
|
+
else
|
|
609
|
+
prop.delete(:canceled)
|
|
610
|
+
prop.delete(:canceled_date)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Archive completed/canceled if requested
|
|
616
|
+
def archive_completed(prop)
|
|
617
|
+
prop[:archived] = true if options.archivecompleted && (prop[:completed] || prop[:canceled])
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Add tags to title
|
|
621
|
+
def add_tags(node, prop, inherit_project_tags, inherit_area_tags)
|
|
622
|
+
tasktags = node.tags.map {|t| t.name }
|
|
623
|
+
if inherit_project_tags
|
|
624
|
+
# Merge project and area tags
|
|
625
|
+
if node.project?
|
|
626
|
+
tasktags |= node.project.tags.map {|t| t.name }
|
|
627
|
+
if options.areas && node.project.area?
|
|
628
|
+
tasktags |= node.project.area.tags.map {|t| t.name }
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
if options.areas && node.area? && inherit_area_tags
|
|
633
|
+
tasktags |= node.area.tags.map {|t| t.name }
|
|
634
|
+
end
|
|
635
|
+
unless tasktags.empty?
|
|
636
|
+
prop[:title] = [prop[:title], tasktags.map {|t| "/" + t + (t.index(" ")?"/":"") }].join(' ')
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Check if node is in the Today list
|
|
641
|
+
def check_today(node, prop)
|
|
642
|
+
if in_focus?('Today', node)
|
|
643
|
+
prop[:start_date] = Time.parse('today at 00:00')
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def add_extra_node(prop, newnode)
|
|
648
|
+
prop[:__newnodes__] = [] unless prop.has_key?(:__newnodes__)
|
|
649
|
+
prop[:__newnodes__].push(newnode)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# Add a new task containing project notes when the project is a THL list,
|
|
653
|
+
# since THL lists cannot have notes
|
|
654
|
+
def add_list_notes(node, prop)
|
|
655
|
+
new_node_type = thl_node_type(node)
|
|
656
|
+
# Process notes only for non-areas
|
|
657
|
+
if (node.type != :area)
|
|
658
|
+
# If the node has notes but the THL node is a list, add the notes as a task in there
|
|
659
|
+
# If the node has notes but the THL node is a folder (this shouldn't happen), print a warning
|
|
660
|
+
if node.notes? && new_node_type == :list
|
|
661
|
+
newnode = {
|
|
662
|
+
:new => :task,
|
|
663
|
+
:with_properties => { :title => "Notes for '#{prop[:name]}'", :notes => node.notes }}
|
|
664
|
+
# Mark as completed if the project is completed
|
|
665
|
+
if node.status == :completed || node.status == :canceled
|
|
666
|
+
newnode[:with_properties][:completed] = true
|
|
667
|
+
archive_completed(newnode[:with_properties])
|
|
668
|
+
end
|
|
669
|
+
add_extra_node(prop, newnode)
|
|
670
|
+
end
|
|
671
|
+
if node.notes? && new_node_type == :folder
|
|
672
|
+
$stderr.puts "Error: cannot transfer notes into new folder: #{node.notes}"
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# When projects are lists, if the project has a due date, we add a bogus task to it
|
|
678
|
+
# to represent its due date, since lists in THL cannot have due dates.
|
|
679
|
+
def add_project_duedate(node, prop)
|
|
680
|
+
new_node_type = thl_node_type(node)
|
|
681
|
+
return unless node.type == :project && new_node_type == :list && node.due_date?
|
|
682
|
+
|
|
683
|
+
# Create the new node
|
|
684
|
+
newnode = {
|
|
685
|
+
:new => :task,
|
|
686
|
+
:with_properties => { :title => "Due date for '#{prop[:name]}'", :due_date => node.due_date }
|
|
687
|
+
}
|
|
688
|
+
add_extra_node(prop, newnode)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
end #### class Converter
|
|
692
|
+
|
|
693
|
+
####################################################################
|
|
694
|
+
|
|
695
|
+
def Things2THL.new(opt_struct = nil, things_db = nil, thl_location = nil)
|
|
696
|
+
Converter.new(opt_struct, things_db, thl_location)
|
|
697
|
+
end
|
|
698
|
+
end
|
data/things2thl.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = %q{things2thl}
|
|
5
|
+
s.version = "0.2.0"
|
|
6
|
+
|
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
8
|
+
s.authors = ["Diego Zamboni"]
|
|
9
|
+
s.date = %q{2009-05-18}
|
|
10
|
+
s.default_executable = %q{things2thl}
|
|
11
|
+
s.description = %q{Library and command-line tool for migrating Things data to The Hit List}
|
|
12
|
+
s.email = %q{diego@zzamboni.org}
|
|
13
|
+
s.executables = ["things2thl"]
|
|
14
|
+
s.extra_rdoc_files = [
|
|
15
|
+
"README"
|
|
16
|
+
]
|
|
17
|
+
s.files = [
|
|
18
|
+
"Manifest",
|
|
19
|
+
"README",
|
|
20
|
+
"Rakefile",
|
|
21
|
+
"VERSION",
|
|
22
|
+
"bin/things2thl",
|
|
23
|
+
"lib/Things2THL.rb",
|
|
24
|
+
"things2thl.gemspec"
|
|
25
|
+
]
|
|
26
|
+
s.has_rdoc = true
|
|
27
|
+
s.homepage = %q{http://zzamboni.github.com/things2thl/}
|
|
28
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
29
|
+
s.require_paths = ["lib"]
|
|
30
|
+
s.rubygems_version = %q{1.3.1}
|
|
31
|
+
s.summary = %q{Library and command-line tool for migrating Things data to The Hit List}
|
|
32
|
+
|
|
33
|
+
if s.respond_to? :specification_version then
|
|
34
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
35
|
+
s.specification_version = 2
|
|
36
|
+
|
|
37
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
38
|
+
else
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
end
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zzamboni-things2thl
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Diego Zamboni
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-05-18 00:00:00 -07:00
|
|
13
|
+
default_executable: things2thl
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Library and command-line tool for migrating Things data to The Hit List
|
|
17
|
+
email: diego@zzamboni.org
|
|
18
|
+
executables:
|
|
19
|
+
- things2thl
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- README
|
|
24
|
+
files:
|
|
25
|
+
- Manifest
|
|
26
|
+
- README
|
|
27
|
+
- Rakefile
|
|
28
|
+
- VERSION
|
|
29
|
+
- bin/things2thl
|
|
30
|
+
- lib/Things2THL.rb
|
|
31
|
+
- things2thl.gemspec
|
|
32
|
+
has_rdoc: true
|
|
33
|
+
homepage: http://zzamboni.github.com/things2thl/
|
|
34
|
+
post_install_message:
|
|
35
|
+
rdoc_options:
|
|
36
|
+
- --charset=UTF-8
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: "0"
|
|
44
|
+
version:
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: "0"
|
|
50
|
+
version:
|
|
51
|
+
requirements: []
|
|
52
|
+
|
|
53
|
+
rubyforge_project:
|
|
54
|
+
rubygems_version: 1.2.0
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 2
|
|
57
|
+
summary: Library and command-line tool for migrating Things data to The Hit List
|
|
58
|
+
test_files: []
|
|
59
|
+
|