Skip to content
Brett Terpstra edited this page Mar 14, 2022 · 12 revisions

Hooks allow you to register commands and script to run when doing performs certain actions. You can register the following events:

  • post_config -- Runs after the main configuration is read, before any local .doingrc configurations
  • post_local_config -- Runs after local configurations are read and merged into the configuration object
  • post_read -- Runs after the contents of the doing file are parsed
  • pre_entry_add -- Runs when an entry is prepared but before it's added
  • post_entry_added -- Runs after an entry has been added to the index, allows modification
  • post_entry_updated -- Runs when an existing entry is modified
  • post_entry_removed -- Runs after an entry has been deleted
  • pre_export -- Runs when an export is ready but before output, allows modification
  • pre_write -- Runs prior to writing the index to a file, allows modification
  • post_write -- Runs after the contents of the doing file are changed. Does not run unless a modification is made

To register a hook, simply place a Ruby file in your plugins folder. This folder is located at ~/.config/doing/plugins by default, but you can specify another location in your config using the plugin_path key.

The ruby file should contain a call to register the hook. Pass a block to perform when the associated event is triggered. A single file can contain as many hooks as you like.

The register method takes an optional priority: argument containing an integer. If multiple hooks are registered, they will be executed in order of priority, lowest to highest.

Doing::Hooks.register :post_read, priority: 20 do { |wwid| ... }

The following runs a shell script called after_doing.sh whenever a change is made to the doing file (:post_write event):

# frozen_string_literal: true

Doing::Hooks.register :post_write do |filename|
  job1 = fork do
    exec "/bin/bash /Users/ttscoff/scripts/after_doing.sh &> /dev/null"
  end

  Process.detach(job1)
  Doing.logger.debug('Hooks:', res)
end

Note that when running external scripts from hooks, you should not do so using a method that will end the current process (such as exec). The preferred method of executign external scripts is to use fork and Process.detach as shown in the :post_write hook. This will execute the script in the background and immediately return control back to Doing (and thus free up your command line even if the external script is long-running).

Events that receive that same block arguments can be combined as array when registering. In a case like :post_entry_updated, which receives wwid, entry, old_entry and post_entry_added, which receives wwid, entry, you can include the extra block parameter and it will just be nil for the event that doesn't need it.

Doing::Hooks.register [:post_entry_updated, :post_entry_added] do |wwid, entry, old_entry|
  # Actions, can test for old_entry.nil?
end

That's all there is to it. As an example, I use the :post_write hook to execute a shell script which updates my iTerm status bar and my Touch Bar (BetterTouchTool widget) with my current task whenever the file changes. Any time I add or complete an entry, my status bars update.

:post_config

The post_config hook receives the Doing::WWID object. This contains the @config hash and has access to all of the WWID methods. Changes made to the object within the block will carry through to the current doing operation.

If you modify the configuration variable, the changes will be incorporated into the current operation but will not be saved. If you want to save the modified configuration, use wwid.write_config to save it to a file. This will overwrite your current configuration with whatever you've done to the wwid.configuration object, so be careful. You can provide a filename argument to wwid.write to write to a different file than the primary config, e.g. (wwid.write_config(File.expand_path('~/configuration.yml'))).

Doing::Hooks.register :post_config do |wwid|
  wwid.config['new config key'] = 'My new value'
end

:post_local_config

The post_local_config hook runs after local .doingrc files are merged into the configuration. Keys from these files are not written out to any configurations by default, so performing any destructive file write operations in this hook is discouraged.

Doing::Hooks.register :post_config do |wwid|
  wwid.config['new config key'] = 'My new value'
end

:post_read

The post_read hook receives the current Doing::WWID object. This includes the @content object. wwid.content acts like an Array of items. Each item has values for date(Date), title(String), section(String), and note(Note < Array). Additionally, there's a wwid.content.sections attribute containing an array of section titles.

The items found in wwid.content have a variety of methods for testing whether they match search strings or tag filters (see the #tags? and #search methods), as well as for adding tags and modifying them. Modifications made to items by your hook will be passed on and saved when the file writes out at the end of a command. Not all commands write to disk, though; commands like show and view never save changes.

Doing::Hooks.register :post_read do |wwid|
  Doing.logger.info('Hook:', wwid.content.keys)
end

:pre_entry_add

Called before every item is added, and receives the WWID object and the new entry, which can be modified before it's added.

Doing::Hooks.register :post_entry_add do |wwid, entry|
  if entry.tags?('hooked')
    entry.tag('addhook')
  end
end

Or make entry of a note optional when adding new items without one:

Hooks.register :pre_entry_add do |wwid, entry|
  if entry.note.empty?
    res = Doing::Prompt.yn('Want to add a note?', default_response: false)
    if res
      note = Doing::Prompt.enter_text('Enter note')
      entry.note.add(note) unless note.empty?
    end
  end
end

:post_entry_added

Called after adding a new entry, receives the WWID object and a copy of the added entry.

Hooks.register :post_entry_added do |wwid, entry|
  # Do something with the new entry, such as setting an environment variable 
  # that you can use in your prompt. Changes to the entry variable will not be 
  # saved.
end

:post_entry_updated

Called after an existing entry is modified, receives the WWID object, the updated entry (which can be modified), and a clone of the entry prior to the update

Hooks.register :post_entry_updated do |wwid, entry, old_entry|
  # entry (Item) can be modified and changes will be saved
end

:post_entry_removed

Called after removing an entry, receives the WWID object and a copy of the removed entry

Doing::Hooks.register :post_entry_removed { |wwid, removed| ... }

:pre_export

Called before any output operation, receives the WWID object, the output format, and the array of entries (which can be modified, but modifications will only affect the output, not the doing file content)

Doing::Hooks.register :pre_export { |wwid, output_format, items| ... }

:pre_write

The pre_write hook receives the WWID object and the filename that is going to be written. Modifications made to the WWID.content object at this point will be written out to filename (and thus permanently saved).

To see a diff of items being added and removed, use wwid.changes, which outputs a hash containing { added: [Items], removed: [Items] }.

Doing::Hooks.register :pre_write do |wwid, filename|
  return unless filename == Doing.setting('doing_file')
  
  wwid.content.each do |section, content|
    content[:items].each do |i|
      # If any item has @test AND @todo tags, add @hooktest
      if i.tags?(['test', 'todo'], :and)
        i.tag('hooktest')
      end
    end
  end
end

:post_write

The post_write hook receives the filename that was saved to.

If you want to see what changed, you can use WWID.new.get_diff(filename). Note that this does a line-by-line diff between the current Doing file and the most recent backup, so it can be slow and will introduce a couple of seconds wait after any command that updates the doing file. :get_diff returns an Items object (which is an array of Items, each with date, title, note, and section, plus its own ). The more efficient way to get a list of changes is to use wwid.changes in the :pre_write hook.

Doing::Hooks.register :post_write do |filename|
  job1 = fork do
    exec "/bin/bash /Users/ttscoff/scripts/after_doing.sh &> /dev/null"
  end

  Process.detach(job1)
  # This is what I use to update things like my
  # BetterTouchTool widgets and my iTerm status bar.
end