doing 1.0.92 → 2.0.5.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +596 -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 +15 -699
  9. data/Rakefile +79 -0
  10. data/_config.yml +1 -0
  11. data/bin/doing +1012 -486
  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 +312 -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 +347 -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 +1868 -2356
  52. data/lib/doing/wwidfile.rb +117 -0
  53. data/lib/doing.rb +44 -4
  54. data/lib/examples/commands/wiki.rb +81 -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 -7
  72. data/lib/doing/helpers.rb +0 -191
  73. data/lib/doing/markdown_export.rb +0 -16
@@ -0,0 +1,312 @@
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
+ 'timer_format' => 'text',
35
+
36
+ 'templates' => {
37
+ 'default' => {
38
+ 'date_format' => '%Y-%m-%d %H:%M',
39
+ 'template' => '%date | %title%note',
40
+ 'wrap_width' => 0,
41
+ 'order' => 'asc'
42
+ },
43
+ 'today' => {
44
+ 'date_format' => '%_I:%M%P',
45
+ 'template' => '%date: %title %interval%note',
46
+ 'wrap_width' => 0,
47
+ 'order' => 'asc'
48
+ },
49
+ 'last' => {
50
+ 'date_format' => '%-I:%M%P on %a',
51
+ 'template' => '%title (at %date)%odnote',
52
+ 'wrap_width' => 88
53
+ },
54
+ 'recent' => {
55
+ 'date_format' => '%_I:%M%P',
56
+ 'template' => '%shortdate: %title (%section)',
57
+ 'wrap_width' => 88,
58
+ 'count' => 10,
59
+ 'order' => 'asc'
60
+ }
61
+ },
62
+
63
+ 'export_templates' => {},
64
+
65
+ 'views' => {
66
+ 'done' => {
67
+ 'date_format' => '%_I:%M%P',
68
+ 'template' => '%date | %title%note',
69
+ 'wrap_width' => 0,
70
+ 'section' => 'All',
71
+ 'count' => 0,
72
+ 'order' => 'desc',
73
+ 'tags' => 'done complete cancelled',
74
+ 'tags_bool' => 'OR'
75
+ },
76
+ 'color' => {
77
+ 'date_format' => '%F %_I:%M%P',
78
+ 'template' => '%boldblack%date %boldgreen| %boldwhite%title%default%note',
79
+ 'wrap_width' => 0,
80
+ 'section' => 'Currently',
81
+ 'count' => 10,
82
+ 'order' => 'asc'
83
+ }
84
+ },
85
+ 'marker_tag' => 'flagged',
86
+ 'marker_color' => 'red',
87
+ 'default_tags' => [],
88
+ 'tag_sort' => 'name',
89
+ 'include_notes' => true
90
+ }
91
+
92
+ def initialize(file = nil, options: {})
93
+ if file
94
+ cf = File.expand_path(file)
95
+ # raise MissingConfigFile, "Config not found (#{cf})" unless File.exist?(cf)
96
+
97
+ @config_file = cf
98
+ end
99
+
100
+ @settings = configure(options)
101
+ end
102
+
103
+ def additional_configs
104
+ @additional_configs ||= find_local_config
105
+ end
106
+
107
+ def value_for_key(keypath = '')
108
+ cfg = @settings
109
+ unless keypath =~ /^[.*]?$/
110
+ paths = keypath.split(/[:.]/)
111
+ while paths.length.positive? && !cfg.nil?
112
+ path = paths.shift
113
+ new_cfg = nil
114
+ cfg.each do |key, val|
115
+ next unless key =~ /#{path.to_rx(2)}/
116
+
117
+ new_cfg = val
118
+ break
119
+ end
120
+
121
+ if new_cfg.nil?
122
+ Doing.logger.error("Key match not found: #{path}")
123
+ break
124
+ end
125
+
126
+ cfg = new_cfg
127
+ end
128
+ end
129
+
130
+ cfg
131
+ end
132
+
133
+ # It takes the input, fills in the defaults where values do not exist.
134
+ #
135
+ # user_config - a Hash or Configuration of overrides.
136
+ #
137
+ # Returns a Configuration filled with defaults.
138
+ def from(user_config)
139
+ Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
140
+ end
141
+
142
+ def config_file
143
+ @config_file ||= File.join(Util.user_home, '.doingrc')
144
+ end
145
+
146
+ def config_file=(file)
147
+ @config_file = file
148
+ end
149
+
150
+ ##
151
+ ## @brief Read user configuration and merge with defaults
152
+ ##
153
+ ## @param opt (Hash) Additional Options
154
+ ##
155
+ def configure(opt = {})
156
+ @ignore_local = opt[:ignore_local] if opt[:ignore_local]
157
+
158
+ config = read_config.dup
159
+
160
+ plugin_config = Util.deep_merge_hashes(DEFAULTS['plugins'], config['plugins'] || {})
161
+
162
+ load_plugins(plugin_config['plugin_path'])
163
+
164
+ Plugins.plugins.each do |_type, plugins|
165
+ plugins.each do |title, plugin|
166
+ plugin_config[title] = plugin[:config] if plugin[:config] && !plugin[:config].empty?
167
+ config['export_templates'][title] ||= nil if plugin[:templates] && !plugin[:templates].empty?
168
+ end
169
+ end
170
+
171
+ config = Util.deep_merge_hashes({
172
+ 'plugins' => plugin_config
173
+ }, config)
174
+
175
+ config = find_deprecations(config)
176
+
177
+ if !File.exist?(config_file) || opt[:rewrite]
178
+ Util.write_to_file(config_file, YAML.dump(config), backup: true)
179
+ Doing.logger.warn('Config:', "Config file written to #{config_file}")
180
+ end
181
+
182
+ Hooks.trigger :post_config, self
183
+
184
+ # config = local_config.deep_merge(config) unless @ignore_local
185
+ config = Util.deep_merge_hashes(config, local_config) unless @ignore_local
186
+
187
+ Hooks.trigger :post_local_config, self
188
+
189
+ config
190
+ end
191
+
192
+ private
193
+
194
+ def find_deprecations(config)
195
+ deprecated = false
196
+ if config.key?('editor')
197
+ deprecated = true
198
+ config['editors']['default'] ||= config['editor']
199
+ config.delete('editor')
200
+ Doing.logger.debug('Deprecated:', "config key 'editor' is now 'editors->default', please update your config.")
201
+ end
202
+
203
+ if config.key?('config_editor_app') && !config['editors']['config']
204
+ deprecated = true
205
+ config['editors']['config'] = config['config_editor_app']
206
+ config.delete('config_editor_app')
207
+ Doing.logger.debug('Deprecated:', "config key 'config_editor_app' is now 'editors->config', please update your config.")
208
+ end
209
+
210
+ if config.key?('editor_app') && !config['editors']['doing_file']
211
+ deprecated = true
212
+ config['editors']['doing_file'] = config['editor_app']
213
+ config.delete('editor_app')
214
+ Doing.logger.debug('Deprecated:', "config key 'editor_app' is now 'editors->doing_file', please update your config.")
215
+ end
216
+
217
+ Doing.logger.warn('Deprecated:', 'outdated keys found, please run `doing config --update`.') if deprecated
218
+ config
219
+ end
220
+
221
+ ##
222
+ ## @brief Read local configurations
223
+ ##
224
+ ## @return Hash of config options
225
+ ##
226
+ def local_config
227
+ return {} if @ignore_local
228
+
229
+ local_configs = read_local_configs || {}
230
+
231
+ if additional_configs&.count
232
+ file_list = additional_configs.map { |p| p.sub(/^#{Util.user_home}/, '~') }.join(', ')
233
+ Doing.logger.debug('Config:', "Local config files found: #{file_list}")
234
+ end
235
+
236
+ local_configs
237
+ end
238
+
239
+ def read_local_configs
240
+ local_configs = {}
241
+
242
+ begin
243
+ additional_configs.each do |cfg|
244
+ local_configs.deep_merge(Util.safe_load_file(cfg))
245
+ end
246
+ rescue StandardError
247
+ Doing.logger.error('Config:', 'Error reading local configuration(s)')
248
+ end
249
+
250
+ local_configs
251
+ end
252
+
253
+ ##
254
+ ## @brief Reads a configuration.
255
+ ##
256
+ def read_config
257
+ unless File.exist?(config_file)
258
+ Doing.logger.info('Config:', 'Config file doesn\'t exist, using default configuration' )
259
+ return {}.deep_merge(DEFAULTS)
260
+ end
261
+
262
+ begin
263
+ user_config = Util.safe_load_file(config_file)
264
+ if user_config.key?('html_template')
265
+ user_config['export_templates'] ||= {}
266
+ user_config['export_templates'].deep_merge(user_config.delete('html_template'))
267
+ end
268
+
269
+ user_config['include_notes'] = user_config.delete(':include_notes') if user_config.key?(':include_notes')
270
+
271
+ user_config.deep_merge(DEFAULTS)
272
+ rescue StandardError => e
273
+ Doing.logger.error('Config:', 'Error reading default configuration')
274
+ Doing.logger.error('Error:', e.message)
275
+ user_config = DEFAULTS
276
+ end
277
+
278
+ user_config
279
+ end
280
+
281
+ ##
282
+ ## @brief Finds a project-specific configuration file
283
+ ##
284
+ ## @return (String) A file path
285
+ ##
286
+ def find_local_config
287
+ dir = Dir.pwd
288
+
289
+ local_config_files = []
290
+
291
+ while dir != '/' && (dir =~ %r{[A-Z]:/}).nil?
292
+ local_config_files.push(File.join(dir, '.doingrc')) if File.exist? File.join(dir, '.doingrc')
293
+
294
+ dir = File.dirname(dir)
295
+ end
296
+
297
+ local_config_files.delete(config_file)
298
+
299
+ local_config_files
300
+ end
301
+
302
+ def load_plugins(add_dir = nil)
303
+ begin
304
+ FileUtils.mkdir_p(add_dir) if add_dir && !File.exist?(add_dir)
305
+ rescue
306
+ nil
307
+ end
308
+
309
+ Plugins.load_plugins(add_dir)
310
+ end
311
+ end
312
+ 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