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,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
|
data/lib/doing/errors.rb
ADDED
@@ -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
|
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
|