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
<span class="keyword">end</span>


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? } 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[] 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><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>[]
<span class="keyword">end</span>


def self.process(tasks, handler) new(tasks, handler).process end


def process_task(task, level = 0) @handler.handle(task, level) @processed[] = 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 = <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



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 projects_file tasks_file = projects.backend.path tasks = Task.parse tasks_file task_list = tasks


Tags , , , , 1 comment no trackbacks