I wanted to update an outline with information gathered from various web sites and some calculations. I don’t like applescript and my language of choice is Ruby, so this short note gives some code and guidance as to how I did this.
I will use the budget template as an example.
The heavy lifting is done using the Ruby script (update_oo.rb):
require "nokogiri"
require 'date'
class OmniOutliner
OO = {'oo' => "http://www.omnigroup.com/namespace/OmniOutliner/v3"} # shorthand
def initialize (filename)
@filename = filename
@xml_file = File.join(filename, "contents.xml")
@doc = Nokogiri::XML(File.open(@xml_file))
@column_names = @doc.xpath("//oo:column", OO).map do |node|
title = node.xpath("./oo:title/oo:text/oo:p", OO)
title.text.strip
end
# First column has a blank name and seems extra to the values stored per line.
@column_names.shift
end
def update_file (&block)
update_lines(block)
File.write(@xml_file, @doc.to_xml)
end
# Convert the the node into Ruby types.
def value_from_node (node)
case node.name
when 'text'
node.text.strip
when 'null'
nil
when 'date'
DateTime.parse(node.text)
when 'number'
node.text.to_f
else
raise "unknown node type: #{node.name}"
end
end
def node_from_value (value)
case value
when nil
new_node = Nokogiri::XML::Node.new('null', @doc)
when String
lit_node = Nokogiri::XML::Node.new('lit', @doc)
lit_node.content = value
new_node = Nokogiri::XML::Node.new('text', @doc) <<
Nokogiri::XML::Node.new('p', @doc)
new_node.child << Nokogiri::XML::Node.new('run', @doc)
new_node.child.child << lit_node
when Integer, Float
new_node = Nokogiri::XML::Node.new('number', @doc)
new_node.content = value.to_s
when DateTime
new_node = Nokogiri::XML::Node.new('date', @doc)
new_node.content = value.strftime("%Y-%m-%d %H:%M:%S %z")
end
new_node
end
def scan_lines (&block)
line_values = {}
@doc.xpath("//oo:values", OO).each do |line_nodes|
# Convert a line's nodes into something more useful.
line_nodes.xpath("*").each_with_index do |node, i|
line_values[@column_names[i]] = value_from_node(node)
end
block.call(line_values)
end
end
def update_lines (block)
line_values = {}
@doc.xpath("//oo:values", OO).each do |line_nodes|
# Convert a line's nodes into something more useful.
line_nodes.xpath("*").each_with_index do |node, i|
line_values[@column_names[i]] = value_from_node(node)
end
block.call(line_values)
# Update the node with any changed values.
line_nodes.xpath("*").each_with_index do |node, i|
col_name = @column_names[i]
if line_values[col_name] != value_from_node(node)
new_node = node_from_value(line_values[col_name])
node.replace(new_node)
end
end
end
end
end
oo = OmniOutliner.new(ARGV[0])
# This is a trivial examples showing how the budget.oo template file can be
# manipulated. You shouldn't have to make any changes to the code above
# and your updates/calculations should just be in the lines below.
oo.update_file do |values|
# values is a hash holding the columns for a line. The entries in the
# hash are named after the columns. The column names must match exactly.
# If a column doesn't have a value it will be retuned as a nil and most
# expressions using a nil value will cause an exception so it is best
# to test that all the columns for your calculation have values before
# you try to use them.
# Update number values.
if values['Amount']
values['Amount'] *= 2
end
# Update date value.
if values['Date']
values['Date'] = DateTime.now
end
# Update string value.
case values['Topic']
when 'Coffee' then values['Topic'] = 'Tea'
when 'Tea' then values['Topic'] = 'Coffee'
end
end
The first part is a class that parses the omnioutliner file format. It expects the file to be uncompresses so select this in the document inspector. The second part is where the calculations and updating of the data is done. This block of code is called once for every row in the file (at any level) and passes the column values in as a hash (or dictionary). Any values that are updated in the hash will be inserted into the file.
Text, dates and numbers can all be changed and in the example here the Amount field is doubled, the Date field is set to today’s date and the Coffee/Tea text is toggled, but you would probably want to do something more useful :-)
Parsing and manipulating the XML file used by omnioutliner is done using a library (gem in Ruby parlance) called nokogiri. This needs to be installed before this script can be used and unfortunately this has got a lot harder with El Capitan. There are many references on the web to guide you with this.
From the command line you can run this script using:
ruby update_oo.rb path-to-your-oo3-file
It would be convenient to be able to run this from within omnioutliner and this can be done using the following applescript (Update):
tell application id "OOut"
try
set doc_file to file of front document
set filename to POSIX path of file doc_file
set scriptpath to quoted form of POSIX path of ((path to me as text) & "::" & "update_oo.rb")
set thedoc to front document
save thedoc
set have_file_coord to "no"
tell application "Finder" to if exists "/usr/local/bin/file-coord" as POSIX file then set have_file_coord to "yes"
if have_file_coord contains "yes" then
-- Use file update events to let oo know the file has changed on disk.
do shell script "/usr/local/bin/file-coord " & filename & " -- /bin/bash -c \" ruby " & scriptpath & " " & filename & "\""
else
close thedoc
do shell script "/bin/bash -c \" ruby " & scriptpath & " " & filename & "\""
open doc_file
end if
on error errStr number errorNumber
activate
display dialog errStr
end try
end tell
Store this applescript and the Ruby script in ~/Library/Application\ Scripts/com.omnigroup.OmniOutliner4/ and add Update to your toolbar. Clicking on the script will now run it.
Running the script will update the file as already described (if run with budget.003) and the window will close and reopen.
An alternative behaviour is possible where the window doesn’t close and the code and instructions to do this can be found here courtesy of Ken Case. If you compile and move the executable to /usr/local/bin then this will be used to keep the window open and show the updates when ready.
Enjoy!
Dave.