doing 2.1.37 → 2.1.40
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/CHANGELOG.md +61 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +7 -1
- data/bin/commands/config.rb +43 -34
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +30 -25
- data/bin/commands/grep.rb +3 -14
- data/bin/commands/last.rb +2 -8
- data/bin/commands/meanwhile.rb +13 -6
- data/bin/commands/now.rb +2 -4
- data/bin/commands/on.rb +4 -15
- data/bin/commands/recent.rb +2 -8
- data/bin/commands/reset.rb +24 -1
- data/bin/commands/select.rb +1 -1
- data/bin/commands/show.rb +8 -16
- data/bin/commands/since.rb +1 -12
- data/bin/commands/today.rb +2 -13
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +41 -36
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +166 -20
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +1 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
- data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
- data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
- data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
- data/docs/doc/Doing/Errors/NoResults.html +10 -2
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
- data/docs/doc/Doing/Errors.html +9 -9
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +114 -1576
- data/docs/doc/Doing/Items.html +121 -5
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +2 -2
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +5 -5
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +197 -4033
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/FalseClass.html +1 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +26 -5
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +237 -709
- data/docs/doc/top-level-namespace.html +3 -3
- data/docs/index.md +1 -1
- data/doing.rdoc +54 -7
- data/lib/completion/_doing.zsh +6 -6
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +8 -2
- data/lib/doing/add_options.rb +31 -1
- data/lib/doing/chronify/array.rb +68 -18
- data/lib/doing/chronify/string.rb +3 -1
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/query.rb +433 -0
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/item.rb +6 -537
- data/lib/doing/items.rb +39 -14
- data/lib/doing/plugin_manager.rb +3 -3
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/prompt.rb +6 -8
- data/lib/doing/string/tags.rb +8 -2
- data/lib/doing/util_backup.rb +6 -8
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +399 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +186 -0
- data/lib/doing/wwid/filter.rb +218 -0
- data/lib/doing/wwid/guess.rb +87 -0
- data/lib/doing/wwid/interactive.rb +385 -0
- data/lib/doing/wwid/modify.rb +618 -0
- data/lib/doing/wwid/tags.rb +54 -0
- data/lib/doing/wwid/timers.rb +345 -0
- data/lib/doing/wwid/wwidutil.rb +104 -0
- data/lib/doing/wwid.rb +31 -2308
- metadata +19 -2
data/lib/doing/item.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'item/dates'
|
|
4
|
+
require_relative 'item/tags'
|
|
5
|
+
require_relative 'item/state'
|
|
6
|
+
require_relative 'item/query'
|
|
7
|
+
|
|
3
8
|
module Doing
|
|
4
9
|
##
|
|
5
10
|
## This class describes a single WWID item
|
|
@@ -29,57 +34,6 @@ module Doing
|
|
|
29
34
|
@note = Note.new(note)
|
|
30
35
|
end
|
|
31
36
|
|
|
32
|
-
# def date=(new_date)
|
|
33
|
-
# @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
|
|
34
|
-
# end
|
|
35
|
-
|
|
36
|
-
## If the entry doesn't have a @done date, return the elapsed time
|
|
37
|
-
def duration
|
|
38
|
-
return nil unless should_time? && should_finish?
|
|
39
|
-
|
|
40
|
-
return nil if @title =~ /(?<=^| )@done\b/
|
|
41
|
-
|
|
42
|
-
return Time.now - @date
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
##
|
|
46
|
-
## Get the difference between the item's start date and
|
|
47
|
-
## the value of its @done tag (if present)
|
|
48
|
-
##
|
|
49
|
-
## @return Interval in seconds
|
|
50
|
-
##
|
|
51
|
-
def interval
|
|
52
|
-
@interval ||= calc_interval
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
##
|
|
56
|
-
## Get the value of the item's @done tag
|
|
57
|
-
##
|
|
58
|
-
## @return [Time] @done value
|
|
59
|
-
##
|
|
60
|
-
def end_date
|
|
61
|
-
@end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def calculate_end_date(opt)
|
|
65
|
-
if opt[:took]
|
|
66
|
-
if @date + opt[:took] > Time.now
|
|
67
|
-
@date = Time.now - opt[:took]
|
|
68
|
-
Time.now
|
|
69
|
-
else
|
|
70
|
-
@date + opt[:took]
|
|
71
|
-
end
|
|
72
|
-
elsif opt[:back]
|
|
73
|
-
if opt[:back].is_a? Integer
|
|
74
|
-
@date + opt[:back]
|
|
75
|
-
else
|
|
76
|
-
@date + (opt[:back] - @date)
|
|
77
|
-
end
|
|
78
|
-
else
|
|
79
|
-
Time.now
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
37
|
# Generate a hash that represents the entry
|
|
84
38
|
#
|
|
85
39
|
# @return [String] entry hash
|
|
@@ -107,296 +61,6 @@ module Doing
|
|
|
107
61
|
true
|
|
108
62
|
end
|
|
109
63
|
|
|
110
|
-
##
|
|
111
|
-
## Test if two items occur at the same time (same start date and equal duration)
|
|
112
|
-
##
|
|
113
|
-
## @param item_b [Item] The item to compare
|
|
114
|
-
##
|
|
115
|
-
## @return [Boolean] is equal?
|
|
116
|
-
##
|
|
117
|
-
def same_time?(item_b)
|
|
118
|
-
date == item_b.date ? interval == item_b.interval : false
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
##
|
|
122
|
-
## Test if the interval between start date and @done
|
|
123
|
-
## value overlaps with another item's
|
|
124
|
-
##
|
|
125
|
-
## @param item_b [Item] The item to compare
|
|
126
|
-
##
|
|
127
|
-
## @return [Boolean] overlaps?
|
|
128
|
-
##
|
|
129
|
-
def overlapping_time?(item_b)
|
|
130
|
-
return true if same_time?(item_b)
|
|
131
|
-
|
|
132
|
-
start_a = date
|
|
133
|
-
a_interval = interval
|
|
134
|
-
end_a = a_interval ? start_a + a_interval.to_i : start_a
|
|
135
|
-
start_b = item_b.date
|
|
136
|
-
b_interval = item_b.interval
|
|
137
|
-
end_b = b_interval ? start_b + b_interval.to_i : start_b
|
|
138
|
-
(start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
##
|
|
142
|
-
## Updates the title of the Item by expanding natural
|
|
143
|
-
## language dates within configured date tags (tags
|
|
144
|
-
## whose value is expected to be a date)
|
|
145
|
-
##
|
|
146
|
-
## @param additional_tags An array of additional
|
|
147
|
-
## tag names to consider
|
|
148
|
-
## dates
|
|
149
|
-
##
|
|
150
|
-
def expand_date_tags(additional_tags = nil)
|
|
151
|
-
@title.expand_date_tags(additional_tags)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
##
|
|
155
|
-
## Add (or remove) tags from the title of the item
|
|
156
|
-
##
|
|
157
|
-
## @param tags [Array] The tags to apply
|
|
158
|
-
## @param options Additional options
|
|
159
|
-
##
|
|
160
|
-
## @option options :date [Boolean] Include timestamp?
|
|
161
|
-
## @option options :single [Boolean] Log as a single change?
|
|
162
|
-
## @option options :value [String] A value to include as @tag(value)
|
|
163
|
-
## @option options :remove [Boolean] if true remove instead of adding
|
|
164
|
-
## @option options :rename_to [String] if not nil, rename target tag to this tag name
|
|
165
|
-
## @option options :regex [Boolean] treat target tag string as regex pattern
|
|
166
|
-
## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
|
|
167
|
-
##
|
|
168
|
-
def tag(tags, **options)
|
|
169
|
-
added = []
|
|
170
|
-
removed = []
|
|
171
|
-
|
|
172
|
-
date = options.fetch(:date, false)
|
|
173
|
-
options[:value] ||= date ? Time.now.strftime('%F %R') : nil
|
|
174
|
-
options.delete(:date)
|
|
175
|
-
|
|
176
|
-
single = options.fetch(:single, false)
|
|
177
|
-
options.delete(:single)
|
|
178
|
-
|
|
179
|
-
tags = tags.to_tags if tags.is_a? ::String
|
|
180
|
-
|
|
181
|
-
remove = options.fetch(:remove, false)
|
|
182
|
-
tags.each do |tag|
|
|
183
|
-
bool = remove ? :and : :not
|
|
184
|
-
if tags?(tag, bool)
|
|
185
|
-
@title = @title.tag(tag, **options).strip
|
|
186
|
-
remove ? removed.push(tag) : added.push(tag)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
|
|
191
|
-
|
|
192
|
-
self
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
##
|
|
196
|
-
## Get a list of tags on the item
|
|
197
|
-
##
|
|
198
|
-
## @return [Array] array of tags (no values)
|
|
199
|
-
##
|
|
200
|
-
def tags
|
|
201
|
-
@title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
##
|
|
205
|
-
## convert tags on item to an array with @ symbols removed
|
|
206
|
-
##
|
|
207
|
-
## @return [Array] array of tags
|
|
208
|
-
##
|
|
209
|
-
def tag_array
|
|
210
|
-
tags.tags_to_array
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
##
|
|
214
|
-
## Test if item contains tag(s)
|
|
215
|
-
##
|
|
216
|
-
## @param tags (Array or String) The tags to test. Can be an array or a comma-separated string.
|
|
217
|
-
## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
|
|
218
|
-
## @param negate [Boolean] negate the result?
|
|
219
|
-
##
|
|
220
|
-
## @return [Boolean] true if tag/bool combination passes
|
|
221
|
-
##
|
|
222
|
-
def tags?(tags, bool = :and, negate: false)
|
|
223
|
-
if bool == :pattern
|
|
224
|
-
tags = tags.to_tags.tags_to_array.join(' ')
|
|
225
|
-
matches = tag_pattern?(tags)
|
|
226
|
-
|
|
227
|
-
return negate ? !matches : matches
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
tags = split_tags(tags)
|
|
231
|
-
bool = bool.normalize_bool
|
|
232
|
-
|
|
233
|
-
matches = case bool
|
|
234
|
-
when :and
|
|
235
|
-
all_tags?(tags)
|
|
236
|
-
when :not
|
|
237
|
-
no_tags?(tags)
|
|
238
|
-
else
|
|
239
|
-
any_tags?(tags)
|
|
240
|
-
end
|
|
241
|
-
negate ? !matches : matches
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
##
|
|
245
|
-
## Test if item matches tag values
|
|
246
|
-
##
|
|
247
|
-
## @param queries (Array) The tag value queries to test
|
|
248
|
-
## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
|
|
249
|
-
## @param negate [Boolean] negate the result?
|
|
250
|
-
##
|
|
251
|
-
## @return [Boolean] true if tag/bool combination passes
|
|
252
|
-
##
|
|
253
|
-
def tag_values?(queries, bool = :and, negate: false)
|
|
254
|
-
bool = bool.normalize_bool
|
|
255
|
-
|
|
256
|
-
matches = case bool
|
|
257
|
-
when :and
|
|
258
|
-
all_values?(queries)
|
|
259
|
-
when :not
|
|
260
|
-
no_values?(queries)
|
|
261
|
-
else
|
|
262
|
-
any_values?(queries)
|
|
263
|
-
end
|
|
264
|
-
negate ? !matches : matches
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
##
|
|
268
|
-
## Determine if case should be ignored for searches
|
|
269
|
-
##
|
|
270
|
-
## @param search [String] The search string
|
|
271
|
-
## @param case_type [Symbol] The case type
|
|
272
|
-
##
|
|
273
|
-
## @return [Boolean] case should be ignored
|
|
274
|
-
##
|
|
275
|
-
def ignore_case(search, case_type)
|
|
276
|
-
(case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def highlight_search(search, distance: nil, negate: false, case_type: nil)
|
|
280
|
-
prefs = Doing.setting('search', {})
|
|
281
|
-
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
|
282
|
-
distance ||= prefs.fetch('distance', 3).to_i
|
|
283
|
-
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
|
284
|
-
new_note = Note.new
|
|
285
|
-
|
|
286
|
-
if search.rx? || matching == :fuzzy
|
|
287
|
-
rx = search.to_rx(distance: distance, case_type: case_type)
|
|
288
|
-
new_title = @title.gsub(rx) { |m| yellow(m) }
|
|
289
|
-
new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
|
|
290
|
-
else
|
|
291
|
-
query = search.strip.to_phrase_query
|
|
292
|
-
|
|
293
|
-
if query[:must].nil? && query[:must_not].nil?
|
|
294
|
-
query[:must] = query[:should]
|
|
295
|
-
query[:should] = []
|
|
296
|
-
end
|
|
297
|
-
query[:must].concat(query[:should]).each do |s|
|
|
298
|
-
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
299
|
-
new_title = @title.gsub(rx) { |m| yellow(m) }
|
|
300
|
-
new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
Item.new(@date, new_title, @section, new_note)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
##
|
|
308
|
-
## Test if item matches search string
|
|
309
|
-
##
|
|
310
|
-
## @param search [String] The search string
|
|
311
|
-
## @param negate [Boolean] negate results
|
|
312
|
-
## @param case_type (Symbol) The case-sensitivity
|
|
313
|
-
## type (:sensitive,
|
|
314
|
-
## :ignore, :smart)
|
|
315
|
-
##
|
|
316
|
-
## @return [Boolean] matches search criteria
|
|
317
|
-
##
|
|
318
|
-
def search(search, distance: nil, negate: false, case_type: nil)
|
|
319
|
-
prefs = Doing.setting('search', {})
|
|
320
|
-
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
|
321
|
-
distance ||= prefs.fetch('distance', 3).to_i
|
|
322
|
-
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
|
323
|
-
|
|
324
|
-
if search.rx? || matching == :fuzzy
|
|
325
|
-
matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
|
|
326
|
-
else
|
|
327
|
-
query = search.strip.to_phrase_query
|
|
328
|
-
|
|
329
|
-
if query[:must].nil? && query[:must_not].nil?
|
|
330
|
-
query[:must] = query[:should]
|
|
331
|
-
query[:should] = []
|
|
332
|
-
end
|
|
333
|
-
matches = no_searches?(query[:must_not], case_type: case_type)
|
|
334
|
-
matches &&= all_searches?(query[:must], case_type: case_type)
|
|
335
|
-
matches &&= any_searches?(query[:should], case_type: case_type)
|
|
336
|
-
end
|
|
337
|
-
# if search =~ /(?<=\A| )[+-]\S/
|
|
338
|
-
# else
|
|
339
|
-
# text = @title + @note.to_s
|
|
340
|
-
# matches = text =~ search.to_rx(distance: distance, case_type: case_type)
|
|
341
|
-
# end
|
|
342
|
-
|
|
343
|
-
# if search.rx? || !fuzzy
|
|
344
|
-
# matches = text =~ search.to_rx(distance: distance, case_type: case_type)
|
|
345
|
-
# else
|
|
346
|
-
# distance = 0.25 if distance > 1
|
|
347
|
-
# score = if (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
|
348
|
-
# text.downcase.pair_distance_similar(search.downcase)
|
|
349
|
-
# else
|
|
350
|
-
# score = text.pair_distance_similar(search)
|
|
351
|
-
# end
|
|
352
|
-
|
|
353
|
-
# if score >= distance
|
|
354
|
-
# matches = true
|
|
355
|
-
# Doing.logger.debug('Fuzzy Match:', %(#{@title}, "#{search}" #{score}))
|
|
356
|
-
# end
|
|
357
|
-
# end
|
|
358
|
-
|
|
359
|
-
negate ? !matches : matches
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
##
|
|
363
|
-
## Test if item has a @done tag
|
|
364
|
-
##
|
|
365
|
-
## @return [Boolean] true item has @done tag
|
|
366
|
-
##
|
|
367
|
-
def finished?
|
|
368
|
-
tags?('done')
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
##
|
|
372
|
-
## Test if item does not contain @done tag
|
|
373
|
-
##
|
|
374
|
-
## @return [Boolean] true if item is missing @done tag
|
|
375
|
-
##
|
|
376
|
-
def unfinished?
|
|
377
|
-
tags?('done', negate: true)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
##
|
|
381
|
-
## Test if item is included in never_finish config and
|
|
382
|
-
## thus should not receive a @done tag
|
|
383
|
-
##
|
|
384
|
-
## @return [Boolean] item should receive @done tag
|
|
385
|
-
##
|
|
386
|
-
def should_finish?
|
|
387
|
-
should?('never_finish')
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
##
|
|
391
|
-
## Test if item is included in never_time config and
|
|
392
|
-
## thus should not receive a date on the @done tag
|
|
393
|
-
##
|
|
394
|
-
## @return [Boolean] item should receive @done date
|
|
395
|
-
##
|
|
396
|
-
def should_time?
|
|
397
|
-
should?('never_time')
|
|
398
|
-
end
|
|
399
|
-
|
|
400
64
|
##
|
|
401
65
|
## Move item from current section to destination section
|
|
402
66
|
##
|
|
@@ -449,206 +113,11 @@ module Doing
|
|
|
449
113
|
# @private
|
|
450
114
|
def inspect
|
|
451
115
|
# %(<Doing::Item @date=#{@date} @title="#{@title}" @section:"#{@section}" @note:#{@note.to_s}>)
|
|
452
|
-
%(<Doing::Item @date=#{@date}>)
|
|
116
|
+
%(<Doing::Item @date=#{@date.strftime('%F %T')} @section=#{@section} @title=#{@title.trunc(30)}>)
|
|
453
117
|
end
|
|
454
118
|
|
|
455
119
|
def clone
|
|
456
120
|
Marshal.load(Marshal.dump(self))
|
|
457
121
|
end
|
|
458
|
-
|
|
459
|
-
private
|
|
460
|
-
|
|
461
|
-
def should?(key)
|
|
462
|
-
config = Doing.settings
|
|
463
|
-
return true unless config[key].is_a?(Array)
|
|
464
|
-
|
|
465
|
-
config[key].each do |tag|
|
|
466
|
-
if tag =~ /^@/
|
|
467
|
-
return false if tags?(tag.sub(/^@/, '').downcase)
|
|
468
|
-
elsif section.downcase == tag.downcase
|
|
469
|
-
return false
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
true
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
def calc_interval
|
|
477
|
-
return nil unless should_time? && should_finish?
|
|
478
|
-
|
|
479
|
-
done = end_date
|
|
480
|
-
return nil if done.nil?
|
|
481
|
-
|
|
482
|
-
start = @date
|
|
483
|
-
|
|
484
|
-
t = (done - start).to_i
|
|
485
|
-
t.positive? ? t : nil
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
def all_searches?(searches, case_type: :smart)
|
|
489
|
-
return true unless searches.good?
|
|
490
|
-
|
|
491
|
-
text = @title + @note.to_s
|
|
492
|
-
searches.each do |s|
|
|
493
|
-
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
494
|
-
return false unless text =~ rx
|
|
495
|
-
end
|
|
496
|
-
true
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
def no_searches?(searches, case_type: :smart)
|
|
500
|
-
return true unless searches.good?
|
|
501
|
-
|
|
502
|
-
text = @title + @note.to_s
|
|
503
|
-
searches.each do |s|
|
|
504
|
-
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
505
|
-
return false if text =~ rx
|
|
506
|
-
end
|
|
507
|
-
true
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
def any_searches?(searches, case_type: :smart)
|
|
511
|
-
return true unless searches.good?
|
|
512
|
-
|
|
513
|
-
text = @title + @note.to_s
|
|
514
|
-
searches.each do |s|
|
|
515
|
-
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
516
|
-
return true if text =~ rx
|
|
517
|
-
end
|
|
518
|
-
false
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
def all_tags?(tags)
|
|
522
|
-
return true unless tags.good?
|
|
523
|
-
|
|
524
|
-
tags.each do |tag|
|
|
525
|
-
return false unless @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
|
526
|
-
end
|
|
527
|
-
true
|
|
528
|
-
end
|
|
529
|
-
|
|
530
|
-
def no_tags?(tags)
|
|
531
|
-
return true unless tags.good?
|
|
532
|
-
|
|
533
|
-
tags.each do |tag|
|
|
534
|
-
return false if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
|
535
|
-
end
|
|
536
|
-
true
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
def any_tags?(tags)
|
|
540
|
-
return true unless tags.good?
|
|
541
|
-
|
|
542
|
-
tags.each do |tag|
|
|
543
|
-
return true if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
|
544
|
-
end
|
|
545
|
-
false
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
def tag_value(tag)
|
|
549
|
-
res = @title.match(/@#{tag.sub(/^@/, '').wildcard_to_rx}\((.*?)\)/)
|
|
550
|
-
res ? res[1] : nil
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
def number_or_date(value)
|
|
554
|
-
return nil unless value
|
|
555
|
-
|
|
556
|
-
if value.strip =~ /^[0-9.]+%?$/
|
|
557
|
-
value.strip.to_f
|
|
558
|
-
else
|
|
559
|
-
value.strip.chronify(guess: :end)
|
|
560
|
-
end
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
def split_value_query(query)
|
|
564
|
-
val_rx = /^(!)?@?(\S+) +(!?[<>=][=*]?|[$*^]=) +(.*?)$/
|
|
565
|
-
query.match(val_rx)
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
def any_values?(queries)
|
|
569
|
-
return true unless queries.good?
|
|
570
|
-
|
|
571
|
-
queries.each do |q|
|
|
572
|
-
parts = split_value_query(q)
|
|
573
|
-
return true if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
|
|
574
|
-
end
|
|
575
|
-
false
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
def all_values?(queries)
|
|
579
|
-
return true unless queries.good?
|
|
580
|
-
|
|
581
|
-
queries.each do |q|
|
|
582
|
-
parts = split_value_query(q)
|
|
583
|
-
return false unless tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
|
|
584
|
-
end
|
|
585
|
-
true
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
def no_values?(queries)
|
|
589
|
-
return true unless queries.good?
|
|
590
|
-
|
|
591
|
-
queries.each do |q|
|
|
592
|
-
parts = split_value_query(q)
|
|
593
|
-
return false if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
|
|
594
|
-
end
|
|
595
|
-
true
|
|
596
|
-
end
|
|
597
|
-
|
|
598
|
-
def tag_value_matches?(tag, comp, value, negate)
|
|
599
|
-
if all_tags?([tag])
|
|
600
|
-
tag_val = tag_value(tag)
|
|
601
|
-
|
|
602
|
-
if (value.chronify.nil? && value =~ /[a-z]/i && comp =~ /^!?==?$/) || comp =~ /[$*^]=/
|
|
603
|
-
is_match = case comp
|
|
604
|
-
when /\^=/
|
|
605
|
-
tag_val =~ /^#{value.wildcard_to_rx}/i
|
|
606
|
-
when /\$=/
|
|
607
|
-
tag_val =~ /#{value.wildcard_to_rx}$/i
|
|
608
|
-
when %r{==}
|
|
609
|
-
tag_val =~ /^#{value.wildcard_to_rx}$/i
|
|
610
|
-
else
|
|
611
|
-
tag_val =~ /#{value.wildcard_to_rx}/i
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
comp =~ /!/ || negate ? !is_match : is_match
|
|
615
|
-
else
|
|
616
|
-
tag_val = number_or_date(tag_val)
|
|
617
|
-
val = number_or_date(value)
|
|
618
|
-
|
|
619
|
-
return false if val.nil? || tag_val.nil?
|
|
620
|
-
|
|
621
|
-
return false unless val.class == tag_val.class
|
|
622
|
-
|
|
623
|
-
matches = case comp
|
|
624
|
-
when /^<$/
|
|
625
|
-
tag_val < val
|
|
626
|
-
when /^<=$/
|
|
627
|
-
tag_val <= val
|
|
628
|
-
when /^>$/
|
|
629
|
-
tag_val > val
|
|
630
|
-
when /^>=$/
|
|
631
|
-
tag_val >= val
|
|
632
|
-
when /^!=/
|
|
633
|
-
tag_val != val
|
|
634
|
-
when /^=/
|
|
635
|
-
tag_val == val
|
|
636
|
-
end
|
|
637
|
-
negate.nil? ? matches : !matches
|
|
638
|
-
end
|
|
639
|
-
else
|
|
640
|
-
false
|
|
641
|
-
end
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
def tag_pattern?(tags)
|
|
645
|
-
query = tags.to_query
|
|
646
|
-
|
|
647
|
-
no_tags?(query[:must_not]) && all_tags?(query[:must]) && any_tags?(query[:should])
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
def split_tags(tags)
|
|
651
|
-
tags.to_tags.tags_to_array
|
|
652
|
-
end
|
|
653
122
|
end
|
|
654
123
|
end
|
data/lib/doing/items.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Doing
|
|
4
|
-
#
|
|
4
|
+
# A collection of Item objects
|
|
5
5
|
class Items < Array
|
|
6
6
|
attr_accessor :sections
|
|
7
7
|
|
|
@@ -27,13 +27,29 @@ module Doing
|
|
|
27
27
|
def section?(section)
|
|
28
28
|
has_section = false
|
|
29
29
|
section = section.is_a?(Section) ? section.title.downcase : section.downcase
|
|
30
|
-
@sections.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
@sections.map { |i| i.title.downcase }.include?(section)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
## Return the best section match for a search query
|
|
35
|
+
##
|
|
36
|
+
## @param frag The search query
|
|
37
|
+
## @param distance The distance apart characters can be (fuzziness)
|
|
38
|
+
##
|
|
39
|
+
## @return [Section] (first) matching section object
|
|
40
|
+
##
|
|
41
|
+
def guess_section(frag, distance: 2)
|
|
42
|
+
section = nil
|
|
43
|
+
re = frag.to_rx(distance: distance, case_type: :ignore)
|
|
44
|
+
@sections.each do |sect|
|
|
45
|
+
next unless sect.title =~ /#{re}/i
|
|
46
|
+
|
|
47
|
+
Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
|
|
48
|
+
section = sect
|
|
49
|
+
break
|
|
35
50
|
end
|
|
36
|
-
|
|
51
|
+
|
|
52
|
+
section
|
|
37
53
|
end
|
|
38
54
|
|
|
39
55
|
# Add a new section to the sections array. Accepts
|
|
@@ -131,17 +147,26 @@ module Doing
|
|
|
131
147
|
end
|
|
132
148
|
|
|
133
149
|
##
|
|
134
|
-
## Return Items containing items that don't exist in
|
|
150
|
+
## Return Items containing items that don't exist in
|
|
151
|
+
## receiver
|
|
135
152
|
##
|
|
136
153
|
## @param items [Items] Receiver
|
|
137
154
|
##
|
|
155
|
+
## @return [Hash] Hash of added and deleted items
|
|
156
|
+
##
|
|
138
157
|
def diff(items)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
a = clone
|
|
159
|
+
b = items.clone
|
|
160
|
+
|
|
161
|
+
a.delete_if do |item|
|
|
162
|
+
if b.index(item)
|
|
163
|
+
b.delete(item)
|
|
164
|
+
true
|
|
165
|
+
else
|
|
166
|
+
false
|
|
167
|
+
end
|
|
143
168
|
end
|
|
144
|
-
|
|
169
|
+
{ deleted: b, added: a }
|
|
145
170
|
end
|
|
146
171
|
|
|
147
172
|
##
|
|
@@ -179,7 +204,7 @@ module Doing
|
|
|
179
204
|
out = []
|
|
180
205
|
@sections.each do |section|
|
|
181
206
|
out.push(section.original)
|
|
182
|
-
items = in_section(section.title).sort_by
|
|
207
|
+
items = in_section(section.title).sort_by { |i| [i.date, i.title] }
|
|
183
208
|
items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
|
|
184
209
|
items.each { |item| out.push(item.to_s) }
|
|
185
210
|
end
|
data/lib/doing/plugin_manager.rb
CHANGED
|
@@ -84,11 +84,11 @@ module Doing
|
|
|
84
84
|
def validate_plugin(title, type, klass)
|
|
85
85
|
type = valid_type(type)
|
|
86
86
|
if type == :import && !klass.respond_to?(:import)
|
|
87
|
-
raise Errors::PluginUncallable.new('Import plugins must respond to :import', type
|
|
87
|
+
raise Errors::PluginUncallable.new('Import plugins must respond to :import', type, title)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
if type == :export && !klass.respond_to?(:render)
|
|
91
|
-
raise Errors::PluginUncallable.new('Export plugins must respond to :render', type
|
|
91
|
+
raise Errors::PluginUncallable.new('Export plugins must respond to :render', type, title)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
type
|
|
@@ -113,7 +113,7 @@ module Doing
|
|
|
113
113
|
when /^e(x(p(o(r(t)?)?)?)?)?$/
|
|
114
114
|
:export
|
|
115
115
|
else
|
|
116
|
-
raise Errors::InvalidPluginType
|
|
116
|
+
raise Errors::InvalidPluginType.new('Invalid plugin type', 'unrecognized')
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
type.to_sym
|
|
@@ -17,7 +17,7 @@ module Doing
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.render(wwid, items, variables: {})
|
|
20
|
-
|
|
20
|
+
Doing.logger.benchmark(:template_render, :start)
|
|
21
21
|
return if items.nil?
|
|
22
22
|
|
|
23
23
|
opt = variables[:options]
|
|
@@ -126,18 +126,18 @@ module Doing
|
|
|
126
126
|
|
|
127
127
|
output.gsub!(/\\%/, '%')
|
|
128
128
|
|
|
129
|
-
output.highlight_search!(opt[:search]) if opt[:
|
|
129
|
+
output.highlight_search!(opt[:search]) if opt[:output] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
|
|
130
130
|
|
|
131
131
|
out += "#{output}\n"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
# Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:
|
|
134
|
+
# Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:output]}")
|
|
135
135
|
if opt[:totals]
|
|
136
136
|
out += wwid.tag_times(format: Doing.setting('timer_format').to_sym,
|
|
137
137
|
sort_by: opt[:sort_tags],
|
|
138
138
|
sort_order: opt[:tag_order])
|
|
139
139
|
end
|
|
140
|
-
|
|
140
|
+
Doing.logger.benchmark(:template_render, :finish)
|
|
141
141
|
out
|
|
142
142
|
end
|
|
143
143
|
|
|
Binary file
|