Skip to content
Brett Terpstra edited this page Oct 22, 2021 · 19 revisions

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

The Required methods

self.settings

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

self.template

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

self.render

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

Register the plugin with Doing

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.

Debugging

To enable verbose output and traces for debugging, run GLI_DEBUG=true doing show -o NAME.