Toxic Elephant

Don't bury it in your back yard!

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">&quot;</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">&quot;</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))

Tags , , , , 1 comment no trackbacks

Comments

  1. Matijs van Zuijlen said 06/06/2018 at 12h51:

    If you want to suggest changes, there's [a gist](https://gist.github.com/mvz/f781b36eee0fbfb9e36407e75bd21bec) for this.

Comments are disabled