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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +616 -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 +1055 -494
  12. data/doing.gemspec +34 -0
  13. data/doing.rdoc +1839 -0
  14. data/example_plugin.rb +209 -0
  15. data/generate_completions.sh +5 -0
  16. data/img/doing-colors.jpg +0 -0
  17. data/img/doing-printf-wrap-800.jpg +0 -0
  18. data/img/doing-show-note-formatting-800.jpg +0 -0
  19. data/lib/completion/_doing.zsh +203 -0
  20. data/lib/completion/doing.bash +449 -0
  21. data/lib/completion/doing.fish +329 -0
  22. data/lib/doing/array.rb +8 -0
  23. data/lib/doing/cli_status.rb +70 -0
  24. data/lib/doing/colors.rb +136 -0
  25. data/lib/doing/configuration.rb +312 -0
  26. data/lib/doing/errors.rb +109 -0
  27. data/lib/doing/hash.rb +31 -0
  28. data/lib/doing/hooks.rb +59 -0
  29. data/lib/doing/item.rb +155 -0
  30. data/lib/doing/log_adapter.rb +344 -0
  31. data/lib/doing/markdown_document_listener.rb +174 -0
  32. data/lib/doing/note.rb +59 -0
  33. data/lib/doing/pager.rb +95 -0
  34. data/lib/doing/plugin_manager.rb +208 -0
  35. data/lib/doing/plugins/export/csv_export.rb +48 -0
  36. data/lib/doing/plugins/export/html_export.rb +83 -0
  37. data/lib/doing/plugins/export/json_export.rb +140 -0
  38. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  39. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  40. data/lib/doing/plugins/export/template_export.rb +141 -0
  41. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  42. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  43. data/lib/doing/plugins/import/doing_import.rb +144 -0
  44. data/lib/doing/plugins/import/timing_import.rb +78 -0
  45. data/lib/doing/string.rb +348 -0
  46. data/lib/doing/symbol.rb +16 -0
  47. data/lib/doing/time.rb +18 -0
  48. data/lib/doing/util.rb +186 -0
  49. data/lib/doing/version.rb +1 -1
  50. data/lib/doing/wwid.rb +1868 -2349
  51. data/lib/doing/wwidfile.rb +117 -0
  52. data/lib/doing.rb +43 -3
  53. data/lib/examples/commands/autotag.rb +63 -0
  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 +211 -0
  69. data/scripts/generate_fish_completions.rb +204 -0
  70. data/scripts/generate_zsh_completions.rb +168 -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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ module Errors
5
+ class UserCancelled < ::StandardError
6
+ def initialize(msg = 'Cancelled', topic = 'Exited:')
7
+ Doing.logger.output_results
8
+ Doing.logger.log_now(:warn, topic, msg)
9
+ Process.exit 1
10
+ end
11
+ end
12
+
13
+ class EmptyInput < ::StandardError
14
+ def initialize(msg = 'No input', topic = 'Exited:')
15
+ Doing.logger.output_results
16
+ Doing.logger.log_now(:warn, topic, msg)
17
+ Process.exit 1
18
+ end
19
+ end
20
+
21
+ class DoingStandardError < ::StandardError
22
+ def initialize(msg = '')
23
+ Doing.logger.output_results
24
+
25
+ super
26
+ end
27
+ end
28
+
29
+ class WrongCommand < ::StandardError
30
+ def initialize(msg = 'wrong command', topic = 'Error:')
31
+ Doing.logger.warn(topic, msg)
32
+
33
+ super(msg)
34
+ end
35
+ end
36
+
37
+ class DoingRuntimeError < ::RuntimeError
38
+ def initialize(msg = 'Runtime Error', topic: 'Error:')
39
+ Doing.logger.output_results
40
+ Doing.logger.log_now(:error, topic, msg)
41
+ Process.exit 1
42
+ end
43
+ end
44
+
45
+ class NoResults < ::StandardError
46
+ def initialize(msg = 'No results', topic = 'Exited:')
47
+ Doing.logger.output_results
48
+ Doing.logger.log_now(:warn, topic, msg)
49
+ Process.exit 0
50
+
51
+ end
52
+ end
53
+
54
+ class DoingNoTraceError < ::StandardError
55
+ def initialize(msg = nil, level = nil, topic = nil)
56
+ level ||= :error
57
+ Doing.logger.output_results
58
+ if msg
59
+ Doing.logger.log_now(level, topic, msg)
60
+ end
61
+
62
+ Process.exit 1
63
+ end
64
+ end
65
+
66
+ class PluginException < ::StandardError
67
+ attr_reader :plugin
68
+
69
+ def initialize(msg = 'Plugin error', type = nil, plugin = nil)
70
+ @plugin = plugin || 'Unknown Plugin'
71
+
72
+ type ||= 'Unknown'
73
+ @type = case type.to_s
74
+ when /^i/
75
+ 'Import plugin'
76
+ when /^e/
77
+ 'Export plugin'
78
+ else
79
+ type.to_s
80
+ end
81
+
82
+ msg = "(#{@type}: #{@plugin}) #{msg}"
83
+
84
+ Doing.logger.log_now(:error, 'Plugin:', msg)
85
+ Process.exit 1
86
+ end
87
+ end
88
+
89
+ HookUnavailable = Class.new(PluginException)
90
+ InvalidPluginType = Class.new(PluginException)
91
+ PluginUncallable = Class.new(PluginException)
92
+
93
+ InvalidArgument = Class.new(DoingRuntimeError)
94
+ MissingArgument = Class.new(DoingRuntimeError)
95
+ MissingFile = Class.new(DoingRuntimeError)
96
+ MissingEditor = Class.new(DoingRuntimeError)
97
+ NonInteractive = Class.new(StandardError)
98
+
99
+ NoEntryError = Class.new(DoingRuntimeError)
100
+
101
+ InvalidTimeExpression = Class.new(DoingRuntimeError)
102
+ InvalidSection = Class.new(DoingRuntimeError)
103
+ InvalidView = Class.new(DoingRuntimeError)
104
+
105
+ ItemNotFound = Class.new(DoingRuntimeError)
106
+ # FatalException = Class.new(::RuntimeError)
107
+ # InvalidPluginName = Class.new(FatalException)
108
+ end
109
+ 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