-
-
Notifications
You must be signed in to change notification settings - Fork 91
Creating Plugins
Doing has a plugin architecture that allows you to create custom import and export features. Import plugins take data from external sources/files (like Calendar or Timing.app reports) and create new doing entries from them. Export plugins format selected doing entries in specific ways, such as HTML or CSV output. Or anything you want.
A plugin is a single ruby file located in a plugins directory. The directory is ~/.config/doing/plugins
by default, but you can add a plugins_path
key to ~/.doingrc
to point doing to wherever you want to keep your plugins.
Import and export plugins are similar, but have different required methods. Following is a breakdown of an export plugin. You can also view the full source code for the example plugin.
Include some meta at the top of the plugin. It's optional, but helpful.
# frozen_string_literal: true
#
# title: Export plugin example
# description: Speak the most recent entry (macOS)
# author: Brett Terpstra
# url: https://brettterpstra.com
As well as any info a user would need to use/configure it
# Example
#
# doing show -o sayit
#
# ## Configuration
#
# Change what the plugin says by generating a template with
# `doing template --type say`, saving it to a file, and
# putting the path to that file in `export_templates->say` in
# .doingrc.
#
# export_templates:
# say: /path/to/template.txt
#
# Use a different voice by adding a `say_voice` key to your
# .doingrc. Use `say -v ?` to see available voices.
#
# say_voice: Zarvox
Use the example plugin as a skeleton to define required methods:
module Doing
##
## @brief Plugin class
##
class SayExport
include Doing::Util
def self.settings
end
def self.template(trigger) # Optional
end
def self.render(wwid, items, variables: {})
end
Doing::Plugins.register 'say', :export, self
end
end
This method provides doing with options and configuration for your plugin. It just needs to return a Hash object with the proper keys.
Note that when defining a regular expression for the trigger
, all parenthetical groups should be non-capturing, i.e. (?:...)
.
Once your plugin is installed, you can run doing config --update
to add any keys the plugin registers to your config file.
#-------------------------------------------------------
## Plugin Settings. A plugin must have a self.settings
## method that returns a hash with plugin settings.
##
## trigger: (required) Regular expression to match
## FORMAT when used with `--output FORMAT`. Registered
## name of plugin must be able to match the trigger, but
## alternatives can be included
##
## templates: (optional) Array of templates this plugin
## can export (plugin must have :template method)
##
## Each template is a hash containing:
## - name: display name for template
## - trigger: regular expression for
## `template --type FORMAT`
##
## If a template is included, a config key will
## automatically be added for the user to override
## The config key will be available at:
##
## wwid.config['export_templates'][PLUGIN_NAME]
##
## config: (optional) A Hash which will be
## added to the main configuration in the plugins section.
## Options defined here are included when config file is
## created or updated with `config --update`. Use this to
## add new configuration keys, not to override existing
## ones.
##
## The configuration keys will be available at:
##
## wwid.config['plugins'][PLUGIN_NAME][KEY]
##
## @brief Method to return plugin settings (required)
##
## @return Hash of settings for this plugin
##
def self.settings
{
trigger: 'say(?:it)?',
templates: [
{ name: 'say', trigger: 'say(?:it)?' }
],
config: {
'say_voice' => 'Fiona'
}
}
end
If your plugin allows a user-configured template, include a template method
#-------------------------------------------------------
## Output a template. Only required if template(s) are
## included in settings. The method should return a
## string (not output it to the STDOUT).
##
## @brief Method to return template (optional)
##
## @param trigger The trigger passed to the
## template function. When this
## method defines multiple
## templates, the trigger can be
## used to determine which one is
## output.
##
## @return (String) template contents
##
def self.template(trigger)
return unless trigger =~ /^say(it)?$/
'On %date, you were %title, recorded in section %section%took'
end
Lastly, provide a render method that accepts a WWID object, an array of items, and additional options. This is where import and export plugins differ. An import plugin requires an import
method, an export plugin requires a render
method.
##
## @brief Render data received from an output
## command
##
## @param wwid The wwid object with config
## and public methods
## @param items An array of items to be output
## { <Date>date, <String>title,
## <String>section, <Array>note }
## @param variables Additional variables including
## flags passed to command
## (variables[:options])
##
## @return (String) Rendered output
##
def self.render(wwid, items, variables: {})
return if items.nil? || items.empty?
# the :options key includes the flags passed to the
# command that called the plugin use `puts
# variables.inspect` to see properties and methods
# when run
opt = variables[:options]
# This plugin just grabs the last item in the `items`
# list (which could be the oldest or newest, depending
# on the sort order of the command that called the
# plugin). Most of the time you'll want to use :each
# or :map to generate output.
i = items[-1]
# Format the item. Items are a hash with 3 keys: date,
# title, and section (parent section) Start time is in
# item.date. The wwid object has some methods for
# calculation and formatting, including
# wwid.item.end_date to convert the @done
# timestamp to an end date.
interval = wwid.get_interval(i, formatted: true) if wwid.i.end_date && opt[:times]
if interval
d, h, m = interval.split(/:/)
took = ' and it took'
took += " #{d.to_i} days" if d.to_i.positive?
took += " #{h.to_i} hours" if h.to_i.positive?
took += " #{m.to_i} minutes" if m.to_i.positive?
end
date = i.date.strftime('%A %B %e at %I:%M%p')
title = i.title.gsub(/@/, 'hashtag ')
tpl = template('say')
if wwid.config['export_templates'].key?('say')
cfg_tpl = wwid.config['export_templates']['say']
tpl = cfg_tpl unless cfg_tpl.nil? || cfg_tpl.empty?
end
output = tpl.dup
output.gsub!(/%date/, date)
output.gsub!(/%title/, title)
output.gsub!(/%section/, i.section)
output.gsub!(/%took/, took || '')
# Debugging output
# warn "Saying: #{output}"
# To provide results on the command line after the
# command runs, add to the wwid.results array. Results
# are provided on STDERR unless doing is run with
# `--stdout`
wwid.results.push([['Spoke the last entry. Did you hear it?', 0], 0])
# This export runs a command for fun, most plugins won't
`say -v #{wwid.config['say_voice']} "#{output}"`
# Return the result (don't output to terminal with puts or print)
output
end
Now you're ready to register the plugin.
# Register the plugin with doing.
# Doing::Plugins.register 'NAME', TYPE, Class
#
# Name should be lowercase, no spaces
#
# TYPE is :import or :export
#
# Class is the plugin class (e.g. Doing::SayExport), or
# self if called within the class
Doing::Plugins.register 'say', :export, self
Place the plugin in the plugins directory (defined in ~/.doingrc under 'plugin_path', default ~/.config/doing/plugins
). Now you can test it. If it's an export plugin, you should be able to use the name/trigger you registerd as an argument to doing show -o NAME
.
To enable verbose output and traces for debugging, run GLI_DEBUG=true doing show -o NAME
.