doing 1.0.93 → 2.0.2.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/AUTHORS +19 -0
- data/CHANGELOG.md +590 -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 +1012 -486
- data/doing.fish +278 -0
- data/doing.gemspec +34 -0
- data/doing.rdoc +1759 -0
- data/example_plugin.rb +209 -0
- data/generate_completions.sh +4 -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 +151 -0
- data/lib/completion/doing.bash +416 -0
- data/lib/completion/doing.fish +278 -0
- data/lib/doing/array.rb +8 -0
- data/lib/doing/cli_status.rb +66 -0
- data/lib/doing/colors.rb +136 -0
- data/lib/doing/configuration.rb +310 -0
- data/lib/doing/errors.rb +102 -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 +342 -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 +346 -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 +1831 -2358
- data/lib/doing/wwidfile.rb +117 -0
- data/lib/doing.rb +43 -3
- data/lib/examples/commands/wiki.rb +80 -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 +210 -0
- data/scripts/generate_fish_completions.rb +201 -0
- data/scripts/generate_zsh_completions.rb +164 -0
- metadata +82 -7
- data/lib/doing/helpers.rb +0 -191
- data/lib/doing/markdown_export.rb +0 -16
@@ -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
|
data/lib/doing/errors.rb
ADDED
@@ -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
|
data/lib/doing/hooks.rb
ADDED
@@ -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
|