Omniboard is a static kanban webpage generator for OmniFocus. Use it to view your currently active projects as a kanban board, with custom sorting and grouping options.

You will need

Installing

Omniboard comes as a gem, although it's not currently hosted on rubygems or the like.

git clone https://github.com/jyruzicka/omniboard.git
cd omniboard
gem build omniboard.gemspec
gem install omniboard-1.2.5.gem

Alternatively, add it to your Gemfile:

gem "omniboard", git: "https://github.com/jyruzicka/omniboard.git"

Running

Once installed, use the built-in binary:

omniboard

or try the following to see the kanban in your web browser:

omniboard -o kanban.html
open kanban.html

To see the possible flags, run:

omniboard --help

Flags

Columns

Your omniboard is made up a number of columns, with each column made up of a number of different projects from OmniFocus.

The first time you run omniboard, you will generate a configuration folder at ~/.omniboard/. You can find two types of files in your configuration folder: first, a database file (which stores the current cached state of OmniFocus), and second, a series of configuration and column files, all maintained within a folder named columns.

You can create a new column in your Kanban by creating a new file in the columns folder. Name it whatever you like, but make sure that it ends with .rb. Inside, you configure a column using the following syntax:

Omniboard::Column.new "Column name" do
end

You should also find a file called config.rb. This is where you can place global configuration values.

Customising columns

You can customise your columns in all manner of ways. These methods either govern how the column is displayed, or which projects will end up in there.

The following column properties are numeric or symbols. You can alter these inside the column block in the following manner:

Omniboard::Column.new "Sample column" do
        order 1
        display :compact
end

The following column properties take blocks of ruby code. You can alter these inside the column block in the following manner:

Omniboard::Column.new "Active projects only" do

        conditions do |p|
                p.active?
        end
end

Grouping projects

You may want to group your projects - for example, by parent folder, by flagged status, or by due date. Each column may group its projects in different ways, or you may assign one default grouping method to be used over the whole board.

Each group is associated with an identifier - this could be an object, a true/false value, a string, whatever you want. You can use the group_by method to set this identifier:

Omniboard::Column.new "Due soon" do
        group_by{ |p| p.due.to_date - Date.today }
end

This will group each project by how many days in the future it's due. The identifier will be an integer.

You can sort groups as well, using the sort_groups method:

Omniboard::Column.new "Due soon" do
        group_by{ |p| p.due.to_date - Date.today }
        sort_groups{ |i| i }
end

The sort_groups block takes arguments in the same way that sort does, although the arguments passed to the block are the identifiers of the relevant groups.

It's nice to have fancy names for your groups, and sometimes they'll be a little more involved than just the string representations of the group identifiers. You can set groups' names using the group_name block. Again, it gets passed the identifier for each group, and returns the string name of the group:

Omniboard::Column.new "Due soon" do
        group_by{ |p| p.due.to_date - Date.today }
        sort_groups{ |i| i }

        group_name{ |i| "Due in " + (i == 1 ? "1 day" : "#{i} days") }
end

By default, each group is given an arbitrary colour. Sometimes you might want to override that. You can do this with the colour_group method. Note that you set this globally using the Omniboard::Column.config method (see next section for more on this):

Omniboard::Column.config do
  group_by{ |p| p.container || "" }

        colour_group(50){ |identifier| identifier.respond_to?(:name) && identifier.name == "Home Projects" }
end

The colour_group method takes two arguments: first, a numerical value which represents the hue you want the group to be, and a block which takes the group identifier and returns true or false. In the above example, we're using the containing folder as the group identifier. If the containing folder's name is “Home Projects”, we set the group's colour to 50.

A note on groups and identifiers

You may find that upon creating a new column, with a new grouping block, you get an error that looks something like this:

.omniboard/columns/config.rb16:in block (2 levels) in <top (required)>': undefined method 'ancestry' for (1/1):Rational (NoMethodError)
from /usr/local/lib/ruby/gems/2.4.0/gems/omniboard-1.2.1/lib/omniboard/column.rb:163:in 'sort'
from /usr/local/lib/ruby/gems/2.4.0/gems/omniboard-1.2.1/lib/omniboard/column.rb:163:in 'groups'
...

This is probably due to differing group identifiers. For example, the “Due Soon” column above uses integers as group identifiers (1, 2, etc.), while your default columns use Rubyfocus::Folders:

Omniboard::Column.config do
        group_by{ |p| p.container || "" }
        ...
end

Since the default group is a Rubyfocus::Folder, the default sorting and group naming methods assume they're being passed one (or more) Rubyfocus::Folder. If they get passed an integer (or a string, or a boolean, or anything else), they'll throw an error.

The moral of the story: if you're making a new column, with a new group_by method, you probably need to provide a sort_groups and group_name method as well.

Global configuration

You may apply global configuration properties by the following code:

Omniboard::Column.config do
        # Config values here...
end

The majority of the column properties listed above can be applied inside of Omniboard::Column.config as well, giving a resulting global value. How omniboard uses this value depends on the property in question:

You can also set the following configuration options:

Countering global configurations

You may find yourself in a situation where you want to have a default group block for every column except one, which you would prefer to leave ungrouped. In this case, you can specify “no value” (overriding the column default) by setting the property to nil:

Omniboard::Column.new "Don't group me" do
        group_by nil
end

Custom CSS

You can also change the look and feel of your kanban board by customising the CSS. All changes go into the file custom.css, stored in your Omniboard folder (by default, in ~/.omniboard).

Case studies

That's quite a lot to take in, so let's have a look at some of the ways I use this system.

Leaving out folders

Leaving out an entire single folder

I have a bunch of template projects for use in Chris Sauve's amazing Templates.scpt. There's no point in displaying these on my kanban board, so instead I'll block them using a global conditions block.

Omniboard::Column.config do
        conditions do |p|
                !p.contained_within?(name: "Template")
        end
end

This block will return true only if the project is not contained within a folder that has the name “Template”.

Leaving out an entire folder including all subfolders

Sometimes you want to leave out an entire folder including all subfolders. For example, when your folder “Template” also has subfolders such as “Work Projects”, “Private Things” or if you add additional subfolders or rename existing subfolders from time to time. The easiest way to block all subfolders as well:

Omniboard::Column.config do
        conditions do |p|
                !p.ancestry.any?{ |c| c.name == "Template" }
        end
end

Mark based on a note's contents

I use projects' note fields to highlight my important projects that I want to focus on right now. Using a global mark_when block, I can make sure that projects I've flagged in this way always show up marked.

Omniboard::Column.config do
        mark_when do |p|
                p.note && p.note.include?("@flagged")
        end
end

Group & sort based on folders

I like to see my projects grouped by their parent folders, but they're sometimes buried two or three folders deep. Since group names really need to be strings, I can format group names right inside the block.

Omniboard::Column.config do
        group_by do |p|
                if p.ancestry == []
                        "Top level"
                else
                        p.ancestry.map(&:name).reverse.join("→")
                end
        end
end

Note that Project#ancestry is a method that returns an array of the project's containing folders, up to the Document level. If the project has no containing folder, we just give it the group “Top level”. If we just wanted to group based on the top-most folder, we could do something like the following:

Omniboard::Column.config do
        group_by do |p|
                if p.ancestry == []
                        "Top level"
                else
                        p.ancestry.last.name
                end
        end
end

If we want to sort these folders nicely, we could do something like the following:

Omniboard::Column.config do
        sort_groups do |x,y|
                        if x == "Top level"
                                -1
                        elsif y == "Top level"
                                1
                        else
                                x <=> y
                        end
                end
        end
end

This means that a group with the name “Top level” appears at the top of the column; after this groups are displayed in alphabetical order.

Displaying a “backburner” column

I have a compacted column on the left of my Kanban board showing all the projects that are on hold or that have been deferred at the project level. The column's config looks like this:

Omniboard::Column.new "Backburner" do

        conditions do |p|
                p.on_hold? || p.deferred?
        end

        order 0

        display :compact
end

Displaying an “active projects” column

My main column is quite large, taking up twice as much space as regular columns. The projects are displayed as full tickets, with four projects per row:

Omniboard::Column.new "In Progress" do
        order 1
        width 2
        columns 4
        display :full

Only projects which get through the global filter, and are marked “active” in OmniFocus, are shown:

conditions do |p|
                p.active?
        end

Dim a task when you can't do anything

I have a custom context “Waiting for…” which marks tasks I'm waiting to hear from others on. When all available tasks are “Waiting for…” tasks, I can't do anything, so I might as well dim the project.

dim_when do |p|
                p.next_tasks.all? { |t| t.context && t.context.name == "Waiting for..." }
        end

I'd also like to sort these tasks to the end of my projects in each group:

sort do |p|
                if p.next_tasks.all?{ |t| t.context && t.context.name == "Waiting for..." }
                        1
                else
                        0
                end
        end

Show an icon for a given context

In fact, I could mark these “Waiting for…” projects with an icon, just so I know why they're dimmed.

icon do |p|
                if p.next_tasks.all?{ |t| t.context && t.context.name == "Waiting for..." }
                        "waiting.png"
                end
        end

Filter by date

What if you want to filter by defer, due, or completion date? For example:

Before I start on this, I'd like to quickly talk about project vs. task due dates. OmniFocus treats each project as a sub-class of a task, with a couple of extra properties. This means that any property you can assign to a task, you can (in theory) assign to a project. This means that a project can be marked “deferred” for two reasons:

Similarly, it's a good idea to consider the due date on both tasks within a project, and the project itself.

For our first trick, let's consider a column containing projects whose due date is within the next week. Every task has a due property which returns a Time or nil:

one_week_from_now = Time.now + (60 * 60 * 24 * 7)

Omniboard::Column.new "Due soon" do
        conditions do |p|
                p.active? && p.due && p.due <= one_week_from_now
        end
end

This checks the project's due property, and if it's not null (and is less than one week from now), includes it in the board. We also filter out on-hold, completed, or dropped projects.

The “completed” column is pretty similar:

one_week_ago = Time.now - (60 * 60 * 24 * 7)

Omniboard::Column.new "Completed" do
        conditions do |p|
                p.completed? && p.completed >= one_week_ago
        end
end

Note that any completed task or project should always have a set completion time, so we don't need to check if the completed property is nil. You could, just to be double-sure.

Finally, let's look at our “upcoming” column. This is a bit trickier because we're delving into per-task dates, but it's still surprisingly easy to do. One thing I'm going to do here is say that the project's available tasks just need to be deferred to any time tomorrow, not within 24 hours of this exact moment.

tomorrow = Date.today + 1

Omniboard::Column.new "Starting tomorrow" do
        conditions do |p|
                p.actionable_tasks.size == 0 && # No tasks we can do right now
                        p.next_tasks.any?{ |t| t.start.to_date == tomorrow } # Incomplete, non-blocked task starting tomorrow
        end
end

Feedback and further information

If you have any examples of particularly good examples of column configuration, or think I've missed something in the case studies or documentation, get in touch!