doing 1.0.90 → 2.0.2.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +590 -0
  4. data/COMMANDS.md +1181 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +110 -0
  7. data/LICENSE +23 -0
  8. data/README.md +14 -697
  9. data/Rakefile +79 -0
  10. data/_config.yml +1 -0
  11. data/bin/doing +1037 -481
  12. data/doing.fish +278 -0
  13. data/doing.gemspec +34 -0
  14. data/doing.rdoc +1759 -0
  15. data/example_plugin.rb +209 -0
  16. data/generate_completions.sh +4 -0
  17. data/img/doing-colors.jpg +0 -0
  18. data/img/doing-printf-wrap-800.jpg +0 -0
  19. data/img/doing-show-note-formatting-800.jpg +0 -0
  20. data/lib/completion/_doing.zsh +151 -0
  21. data/lib/completion/doing.bash +416 -0
  22. data/lib/completion/doing.fish +278 -0
  23. data/lib/doing/array.rb +8 -0
  24. data/lib/doing/cli_status.rb +66 -0
  25. data/lib/doing/colors.rb +136 -0
  26. data/lib/doing/configuration.rb +310 -0
  27. data/lib/doing/errors.rb +102 -0
  28. data/lib/doing/hash.rb +31 -0
  29. data/lib/doing/hooks.rb +59 -0
  30. data/lib/doing/item.rb +155 -0
  31. data/lib/doing/log_adapter.rb +342 -0
  32. data/lib/doing/markdown_document_listener.rb +174 -0
  33. data/lib/doing/note.rb +59 -0
  34. data/lib/doing/pager.rb +95 -0
  35. data/lib/doing/plugin_manager.rb +208 -0
  36. data/lib/doing/plugins/export/csv_export.rb +48 -0
  37. data/lib/doing/plugins/export/html_export.rb +83 -0
  38. data/lib/doing/plugins/export/json_export.rb +140 -0
  39. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  40. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  41. data/lib/doing/plugins/export/template_export.rb +141 -0
  42. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  43. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  44. data/lib/doing/plugins/import/doing_import.rb +144 -0
  45. data/lib/doing/plugins/import/timing_import.rb +78 -0
  46. data/lib/doing/string.rb +346 -0
  47. data/lib/doing/symbol.rb +16 -0
  48. data/lib/doing/time.rb +18 -0
  49. data/lib/doing/util.rb +186 -0
  50. data/lib/doing/version.rb +1 -1
  51. data/lib/doing/wwid.rb +1838 -2266
  52. data/lib/doing/wwidfile.rb +117 -0
  53. data/lib/doing.rb +43 -2
  54. data/lib/examples/commands/wiki.rb +80 -0
  55. data/lib/examples/plugins/hooks.rb +22 -0
  56. data/lib/examples/plugins/say_export.rb +202 -0
  57. data/lib/examples/plugins/templates/wiki.css +169 -0
  58. data/lib/examples/plugins/templates/wiki.haml +27 -0
  59. data/lib/examples/plugins/templates/wiki_index.haml +18 -0
  60. data/lib/examples/plugins/wiki_export.rb +87 -0
  61. data/lib/templates/doing-markdown.erb +5 -0
  62. data/man/doing.1 +964 -0
  63. data/man/doing.1.html +711 -0
  64. data/man/doing.1.ronn +600 -0
  65. data/package-lock.json +3 -0
  66. data/rdoc_to_mmd.rb +42 -0
  67. data/rdocfixer.rb +13 -0
  68. data/scripts/generate_bash_completions.rb +210 -0
  69. data/scripts/generate_fish_completions.rb +201 -0
  70. data/scripts/generate_zsh_completions.rb +164 -0
  71. metadata +82 -6
  72. data/lib/doing/helpers.rb +0 -121
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## @brief Configuration object
6
+ ##
7
+ class Configuration
8
+ attr_reader :settings
9
+
10
+ attr_writer :ignore_local
11
+
12
+ MissingConfigFile = Class.new(RuntimeError)
13
+
14
+ DEFAULTS = {
15
+ 'autotag' => {
16
+ 'whitelist' => [],
17
+ 'synonyms' => {}
18
+ },
19
+ 'editors' => {
20
+ 'default' => ENV['DOING_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR'],
21
+ 'doing_file' => nil,
22
+ 'config' => nil
23
+ },
24
+ 'plugins' => {
25
+ 'plugin_path' => File.join(Util.user_home, '.config', 'doing', 'plugins'),
26
+ 'command_path' => File.join(Util.user_home, '.config', 'doing', 'commands')
27
+ },
28
+ 'doing_file' => '~/what_was_i_doing.md',
29
+ 'current_section' => 'Currently',
30
+ 'paginate' => false,
31
+ 'never_time' => [],
32
+ 'never_finish' => [],
33
+
34
+ 'templates' => {
35
+ 'default' => {
36
+ 'date_format' => '%Y-%m-%d %H:%M',
37
+ 'template' => '%date | %title%note',
38
+ 'wrap_width' => 0,
39
+ 'order' => 'asc'
40
+ },
41
+ 'today' => {
42
+ 'date_format' => '%_I:%M%P',
43
+ 'template' => '%date: %title %interval%note',
44
+ 'wrap_width' => 0,
45
+ 'order' => 'asc'
46
+ },
47
+ 'last' => {
48
+ 'date_format' => '%-I:%M%P on %a',
49
+ 'template' => '%title (at %date)%odnote',
50
+ 'wrap_width' => 88
51
+ },
52
+ 'recent' => {
53
+ 'date_format' => '%_I:%M%P',
54
+ 'template' => '%shortdate: %title (%section)',
55
+ 'wrap_width' => 88,
56
+ 'count' => 10,
57
+ 'order' => 'asc'
58
+ }
59
+ },
60
+
61
+ 'export_templates' => {},
62
+
63
+ 'views' => {
64
+ 'done' => {
65
+ 'date_format' => '%_I:%M%P',
66
+ 'template' => '%date | %title%note',
67
+ 'wrap_width' => 0,
68
+ 'section' => 'All',
69
+ 'count' => 0,
70
+ 'order' => 'desc',
71
+ 'tags' => 'done complete cancelled',
72
+ 'tags_bool' => 'OR'
73
+ },
74
+ 'color' => {
75
+ 'date_format' => '%F %_I:%M%P',
76
+ 'template' => '%boldblack%date %boldgreen| %boldwhite%title%default%note',
77
+ 'wrap_width' => 0,
78
+ 'section' => 'Currently',
79
+ 'count' => 10,
80
+ 'order' => 'asc'
81
+ }
82
+ },
83
+ 'marker_tag' => 'flagged',
84
+ 'marker_color' => 'red',
85
+ 'default_tags' => [],
86
+ 'tag_sort' => 'name',
87
+ 'include_notes' => true
88
+ }
89
+
90
+ def initialize(file = nil, options: {})
91
+ if file
92
+ cf = File.expand_path(file)
93
+ # raise MissingConfigFile, "Config not found (#{cf})" unless File.exist?(cf)
94
+
95
+ @config_file = cf
96
+ end
97
+
98
+ @settings = configure(options)
99
+ end
100
+
101
+ def additional_configs
102
+ @additional_configs ||= find_local_config
103
+ end
104
+
105
+ def value_for_key(keypath = '')
106
+ cfg = @settings
107
+ unless keypath =~ /^[.*]?$/
108
+ paths = keypath.split(/[:.]/)
109
+ while paths.length.positive? && !cfg.nil?
110
+ path = paths.shift
111
+ new_cfg = nil
112
+ cfg.each do |key, val|
113
+ next unless key =~ /#{path.to_rx(2)}/
114
+
115
+ new_cfg = val
116
+ break
117
+ end
118
+
119
+ if new_cfg.nil?
120
+ Doing.logger.error("Key match not found: #{path}")
121
+ break
122
+ end
123
+
124
+ cfg = new_cfg
125
+ end
126
+ end
127
+
128
+ cfg
129
+ end
130
+
131
+ # It takes the input, fills in the defaults where values do not exist.
132
+ #
133
+ # user_config - a Hash or Configuration of overrides.
134
+ #
135
+ # Returns a Configuration filled with defaults.
136
+ def from(user_config)
137
+ Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
138
+ end
139
+
140
+ def config_file
141
+ @config_file ||= File.join(Util.user_home, '.doingrc')
142
+ end
143
+
144
+ def config_file=(file)
145
+ @config_file = file
146
+ end
147
+
148
+ ##
149
+ ## @brief Read user configuration and merge with defaults
150
+ ##
151
+ ## @param opt (Hash) Additional Options
152
+ ##
153
+ def configure(opt = {})
154
+ @ignore_local = opt[:ignore_local] if opt[:ignore_local]
155
+
156
+ config = read_config.dup
157
+
158
+ plugin_config = Util.deep_merge_hashes(DEFAULTS['plugins'], config['plugins'] || {})
159
+
160
+ load_plugins(plugin_config['plugin_path'])
161
+
162
+ Plugins.plugins.each do |_type, plugins|
163
+ plugins.each do |title, plugin|
164
+ plugin_config[title] = plugin[:config] if plugin[:config] && !plugin[:config].empty?
165
+ config['export_templates'][title] ||= nil if plugin[:templates] && !plugin[:templates].empty?
166
+ end
167
+ end
168
+
169
+ config = Util.deep_merge_hashes({
170
+ 'plugins' => plugin_config
171
+ }, config)
172
+
173
+ config = find_deprecations(config)
174
+
175
+ if !File.exist?(config_file) || opt[:rewrite]
176
+ Util.write_to_file(config_file, YAML.dump(config), backup: true)
177
+ Doing.logger.warn('Config:', "Config file written to #{config_file}")
178
+ end
179
+
180
+ Hooks.trigger :post_config, self
181
+
182
+ # config = local_config.deep_merge(config) unless @ignore_local
183
+ config = Util.deep_merge_hashes(config, local_config) unless @ignore_local
184
+
185
+ Hooks.trigger :post_local_config, self
186
+
187
+ config
188
+ end
189
+
190
+ private
191
+
192
+ def find_deprecations(config)
193
+ deprecated = false
194
+ if config.key?('editor')
195
+ deprecated = true
196
+ config['editors']['default'] ||= config['editor']
197
+ config.delete('editor')
198
+ Doing.logger.debug('Deprecated:', "config key 'editor' is now 'editors->default', please update your config.")
199
+ end
200
+
201
+ if config.key?('config_editor_app') && !config['editors']['config']
202
+ deprecated = true
203
+ config['editors']['config'] = config['config_editor_app']
204
+ config.delete('config_editor_app')
205
+ Doing.logger.debug('Deprecated:', "config key 'config_editor_app' is now 'editors->config', please update your config.")
206
+ end
207
+
208
+ if config.key?('editor_app') && !config['editors']['doing_file']
209
+ deprecated = true
210
+ config['editors']['doing_file'] = config['editor_app']
211
+ config.delete('editor_app')
212
+ Doing.logger.debug('Deprecated:', "config key 'editor_app' is now 'editors->doing_file', please update your config.")
213
+ end
214
+
215
+ Doing.logger.warn('Deprecated:', 'outdated keys found, please run `doing config --update`.') if deprecated
216
+ config
217
+ end
218
+
219
+ ##
220
+ ## @brief Read local configurations
221
+ ##
222
+ ## @return Hash of config options
223
+ ##
224
+ def local_config
225
+ return {} if @ignore_local
226
+
227
+ local_configs = read_local_configs || {}
228
+
229
+ if additional_configs&.count
230
+ file_list = additional_configs.map { |p| p.sub(/^#{Util.user_home}/, '~') }.join(', ')
231
+ Doing.logger.debug('Config:', "Local config files found: #{file_list}")
232
+ end
233
+
234
+ local_configs
235
+ end
236
+
237
+ def read_local_configs
238
+ local_configs = {}
239
+
240
+ begin
241
+ additional_configs.each do |cfg|
242
+ local_configs.deep_merge(Util.safe_load_file(cfg))
243
+ end
244
+ rescue StandardError
245
+ Doing.logger.error('Config:', 'Error reading local configuration(s)')
246
+ end
247
+
248
+ local_configs
249
+ end
250
+
251
+ ##
252
+ ## @brief Reads a configuration.
253
+ ##
254
+ def read_config
255
+ unless File.exist?(config_file)
256
+ Doing.logger.info('Config:', 'Config file doesn\'t exist, using default configuration' )
257
+ return {}.deep_merge(DEFAULTS)
258
+ end
259
+
260
+ begin
261
+ user_config = Util.safe_load_file(config_file)
262
+ if user_config.key?('html_template')
263
+ user_config['export_templates'] ||= {}
264
+ user_config['export_templates'].deep_merge(user_config.delete('html_template'))
265
+ end
266
+
267
+ user_config['include_notes'] = user_config.delete(':include_notes') if user_config.key?(':include_notes')
268
+
269
+ user_config.deep_merge(DEFAULTS)
270
+ rescue StandardError => e
271
+ Doing.logger.error('Config:', 'Error reading default configuration')
272
+ Doing.logger.error('Error:', e.message)
273
+ user_config = DEFAULTS
274
+ end
275
+
276
+ user_config
277
+ end
278
+
279
+ ##
280
+ ## @brief Finds a project-specific configuration file
281
+ ##
282
+ ## @return (String) A file path
283
+ ##
284
+ def find_local_config
285
+ dir = Dir.pwd
286
+
287
+ local_config_files = []
288
+
289
+ while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
290
+ local_config_files.push(File.join(dir, '.doingrc')) if File.exist? File.join(dir, '.doingrc')
291
+
292
+ dir = File.dirname(dir)
293
+ end
294
+
295
+ local_config_files.delete(config_file)
296
+
297
+ local_config_files
298
+ end
299
+
300
+ def load_plugins(add_dir = nil)
301
+ begin
302
+ FileUtils.mkdir_p(add_dir) if add_dir && !File.exist?(add_dir)
303
+ rescue
304
+ nil
305
+ end
306
+
307
+ Plugins.load_plugins(add_dir)
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ module Errors
5
+
6
+ class UserCancelled < ::StandardError
7
+ def initialize(msg='Cancelled')
8
+ Doing.logger.output_results
9
+ Doing.logger.log_now(:warn, 'Exited:', msg)
10
+ Process.exit 1
11
+ end
12
+ end
13
+
14
+ class EmptyInput < ::StandardError
15
+ def initialize(msg='No input')
16
+ Doing.logger.output_results
17
+ Doing.logger.log_now(:warn, 'Exited:', 'Input empty')
18
+ Process.exit 1
19
+ end
20
+ end
21
+
22
+ class DoingStandardError < ::StandardError
23
+ def initialize(msg='')
24
+ Doing.logger.output_results
25
+
26
+ super
27
+ end
28
+ end
29
+
30
+ class DoingRuntimeError < ::RuntimeError
31
+ def initialize(msg='')
32
+ Doing.logger.output_results
33
+
34
+ super
35
+ end
36
+ end
37
+
38
+ class NoResults < ::StandardError
39
+ def initialize(msg='No results')
40
+ Doing.logger.output_results
41
+ Process.exit 0
42
+
43
+ end
44
+ end
45
+
46
+ class DoingNoTraceError < ::StandardError
47
+ def initialize(msg = nil, level = nil)
48
+ level ||= :error
49
+ Doing.logger.output_results
50
+ if msg
51
+ Doing.logger.log_now(level, msg)
52
+ end
53
+
54
+ Process.exit 1
55
+ end
56
+ end
57
+
58
+ class PluginException < ::StandardError
59
+ attr_reader :plugin
60
+
61
+ def initialize(msg = 'Plugin error', type: nil, plugin: nil)
62
+ @plugin = plugin || 'Unknown Plugin'
63
+
64
+ type ||= 'Unknown'
65
+ @type = case type.to_s
66
+ when /^i/
67
+ 'Import plugin'
68
+ when /^e/
69
+ 'Export plugin'
70
+ else
71
+ type.to_s
72
+ end
73
+
74
+ msg = "(#{@type}: #{@plugin}) #{msg}"
75
+
76
+ Doing.logger.error('Plugin Error:', msg)
77
+ Doing.logger.output_results
78
+ Process.exit 1
79
+ end
80
+ end
81
+
82
+ HookUnavailable = Class.new(PluginException)
83
+ InvalidPluginType = Class.new(PluginException)
84
+ PluginUncallable = Class.new(PluginException)
85
+
86
+ InvalidArgument = Class.new(DoingRuntimeError)
87
+ MissingArgument = Class.new(DoingRuntimeError)
88
+ MissingFile = Class.new(DoingRuntimeError)
89
+ MissingEditor = Class.new(DoingRuntimeError)
90
+ NonInteractive = Class.new(StandardError)
91
+
92
+ NoEntryError = Class.new(DoingRuntimeError)
93
+
94
+ InvalidTimeExpression = Class.new(DoingRuntimeError)
95
+ InvalidSection = Class.new(DoingRuntimeError)
96
+ InvalidView = Class.new(DoingRuntimeError)
97
+
98
+ ItemNotFound = Class.new(DoingRuntimeError)
99
+ # FatalException = Class.new(::RuntimeError)
100
+ # InvalidPluginName = Class.new(FatalException)
101
+ end
102
+ end
data/lib/doing/hash.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Hash helpers
5
+ class ::Hash
6
+ ##
7
+ ## @brief Freeze all values in a hash
8
+ ##
9
+ ## @return { description_of_the_return_value }
10
+ ##
11
+ def deep_freeze
12
+ map { |k, v| v.is_a?(Hash) ? v.deep_freeze : v.freeze }.freeze
13
+ end
14
+
15
+ def deep_freeze!
16
+ replace deep_freeze
17
+ end
18
+
19
+ # Turn all keys into string
20
+ #
21
+ # Return a copy of the hash where all its keys are strings
22
+ def stringify_keys
23
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v.is_a?(Hash) ? v.stringify_keys : v }
24
+ end
25
+
26
+ # Turn all keys into symbols
27
+ def symbolize_keys
28
+ each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Hook manager
5
+ module Hooks
6
+ DEFAULT_PRIORITY = 20
7
+
8
+ @registry = {
9
+ post_config: [],
10
+ post_local_config: [],
11
+ post_read: [],
12
+ pre_write: [],
13
+ post_write: []
14
+ }
15
+
16
+ # map of all hooks and their priorities
17
+ @hook_priority = {}
18
+
19
+ # register hook(s) to be called later, public API
20
+ def self.register(event, priority: DEFAULT_PRIORITY, &block)
21
+ register_one(event, priority_value(priority), &block)
22
+ end
23
+
24
+ # Ensure the priority is a Fixnum
25
+ def self.priority_value(priority)
26
+ return priority if priority.is_a?(Integer)
27
+
28
+ PRIORITY_MAP[priority] || DEFAULT_PRIORITY
29
+ end
30
+
31
+ # register a single hook to be called later, internal API
32
+ def self.register_one(event, priority, &block)
33
+ unless @registry[event]
34
+ raise Doing::Errors::HookUnavailable, "Invalid hook. Doing only supports #{@registry.keys.inspect}"
35
+ end
36
+
37
+ raise Doing::Errors::PluginUncallable, 'Hooks must respond to :call' unless block.respond_to? :call
38
+
39
+ Doing.logger.debug('Hook Manager:', "Registered #{event} hook") if ENV['DOING_PLUGIN_DEBUG']
40
+
41
+ insert_hook event, priority, &block
42
+ end
43
+
44
+ def self.insert_hook(event, priority, &block)
45
+ @hook_priority[block] = [-priority, @hook_priority.size]
46
+ @registry[event] << block
47
+ end
48
+
49
+ def self.trigger(event, *args)
50
+ hooks = @registry[event]
51
+ return if hooks.nil? || hooks.empty?
52
+
53
+ # sort and call hooks according to priority and load order
54
+ hooks.sort_by { |h| @hook_priority[h] }.each do |hook|
55
+ hook.call(*args)
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/doing/item.rb ADDED
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## @brief This class describes a single WWID item
6
+ ##
7
+ class Item
8
+ attr_accessor :date, :title, :section, :note
9
+
10
+ def initialize(date, title, section, note = nil)
11
+ @date = date.is_a?(Time) ? date : Time.parse(date)
12
+ @title = title
13
+ @section = section
14
+ @note = Note.new(note)
15
+ end
16
+
17
+ # def date=(new_date)
18
+ # @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
19
+ # end
20
+
21
+ def interval
22
+ @interval ||= calc_interval
23
+ end
24
+
25
+ def end_date
26
+ @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
27
+ end
28
+
29
+ def equal?(other)
30
+ return false if @title.strip != other.title.strip
31
+
32
+ return false if @date != other.date
33
+
34
+ return false unless @note.equal?(other.note)
35
+
36
+ true
37
+ end
38
+
39
+ def same_time?(item_b)
40
+ date == item_b.date ? interval == item_b.interval : false
41
+ end
42
+
43
+ def overlapping_time?(item_b)
44
+ return true if same_time?(item_b)
45
+
46
+ start_a = date
47
+ interval = interval
48
+ end_a = interval ? start_a + interval.to_i : start_a
49
+ start_b = item_b.date
50
+ interval = item_b.interval
51
+ end_b = interval ? start_b + interval.to_i : start_b
52
+ (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
53
+ end
54
+
55
+ def tag(tag, value: nil, remove: false, rename_to: nil, regex: false)
56
+ @title.tag!(tag, value: value, remove: remove, rename_to: rename_to, regex: regex).strip!
57
+ end
58
+
59
+ def tags
60
+ @title.scan(/(?<= |\A)@([^\s(]+)/).map {|tag| tag[0]}.sort.uniq
61
+ end
62
+
63
+ def tags?(tags, bool = :and)
64
+ tags = split_tags(tags)
65
+ bool = bool.normalize_bool
66
+
67
+ case bool
68
+ when :and
69
+ all_tags?(tags)
70
+ when :not
71
+ no_tags?(tags)
72
+ else
73
+ any_tags?(tags)
74
+ end
75
+ end
76
+
77
+ def search(search)
78
+ text = @title + @note.to_s
79
+ pattern = case search.strip
80
+ when %r{^/.*?/$}
81
+ search.sub(%r{/(.*?)/}, '\1')
82
+ when /^'/
83
+ case_sensitive = true
84
+ search.sub(/^'(.*?)'?$/, '\1')
85
+ else
86
+ case_sensitive = true if search =~ /[A-Z]/
87
+ search.split('').join('.{0,3}')
88
+ end
89
+ rx = Regexp.new(pattern, !case_sensitive)
90
+
91
+ text =~ rx
92
+ end
93
+
94
+ def should_finish?
95
+ should?('never_finish')
96
+ end
97
+
98
+ def should_time?
99
+ should?('never_time')
100
+ end
101
+
102
+ private
103
+
104
+ def should?(key)
105
+ config = Doing.config.settings
106
+ return true unless config[key].is_a?(Array)
107
+
108
+ config[key].each do |tag|
109
+ if tag =~ /^@/
110
+ return false if tags?(tag.sub(/^@/, '').downcase)
111
+ elsif section.downcase == tag.downcase
112
+ return false
113
+ end
114
+ end
115
+
116
+ true
117
+ end
118
+
119
+ def calc_interval
120
+ done = end_date
121
+ return nil if done.nil?
122
+
123
+ start = @date
124
+
125
+ t = (done - start).to_i
126
+ t > 0 ? t : nil
127
+ end
128
+
129
+ def all_tags?(tags)
130
+ tags.each do |tag|
131
+ return false unless @title =~ /@#{tag}/
132
+ end
133
+ true
134
+ end
135
+
136
+ def no_tags?(tags)
137
+ tags.each do |tag|
138
+ return false if @title =~ /@#{tag}/
139
+ end
140
+ true
141
+ end
142
+
143
+ def any_tags?(tags)
144
+ tags.each do |tag|
145
+ return true if @title =~ /@#{tag}/
146
+ end
147
+ false
148
+ end
149
+
150
+ def split_tags(tags)
151
+ tags = tags.split(/ *, */) if tags.is_a? String
152
+ tags.map { |t| t.strip.sub(/^@/, '') }
153
+ end
154
+ end
155
+ end