Importing GTG tasks into Taskwarrior
Posted by matijs 06/06/2018 at 12h47
I used to use Getting Things Gnome (GTG) to keep my TODO list. However, the project seems dead right in the middle of its Gtk+ 3.0 port, so I’ve been looking around for an alternative. After much consideration, I decided on Taskwarrior. I wanted to keep my old tasks and couldn’t find a nice way to export them from GTG, let alone import them into Taskwarrior. So in the end I decided to create my own exporter.
Getting Things Gnome keeps your tasks in some simple XML files in a known location. HappyMapper is ideal for this. I started out using its automatic mapping, but as my understanding of the GTG format deepened, I switched to explicit mapping of a Task’s attributes and elements.
On the other side, Taskwarrior can import simple JSON files that are super easy to create using JSON from the standard library. The script below will output this format to STDOUT. It’s up to you to use task import
to process it further.
I implemented this as a spike, so there are no tests, but I like to think the design I ended up with is quite testable. I get annoyed whenever code becomes cluttered, or top-level instance variables start to appear. So I tend to quickly split off classes that have a distinct responsibility. I may yet convert this to a real gem and see how easy it is to bring everything under test.
Finally, before showing the code, I should warn you that it’s probably a good idea to back up your existing Taskwarrior data before playing with this.
Here’s the code:
#!/usr/bin/env ruby
require ’happymapper’
require ’json’
class Task
include HappyMapper
attribute :id, String
attribute :status, String
attribute :tags, String
attribute :uuid, String
element :title, String
element :startdate, String
element :duedate, String
element :modified, DateTime
element :donedate, String
has_many :subtasks, String, tag: ’subtask’
element :content, String
end
class TaskList
def initialize(tasks)
@tasks = tasks
<span class="instance-variable">@tasks_hash</span> = {}
<span class="instance-variable">@tasks</span>.each <span class="keyword">do</span> |task|
<span class="instance-variable">@tasks_hash</span>[task.id] = task
<span class="keyword">end</span>
end
def each_task(&block)
@tasks.each &block
end
def find(task_id)
@tasks_hash[task_id]
end
def root_task(task)
parent = @tasks.find { |it| it.subtasks.include? task.id }
parent && root_task(parent) || task
end
end
class TaskProcessor
def initialize(task_list, handler)
@task_list = task_list
@handler = handler
@processed = {}
end
def process
@processed.clear
@task_list.each_task do |task|
next if @processed[task.id]
root = @task_list.root_task(task)
process_task root
end
<span class="instance-variable">@task_list</span>.each_task <span class="keyword">do</span> |task|
raise <span class="string"><span class="delimiter">"</span><span class="content">Task </span><span class="inline"><span class="inline-delimiter">#{</span>task.id<span class="inline-delimiter">}</span></span><span class="content"> not processed</span><span class="delimiter">"</span></span> <span class="keyword">unless</span> <span class="instance-variable">@processed</span>[task.id]
<span class="keyword">end</span>
end
def self.process(tasks, handler)
new(tasks, handler).process
end
private
def process_task(task, level = 0)
@handler.handle(task, level)
@processed[task.id] = true
process_subtasks task.subtasks, level + 1
end
def process_subtasks(subtask_ids, level)
subtask_ids.each do |task_id|
raise "Task #{task_id} already processed" if @processed[task_id]
task = @task_list.find(task_id)
process_task task, level
end
end
end
class TaskWarriorExporter
def initialize(task_list)
@task_list = task_list
end
def handle(task, level)
status = case task.status
when ’Dismiss’
’deleted’
when ’Done’
’completed’
when ’Active’
’pending’
else
raise "Unknown: #{task.status}"
end
data = {
<span class="key">description</span>: task.title,
<span class="key">status</span>: status,
<span class="key">uuid</span>: task.uuid,
}
<span class="keyword">if</span> task.duedate
<span class="keyword">if</span> task.duedate == <span class="string"><span class="delimiter">'</span><span class="content">soon</span><span class="delimiter">'</span></span>
data[<span class="symbol">:priority</span>] = <span class="string"><span class="delimiter">'</span><span class="content">H</span><span class="delimiter">'</span></span>
<span class="keyword">else</span>
data[<span class="symbol">:due</span>] = task.duedate
<span class="keyword">end</span>
<span class="keyword">end</span>
data[<span class="symbol">:end</span>] = task.donedate <span class="keyword">if</span> task.donedate
data[<span class="symbol">:scheduled</span>] = task.startdate <span class="keyword">if</span> task.startdate
entry = guess_entry(task)
data[<span class="symbol">:entry</span>] = entry
subtask_uuids = task.subtasks.map <span class="keyword">do</span> |subtask_id|
<span class="instance-variable">@task_list</span>.find(subtask_id).uuid
<span class="keyword">end</span>
<span class="keyword">if</span> subtask_uuids.any?
data[<span class="symbol">:depends</span>] = subtask_uuids.join(<span class="string"><span class="delimiter">'</span><span class="content">,</span><span class="delimiter">'</span></span>)
<span class="keyword">end</span>
data[<span class="symbol">:tags</span>] = task.tags <span class="keyword">unless</span> task.tags.empty?
<span class="keyword">if</span> task.content
data[<span class="symbol">:annotations</span>] = [ { <span class="key">entry</span>: entry, <span class="key">description</span>: task.content } ]
<span class="keyword">end</span>
puts data.to_json
end
private
def guess_entry(task)
dates = [task.duedate, task.donedate, task.startdate].compact.
reject { |it| %w(someday soon).include? it }.
sort
dates.first || task.modified.to_s
end
end
projects_file = File.expand_path ’~/.local/share/gtg/projects.xml’
projects = HappyMapper.parse File.read projects_file
tasks_file = projects.backend.path
tasks = Task.parse File.read tasks_file
task_list = TaskList.new tasks
TaskProcessor.process(task_list, TaskWarriorExporter.new(task_list))