doing 1.0.93 → 2.0.6.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AUTHORS +19 -0
- data/CHANGELOG.md +616 -0
- data/COMMANDS.md +1181 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +110 -0
- data/LICENSE +23 -0
- data/README.md +15 -699
- data/Rakefile +79 -0
- data/_config.yml +1 -0
- data/bin/doing +1055 -494
- data/doing.gemspec +34 -0
- data/doing.rdoc +1839 -0
- data/example_plugin.rb +209 -0
- data/generate_completions.sh +5 -0
- data/img/doing-colors.jpg +0 -0
- data/img/doing-printf-wrap-800.jpg +0 -0
- data/img/doing-show-note-formatting-800.jpg +0 -0
- data/lib/completion/_doing.zsh +203 -0
- data/lib/completion/doing.bash +449 -0
- data/lib/completion/doing.fish +329 -0
- data/lib/doing/array.rb +8 -0
- data/lib/doing/cli_status.rb +70 -0
- data/lib/doing/colors.rb +136 -0
- data/lib/doing/configuration.rb +312 -0
- data/lib/doing/errors.rb +109 -0
- data/lib/doing/hash.rb +31 -0
- data/lib/doing/hooks.rb +59 -0
- data/lib/doing/item.rb +155 -0
- data/lib/doing/log_adapter.rb +344 -0
- data/lib/doing/markdown_document_listener.rb +174 -0
- data/lib/doing/note.rb +59 -0
- data/lib/doing/pager.rb +95 -0
- data/lib/doing/plugin_manager.rb +208 -0
- data/lib/doing/plugins/export/csv_export.rb +48 -0
- data/lib/doing/plugins/export/html_export.rb +83 -0
- data/lib/doing/plugins/export/json_export.rb +140 -0
- data/lib/doing/plugins/export/markdown_export.rb +85 -0
- data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
- data/lib/doing/plugins/export/template_export.rb +141 -0
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/plugins/import/calendar_import.rb +76 -0
- data/lib/doing/plugins/import/doing_import.rb +144 -0
- data/lib/doing/plugins/import/timing_import.rb +78 -0
- data/lib/doing/string.rb +348 -0
- data/lib/doing/symbol.rb +16 -0
- data/lib/doing/time.rb +18 -0
- data/lib/doing/util.rb +186 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +1868 -2349
- data/lib/doing/wwidfile.rb +117 -0
- data/lib/doing.rb +43 -3
- data/lib/examples/commands/autotag.rb +63 -0
- data/lib/examples/commands/wiki.rb +81 -0
- data/lib/examples/plugins/hooks.rb +22 -0
- data/lib/examples/plugins/say_export.rb +202 -0
- data/lib/examples/plugins/templates/wiki.css +169 -0
- data/lib/examples/plugins/templates/wiki.haml +27 -0
- data/lib/examples/plugins/templates/wiki_index.haml +18 -0
- data/lib/examples/plugins/wiki_export.rb +87 -0
- data/lib/templates/doing-markdown.erb +5 -0
- data/man/doing.1 +964 -0
- data/man/doing.1.html +711 -0
- data/man/doing.1.ronn +600 -0
- data/package-lock.json +3 -0
- data/rdoc_to_mmd.rb +42 -0
- data/rdocfixer.rb +13 -0
- data/scripts/generate_bash_completions.rb +211 -0
- data/scripts/generate_fish_completions.rb +204 -0
- data/scripts/generate_zsh_completions.rb +168 -0
- metadata +82 -7
- data/lib/doing/helpers.rb +0 -191
- data/lib/doing/markdown_export.rb +0 -16
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class Items < Array
|
5
|
+
def from(path)
|
6
|
+
return [] unless path
|
7
|
+
path = File.expand_path(path)
|
8
|
+
|
9
|
+
if File.exist?(path) && File.file?(path) && File.stat(path).size.positive?
|
10
|
+
input = IO.read(File.expand_path(input))
|
11
|
+
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
12
|
+
end
|
13
|
+
|
14
|
+
section = 'Uncategorized'
|
15
|
+
lines = input.split(/[\n\r]/)
|
16
|
+
current = 0
|
17
|
+
|
18
|
+
lines.each do |line|
|
19
|
+
next if line =~ /^\s*$/
|
20
|
+
|
21
|
+
if line =~ /^(\S[\S ]+):\s*(@\S+\s*)*$/
|
22
|
+
section = Regexp.last_match(1)
|
23
|
+
@sections << { original: line, title: section }
|
24
|
+
current = 0
|
25
|
+
elsif line =~ /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
|
26
|
+
date = Regexp.last_match(1).strip
|
27
|
+
title = Regexp.last_match(2).strip
|
28
|
+
item = Item.new(date, title, section)
|
29
|
+
@items.push(item)
|
30
|
+
current += 1
|
31
|
+
elsif current.zero?
|
32
|
+
# if content[section][:items].length - 1 == current
|
33
|
+
@other_content_top.push(line)
|
34
|
+
elsif line =~ /^\S/
|
35
|
+
@other_content_bottom.push(line)
|
36
|
+
else
|
37
|
+
prev_item = @items[current - 1]
|
38
|
+
prev_item.note = Note.new unless prev_item.note
|
39
|
+
|
40
|
+
prev_item.note.add(line)
|
41
|
+
# end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
Hooks.trigger :post_read, self
|
45
|
+
end
|
46
|
+
|
47
|
+
def section_titles
|
48
|
+
@sections.map { |s| s[:title] }
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
## @brief Adds a section.
|
53
|
+
##
|
54
|
+
## @param title (String) The new section title
|
55
|
+
##
|
56
|
+
def add_section(title)
|
57
|
+
if section_titles.include?(title.cap_first)
|
58
|
+
Doing.logger.debug('Skipped': 'Section already exists')
|
59
|
+
return
|
60
|
+
end
|
61
|
+
|
62
|
+
@sections << { original: "#{title}:", title: title }
|
63
|
+
Doing.logger.info('Added section:', %("#{title.cap_first}"))
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
## @brief Attempt to match a string with an existing section
|
68
|
+
##
|
69
|
+
## @param frag (String) The user-provided string
|
70
|
+
## @param guessed (Boolean) already guessed and failed
|
71
|
+
##
|
72
|
+
def guess_section(frag, guessed: false, suggest: false)
|
73
|
+
return 'All' if frag =~ /^all$/i
|
74
|
+
frag ||= wwid.config['current_section']
|
75
|
+
|
76
|
+
@sections.each { |sect| return sect[:title].cap_first if frag.downcase == sect[:title].downcase }
|
77
|
+
|
78
|
+
section = false
|
79
|
+
re = frag.split('').join('.*?')
|
80
|
+
sections.each do |sect|
|
81
|
+
next unless sect =~ /#{re}/i
|
82
|
+
|
83
|
+
Doing.logger.debug('Section match:', %(Assuming "#{sect}" from "#{frag}"))
|
84
|
+
section = sect
|
85
|
+
break
|
86
|
+
end
|
87
|
+
|
88
|
+
return section if suggest
|
89
|
+
|
90
|
+
unless section || guessed
|
91
|
+
alt = WWID.guess_view(frag, guessed: true, suggest: true)
|
92
|
+
if alt
|
93
|
+
meant_view = WWID.yn("Did you mean `doing view #{alt}`?", default_response: 'n')
|
94
|
+
raise Errors::InvalidSection, "Run again with `doing view #{alt}`" if meant_view
|
95
|
+
end
|
96
|
+
|
97
|
+
res = WWID.yn("Section #{frag} not found, create it", default_response: 'n')
|
98
|
+
|
99
|
+
if res
|
100
|
+
add_section(frag.cap_first)
|
101
|
+
WWID.write(@doing_file)
|
102
|
+
return frag.cap_first
|
103
|
+
end
|
104
|
+
|
105
|
+
raise Errors::InvalidSection, "Unknown section: #{frag}"
|
106
|
+
end
|
107
|
+
section ? section.cap_first : guessed
|
108
|
+
end
|
109
|
+
|
110
|
+
def section_items(section)
|
111
|
+
section = guess_section(section)
|
112
|
+
return @items if section =~ /all/i
|
113
|
+
|
114
|
+
@items.filter { |i| i.section == section }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/doing.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'doing/version'
|
2
4
|
require 'time'
|
3
5
|
require 'date'
|
4
6
|
require 'yaml'
|
@@ -8,7 +10,45 @@ require 'tempfile'
|
|
8
10
|
require 'chronic'
|
9
11
|
require 'haml'
|
10
12
|
require 'json'
|
11
|
-
require '
|
12
|
-
require '
|
13
|
+
require 'logger'
|
14
|
+
require 'safe_yaml/load'
|
15
|
+
require 'doing/hash'
|
16
|
+
require 'doing/colors'
|
17
|
+
require 'doing/string'
|
18
|
+
require 'doing/time'
|
19
|
+
require 'doing/array'
|
20
|
+
require 'doing/symbol'
|
21
|
+
require 'doing/util'
|
22
|
+
require 'doing/configuration'
|
23
|
+
require 'doing/item'
|
24
|
+
require 'doing/note'
|
25
|
+
require 'doing/wwidfile'
|
13
26
|
require 'doing/wwid'
|
27
|
+
require 'doing/log_adapter'
|
28
|
+
require 'doing/errors'
|
29
|
+
require 'doing/hooks'
|
30
|
+
require 'doing/plugin_manager'
|
31
|
+
require 'doing/pager'
|
14
32
|
# require 'doing/markdown_document_listener'
|
33
|
+
|
34
|
+
# Main doing module
|
35
|
+
module Doing
|
36
|
+
class << self
|
37
|
+
#
|
38
|
+
# @brief Fetch the logger
|
39
|
+
#
|
40
|
+
# @return the LogAdapter instance.
|
41
|
+
#
|
42
|
+
def logger
|
43
|
+
@logger ||= LogAdapter.new((ENV['DOING_LOG_LEVEL'] || :info).to_sym)
|
44
|
+
end
|
45
|
+
|
46
|
+
def config
|
47
|
+
@config ||= Configuration.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def config_with(file, options = {})
|
51
|
+
@config = Configuration.new(file, options: options)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example command that calls an existing command (tag) with
|
4
|
+
# preset options
|
5
|
+
desc 'Autotag last entry or filtered entries'
|
6
|
+
command :autotag do |c|
|
7
|
+
# Preserve some switches and flags. Values will be passed
|
8
|
+
# to tag command.
|
9
|
+
c.desc 'Section'
|
10
|
+
c.arg_name 'SECTION_NAME'
|
11
|
+
c.flag %i[s section], default_value: 'All'
|
12
|
+
|
13
|
+
c.desc 'How many recent entries to autotag (0 for all)'
|
14
|
+
c.arg_name 'COUNT'
|
15
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
16
|
+
|
17
|
+
c.desc 'Don\'t ask permission to autotag all entries when count is 0'
|
18
|
+
c.switch %i[force], negatable: false, default_value: false
|
19
|
+
|
20
|
+
c.desc 'Autotag last entry (or entries) not marked @done'
|
21
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
22
|
+
|
23
|
+
c.desc 'Autotag the last X entries containing TAG.
|
24
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
25
|
+
c.arg_name 'TAG'
|
26
|
+
c.flag [:tag]
|
27
|
+
|
28
|
+
c.desc 'Autotag entries matching search filter,
|
29
|
+
surround with slashes for regex (e.g. "/query.*/"),
|
30
|
+
start with single quote for exact match ("\'query")'
|
31
|
+
c.arg_name 'QUERY'
|
32
|
+
c.flag [:search]
|
33
|
+
|
34
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
35
|
+
c.arg_name 'BOOLEAN'
|
36
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
37
|
+
|
38
|
+
c.desc 'Select item(s) to tag from a menu of matching entries'
|
39
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
40
|
+
|
41
|
+
c.action do |global, options, _args|
|
42
|
+
# Force some switches and flags. We're using the tag
|
43
|
+
# command with settings that would invoke autotagging.
|
44
|
+
|
45
|
+
# Force enable autotag
|
46
|
+
options[:a] = true
|
47
|
+
options[:autotag] = true
|
48
|
+
|
49
|
+
# No need for date values
|
50
|
+
options[:d] = false
|
51
|
+
options[:date] = false
|
52
|
+
|
53
|
+
# Don't remove any tags
|
54
|
+
options[:rename] = nil
|
55
|
+
options[:regex] = false
|
56
|
+
options[:r] = false
|
57
|
+
options[:remove] = false
|
58
|
+
|
59
|
+
cmd = commands[:tag]
|
60
|
+
action = cmd.send(:get_action, nil)
|
61
|
+
action.call(global, options, [])
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
desc 'Output a tag wiki'
|
2
|
+
command :wiki do |c|
|
3
|
+
c.desc 'Section to rotate'
|
4
|
+
c.arg_name 'SECTION_NAME'
|
5
|
+
c.flag %i[s section], default_value: 'All'
|
6
|
+
|
7
|
+
c.desc 'Tag filter, combine multiple tags with a comma, use with --bool'
|
8
|
+
c.arg_name 'TAG'
|
9
|
+
c.flag [:tag]
|
10
|
+
|
11
|
+
c.desc 'Tag boolean (AND,OR,NOT)'
|
12
|
+
c.arg_name 'BOOLEAN'
|
13
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
14
|
+
|
15
|
+
c.desc 'Include entries older than date'
|
16
|
+
c.arg_name 'DATE_STRING'
|
17
|
+
c.flag [:before]
|
18
|
+
|
19
|
+
c.desc 'Include entries newer than date'
|
20
|
+
c.arg_name 'DATE_STRING'
|
21
|
+
c.flag [:after]
|
22
|
+
|
23
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
24
|
+
c.arg_name 'QUERY'
|
25
|
+
c.flag [:search]
|
26
|
+
|
27
|
+
c.desc %(
|
28
|
+
Date range to include, or a single day to filter date on.
|
29
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
30
|
+
To specify a range, use "to" or "through": `doing show --from "monday to friday"`
|
31
|
+
)
|
32
|
+
c.arg_name 'DATE_OR_RANGE'
|
33
|
+
c.flag %i[f from]
|
34
|
+
|
35
|
+
c.desc 'Only show items with recorded time intervals'
|
36
|
+
c.switch [:only_timed], default_value: false, negatable: false
|
37
|
+
|
38
|
+
c.action do |global, options, args|
|
39
|
+
wwid = global[:wwid]
|
40
|
+
tags = wwid.tag_groups([], opt: options)
|
41
|
+
|
42
|
+
wiki = Doing::Plugins.plugins.dig(:export, 'wiki', :class)
|
43
|
+
|
44
|
+
tags.each do |tag, items|
|
45
|
+
export_options = { page_title: tag, is_single: false, options: options }
|
46
|
+
|
47
|
+
raise RuntimeError, 'Missing plugin "wiki"' unless wiki
|
48
|
+
|
49
|
+
out = wiki.render(wwid, items, variables: export_options)
|
50
|
+
|
51
|
+
if out
|
52
|
+
FileUtils.mkdir_p('doing_wiki')
|
53
|
+
File.open(File.join('doing_wiki', tag + '.html'), 'w') do |f|
|
54
|
+
f.puts out
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
template = if wwid.config['export_templates']['wiki_index'] && File.exist?(File.expand_path(wwid.config['export_templates']['wiki_index']))
|
60
|
+
IO.read(File.expand_path(wwid.config['export_templates']['wiki_index']))
|
61
|
+
else
|
62
|
+
wiki.template('wiki_index')
|
63
|
+
end
|
64
|
+
style = if wwid.config['export_templates']['wiki_css'] && File.exist?(File.expand_path(wwid.config['export_templates']['wiki_css']))
|
65
|
+
IO.read(File.expand_path(wwid.config['export_templates']['wiki_css']))
|
66
|
+
else
|
67
|
+
wiki.template('wiki_css')
|
68
|
+
end
|
69
|
+
tags_out = tags.map { |t| {url: "#{t}.html"} }
|
70
|
+
engine = Haml::Engine.new(template)
|
71
|
+
index_out = engine.render(Object.new,
|
72
|
+
{ :@tags => tags.each_with_object([]) { |(tag, items), arr| arr << { name: tag, count: items.count } }, :@page_title => "Tags wiki", :@style => style })
|
73
|
+
|
74
|
+
if index_out
|
75
|
+
File.open(File.join('doing_wiki', 'index.html'), 'w') do |f|
|
76
|
+
f.puts index_out
|
77
|
+
end
|
78
|
+
Doing.logger.warn("Wiki written to doing_wiki directory")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# Hooks.register :post_config do |wwid|
|
5
|
+
# wwid.config['twizzle'] = 'Fo shizzle'
|
6
|
+
# wwid.write_config(File.expand_path('~/Desktop/wwidconfig.yml'))
|
7
|
+
# end
|
8
|
+
|
9
|
+
# Hooks.register :post_read, priority: 10 do |wwid|
|
10
|
+
# Doing.logger.warn('Hook 1:', 'triggered priority 10')
|
11
|
+
# Doing.logger.warn('Hook 2:', wwid.config['twizzle'])
|
12
|
+
# end
|
13
|
+
|
14
|
+
# Hooks.register :post_read, priority: 100 do |wwid|
|
15
|
+
# Doing.logger.warn('Hook 2:', 'triggered priority 100')
|
16
|
+
# end
|
17
|
+
|
18
|
+
Hooks.register :post_write do |filename|
|
19
|
+
res = `/bin/bash /Users/ttscoff/scripts/after_doing.sh`.strip
|
20
|
+
Doing.logger.debug('Hooks:', res) unless res =~ /^\.\.\.$/
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# title: Export plugin example
|
4
|
+
# description: Speak the most recent entry (macOS)
|
5
|
+
# author: Brett Terpstra
|
6
|
+
# url: https://brettterpstra.com
|
7
|
+
|
8
|
+
# Example
|
9
|
+
#
|
10
|
+
# doing show -o sayit
|
11
|
+
#
|
12
|
+
# ## Configuration
|
13
|
+
#
|
14
|
+
# Change what the plugin says by generating a template with
|
15
|
+
# `doing template --type say`, saving it to a file, and
|
16
|
+
# putting the path to that file in `export_templates->say` in
|
17
|
+
# .doingrc.
|
18
|
+
#
|
19
|
+
# export_templates:
|
20
|
+
# say: /path/to/template.txt
|
21
|
+
#
|
22
|
+
# Use a different voice by adding a `say_voice` key to your
|
23
|
+
# .doingrc. Use `say -v ?` to see available voices.
|
24
|
+
#
|
25
|
+
# say_voice: Zarvox
|
26
|
+
|
27
|
+
module Doing
|
28
|
+
##
|
29
|
+
## @brief Plugin class
|
30
|
+
##
|
31
|
+
class SayExport
|
32
|
+
include Doing::Util
|
33
|
+
|
34
|
+
#-------------------------------------------------------
|
35
|
+
## Plugin Settings. A plugin must have a self.settings
|
36
|
+
## method that returns a hash with plugin settings.
|
37
|
+
##
|
38
|
+
## trigger: (required) Regular expression to match
|
39
|
+
## FORMAT when used with `--output FORMAT`. Registered
|
40
|
+
## name of plugin must be able to match the trigger, but
|
41
|
+
## alternatives can be included
|
42
|
+
##
|
43
|
+
## templates: (optional) Array of templates this plugin
|
44
|
+
## can export (plugin must have :template method)
|
45
|
+
##
|
46
|
+
## Each template is a hash containing:
|
47
|
+
## - name: display name for template
|
48
|
+
## - trigger: regular expression for
|
49
|
+
## `template --type FORMAT`
|
50
|
+
##
|
51
|
+
## If a template is included, a config key will
|
52
|
+
## automatically be added for the user to override
|
53
|
+
## The config key will be available at:
|
54
|
+
##
|
55
|
+
## wwid.config['export_templates'][PLUGIN_NAME]
|
56
|
+
##
|
57
|
+
## config: (optional) A Hash which will be
|
58
|
+
## added to the main configuration in the plugins section.
|
59
|
+
## Options defined here are included when config file is
|
60
|
+
## created or updated with `config --update`. Use this to
|
61
|
+
## add new configuration keys, not to override existing
|
62
|
+
## ones.
|
63
|
+
##
|
64
|
+
## The configuration keys will be available at:
|
65
|
+
##
|
66
|
+
## wwid.config['plugins'][PLUGIN_NAME][KEY]
|
67
|
+
##
|
68
|
+
## @brief Method to return plugin settings (required)
|
69
|
+
##
|
70
|
+
## @return Hash of settings for this plugin
|
71
|
+
##
|
72
|
+
def self.settings
|
73
|
+
{
|
74
|
+
trigger: 'say(?:it)?',
|
75
|
+
templates: [
|
76
|
+
{ name: 'say', trigger: 'say(?:it)?' }
|
77
|
+
],
|
78
|
+
config: {
|
79
|
+
'say_voice' => 'Fiona'
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
#-------------------------------------------------------
|
86
|
+
## Output a template. Only required if template(s) are
|
87
|
+
## included in settings. The method should return a
|
88
|
+
## string (not output it to the STDOUT).
|
89
|
+
##
|
90
|
+
## @brief Method to return template (optional)
|
91
|
+
##
|
92
|
+
## @param trigger The trigger passed to the
|
93
|
+
## template function. When this
|
94
|
+
## method defines multiple
|
95
|
+
## templates, the trigger can be
|
96
|
+
## used to determine which one is
|
97
|
+
## output.
|
98
|
+
##
|
99
|
+
## @return (String) template contents
|
100
|
+
##
|
101
|
+
def self.template(trigger)
|
102
|
+
return unless trigger =~ /^say(it)?$/
|
103
|
+
'On %date, you were %title, recorded in section %section%took'
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
##
|
108
|
+
## @brief Render data received from an output
|
109
|
+
## command
|
110
|
+
##
|
111
|
+
## @param wwid The wwid object with config
|
112
|
+
## and public methods
|
113
|
+
## @param items An array of items to be output
|
114
|
+
## { <Date>date, <String>title,
|
115
|
+
## <String>section, <Array>note }
|
116
|
+
## @param variables Additional variables including
|
117
|
+
## flags passed to command
|
118
|
+
## (variables[:options])
|
119
|
+
##
|
120
|
+
## @return (String) Rendered output
|
121
|
+
##
|
122
|
+
def self.render(wwid, items, variables: {})
|
123
|
+
return if items.nil? || items.empty?
|
124
|
+
|
125
|
+
# the :options key includes the flags passed to the
|
126
|
+
# command that called the plugin use `puts
|
127
|
+
# variables.inspect` to see properties and methods
|
128
|
+
# when run
|
129
|
+
opt = variables[:options]
|
130
|
+
|
131
|
+
# This plugin just grabs the last item in the `items`
|
132
|
+
# list (which could be the oldest or newest, depending
|
133
|
+
# on the sort order of the command that called the
|
134
|
+
# plugin). Most of the time you'll want to use :each
|
135
|
+
# or :map to generate output.
|
136
|
+
i = items[-1]
|
137
|
+
|
138
|
+
# Format the item. Items are a hash with 3 keys: date,
|
139
|
+
# title, and section (parent section) Start time is in
|
140
|
+
# item.date. The wwid object has some methods for
|
141
|
+
# calculation and formatting, including
|
142
|
+
# wwid.item.end_date to convert the @done
|
143
|
+
# timestamp to an end date.
|
144
|
+
if opt[:times]
|
145
|
+
interval = i.interval
|
146
|
+
|
147
|
+
if interval
|
148
|
+
took = '. You finished on '
|
149
|
+
finished_at = i.end_date
|
150
|
+
took += finished_at.strftime('%A %B %e at %I:%M%p')
|
151
|
+
|
152
|
+
d, h, m = wwid.format_time(interval)
|
153
|
+
took += ' and it took'
|
154
|
+
took += " #{d.to_i} days" if d.to_i.positive?
|
155
|
+
took += " #{h.to_i} hours" if h.to_i.positive?
|
156
|
+
took += " #{m.to_i} minutes" if m.to_i.positive?
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
date = i.date.strftime('%A %B %e at %I:%M%p')
|
161
|
+
title = i.title.gsub(/ @done\(.*?\)/, '').gsub(/@/, 'hashtag ')
|
162
|
+
tpl = template('say')
|
163
|
+
|
164
|
+
if wwid.config['export_templates'].key?('say')
|
165
|
+
cfg_tpl = wwid.config['export_templates']['say']
|
166
|
+
tpl = cfg_tpl unless cfg_tpl.nil? || cfg_tpl.empty?
|
167
|
+
end
|
168
|
+
output = tpl.dup
|
169
|
+
output.gsub!(/%date/, date)
|
170
|
+
output.gsub!(/%title/, title)
|
171
|
+
output.gsub!(/%section/, i.section)
|
172
|
+
output.gsub!(/%took/, took || '')
|
173
|
+
|
174
|
+
# Debugging output
|
175
|
+
# warn "Saying: #{output}"
|
176
|
+
|
177
|
+
# To provide results on the command line after the
|
178
|
+
# command runs, add to the wwid.results array. Results
|
179
|
+
# are provided on STDERR unless doing is run with
|
180
|
+
# `--stdout`
|
181
|
+
Doing.logger.info('Spoke the last entry. Did you hear it?')
|
182
|
+
|
183
|
+
# This export runs a command for fun, most plugins won't
|
184
|
+
voice = wwid.config['plugins']['say']['say_voice'] || 'Alex'
|
185
|
+
`say -v "#{voice}" "#{output}"`
|
186
|
+
|
187
|
+
# Return the result (don't output to terminal with puts or print)
|
188
|
+
output
|
189
|
+
end
|
190
|
+
|
191
|
+
# Register the plugin with doing.
|
192
|
+
# Doing::Plugins.register 'NAME', TYPE, Class
|
193
|
+
#
|
194
|
+
# Name should be lowercase, no spaces
|
195
|
+
#
|
196
|
+
# TYPE is :import or :export
|
197
|
+
#
|
198
|
+
# Class is the plugin class (e.g. Doing::SayExport), or
|
199
|
+
# self if called within the class
|
200
|
+
Doing::Plugins.register 'say', :export, self
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
body {
|
2
|
+
background: #fff;
|
3
|
+
color: #333;
|
4
|
+
font-family: Helvetica,arial,freesans,clean,sans-serif;
|
5
|
+
font-size: 21px;
|
6
|
+
line-height: 1.5;
|
7
|
+
text-align: justify;
|
8
|
+
}
|
9
|
+
|
10
|
+
@media only screen and (max-width: 900px) {
|
11
|
+
body {
|
12
|
+
font-size: calc(12px + 1vw);
|
13
|
+
}
|
14
|
+
|
15
|
+
.date,
|
16
|
+
.note {
|
17
|
+
font-size: calc(8px + 1vw)!important;
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
h1 {
|
22
|
+
margin-bottom: 1em;
|
23
|
+
margin-left: .1em;
|
24
|
+
position: relative;
|
25
|
+
text-align: left;
|
26
|
+
}
|
27
|
+
|
28
|
+
ul {
|
29
|
+
list-style-position: outside;
|
30
|
+
position: relative;
|
31
|
+
text-align: left;
|
32
|
+
padding-left: 0;
|
33
|
+
}
|
34
|
+
|
35
|
+
article > ul > li {
|
36
|
+
display: grid;
|
37
|
+
grid-template-columns: 14ch auto;
|
38
|
+
line-height: 1.2;
|
39
|
+
list-style-type: none;
|
40
|
+
padding-left: 10px;
|
41
|
+
position: relative;
|
42
|
+
word-break: break-word;
|
43
|
+
transition: background .2s ease-in-out;
|
44
|
+
}
|
45
|
+
|
46
|
+
article > ul > li:hover {
|
47
|
+
background: rgba(150,150,150,.05);
|
48
|
+
}
|
49
|
+
|
50
|
+
.date {
|
51
|
+
color: #7d9ca2;
|
52
|
+
font-size: 17px;
|
53
|
+
padding: 15px 1ch 0 0;
|
54
|
+
text-align: right;
|
55
|
+
white-space: nowrap;
|
56
|
+
transition: color .2s ease-in-out;
|
57
|
+
}
|
58
|
+
|
59
|
+
.entry {
|
60
|
+
border-left: solid 1px #ccc;
|
61
|
+
line-height: 1.2;
|
62
|
+
padding: 10px 10px 10px 3ch;
|
63
|
+
text-indent: -2ch;
|
64
|
+
}
|
65
|
+
|
66
|
+
.tag {
|
67
|
+
color: #999;
|
68
|
+
transition: color 1s ease-in;
|
69
|
+
}
|
70
|
+
|
71
|
+
.note {
|
72
|
+
color: #aaa;
|
73
|
+
display: block;
|
74
|
+
font-size: 17px;
|
75
|
+
line-height: 1.1;
|
76
|
+
padding: 1em 0 0 2ch;
|
77
|
+
position: relative;
|
78
|
+
transition: color .2s ease-in-out;
|
79
|
+
}
|
80
|
+
|
81
|
+
li:hover .note {
|
82
|
+
color: #777;
|
83
|
+
}
|
84
|
+
|
85
|
+
li:hover .tag {
|
86
|
+
color: rgb(182, 120, 125);
|
87
|
+
}
|
88
|
+
|
89
|
+
li:hover .date {
|
90
|
+
color: rgb(100, 169, 165);
|
91
|
+
}
|
92
|
+
|
93
|
+
.note li {
|
94
|
+
margin-bottom: .5em;
|
95
|
+
list-style: none;
|
96
|
+
position: relative;
|
97
|
+
}
|
98
|
+
|
99
|
+
.note li:before {
|
100
|
+
color: #ddd;
|
101
|
+
content: '\25BA';
|
102
|
+
font-size: 12px;
|
103
|
+
font-weight: 300;
|
104
|
+
left: -3ch;
|
105
|
+
position: absolute;
|
106
|
+
top: .25em;
|
107
|
+
}
|
108
|
+
|
109
|
+
.time {
|
110
|
+
background: #f9fced;
|
111
|
+
border-bottom: dashed 1px #ccc;
|
112
|
+
color: #729953;
|
113
|
+
font-size: 15px;
|
114
|
+
margin-right: 4px;
|
115
|
+
padding: 0 5px;
|
116
|
+
position: relative;
|
117
|
+
text-align: right;
|
118
|
+
}
|
119
|
+
|
120
|
+
table td {
|
121
|
+
border-bottom: solid 1px #ddd;
|
122
|
+
height: 24px;
|
123
|
+
}
|
124
|
+
|
125
|
+
caption {
|
126
|
+
border-bottom: solid 1px #aaa;
|
127
|
+
margin: 10px 0;
|
128
|
+
text-align: left;
|
129
|
+
}
|
130
|
+
|
131
|
+
table {
|
132
|
+
margin: 50px 0 0 211px;
|
133
|
+
width: 400px;
|
134
|
+
}
|
135
|
+
|
136
|
+
th {
|
137
|
+
padding-bottom: 10px;
|
138
|
+
}
|
139
|
+
|
140
|
+
th, td {
|
141
|
+
padding-right: 20px;
|
142
|
+
}
|
143
|
+
|
144
|
+
table {
|
145
|
+
margin: 50px 0 2em 16ch;
|
146
|
+
max-width: 400px;
|
147
|
+
}
|
148
|
+
|
149
|
+
.section {
|
150
|
+
border-left: solid 1px rgb(182, 120, 125);
|
151
|
+
border-radius: 25px;
|
152
|
+
border-right: solid 1px rgb(182, 120, 125);
|
153
|
+
color: rgb(182, 120, 125);
|
154
|
+
font-size: .8em;
|
155
|
+
line-height: 1 !important;
|
156
|
+
padding: 0 4px;
|
157
|
+
transition: background .4s ease-in, color .4s ease-in;
|
158
|
+
}
|
159
|
+
|
160
|
+
li:hover .section {
|
161
|
+
color: #fff;
|
162
|
+
background: rgb(182, 120, 125);
|
163
|
+
}
|
164
|
+
|
165
|
+
a:link {
|
166
|
+
background-color: rgba(203, 255, 251, .15);
|
167
|
+
color: #64a9a5;
|
168
|
+
text-decoration: none;
|
169
|
+
}
|