doing 2.1.39 → 2.1.42
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +4 -4
- data/bin/commands/again.rb +1 -3
- data/bin/commands/changes.rb +50 -34
- data/bin/commands/commands.rb +77 -52
- data/bin/commands/commands_accepting.rb +57 -53
- data/bin/commands/config.rb +45 -36
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +90 -59
- data/bin/commands/flag.rb +5 -1
- 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 +151 -107
- data/bin/commands/on.rb +8 -18
- 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 +6 -17
- data/bin/commands/since.rb +1 -12
- data/bin/commands/tag_dir.rb +49 -15
- data/bin/commands/today.rb +2 -13
- data/bin/commands/undo.rb +4 -6
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +15 -8
- data/{Dockerfile → docker/Dockerfile} +3 -1
- data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
- data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
- data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
- data/{bash_profile → docker/bash_profile} +0 -0
- data/{inputrc → docker/inputrc} +0 -0
- data/docs/doc/Array.html +85 -2
- 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/ArrayNestedHash.html +198 -0
- data/docs/doc/Doing/ArrayTags.html +424 -0
- data/docs/doc/Doing/CSVExport.html +266 -0
- data/docs/doc/Doing/CalendarImport.html +232 -0
- data/docs/doc/Doing/Change.html +617 -0
- data/docs/doc/Doing/Changes.html +468 -0
- data/docs/doc/Doing/ChronifyArray.html +347 -0
- data/docs/doc/Doing/ChronifyNumeric.html +271 -0
- data/docs/doc/Doing/ChronifyString.html +682 -0
- data/docs/doc/Doing/Color.html +167 -21
- data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
- data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
- data/docs/doc/Doing/Completion/StringUtils.html +229 -0
- data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
- data/docs/doc/Doing/Completion.html +17 -3
- data/docs/doc/Doing/Configuration.html +3 -2
- data/docs/doc/Doing/DayOneRenderer.html +383 -0
- data/docs/doc/Doing/DayoneExport.html +290 -0
- data/docs/doc/Doing/DoingImport.html +391 -0
- data/docs/doc/Doing/Entry.html +381 -0
- 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/HTMLExport.html +256 -0
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +179 -1660
- data/docs/doc/Doing/ItemDates.html +564 -0
- data/docs/doc/Doing/ItemQuery.html +614 -0
- data/docs/doc/Doing/ItemState.html +387 -0
- data/docs/doc/Doing/ItemTags.html +498 -0
- data/docs/doc/Doing/Items.html +581 -15
- data/docs/doc/Doing/JSONExport.html +222 -0
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/MarkdownExport.html +266 -0
- data/docs/doc/Doing/MarkdownRenderer.html +383 -0
- data/docs/doc/Doing/Note.html +18 -4
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +181 -76
- data/docs/doc/Doing/Prompt.html +32 -683
- data/docs/doc/Doing/PromptChoose.html +484 -0
- data/docs/doc/Doing/PromptFZF.html +391 -0
- data/docs/doc/Doing/PromptInput.html +572 -0
- data/docs/doc/Doing/PromptSTD.html +293 -0
- data/docs/doc/Doing/PromptYN.html +237 -0
- data/docs/doc/Doing/Section.html +58 -2
- data/docs/doc/Doing/StringHighlight.html +533 -0
- data/docs/doc/Doing/StringNormalize.html +929 -0
- data/docs/doc/Doing/StringQuery.html +725 -0
- data/docs/doc/Doing/StringTags.html +884 -0
- data/docs/doc/Doing/StringTransform.html +599 -0
- data/docs/doc/Doing/StringTruncate.html +448 -0
- data/docs/doc/Doing/StringURL.html +409 -0
- data/docs/doc/Doing/SymbolNormalize.html +341 -0
- data/docs/doc/Doing/TaskPaperExport.html +222 -0
- data/docs/doc/Doing/TemplateExport.html +249 -0
- data/docs/doc/Doing/TemplateString.html +102 -3
- data/docs/doc/Doing/TimingImport.html +285 -0
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +11 -163
- data/docs/doc/Doing/Util.html +67 -10
- data/docs/doc/Doing/Version.html +523 -0
- data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
- data/docs/doc/Doing/WWID.html +476 -139
- data/docs/doc/Doing/WWIDDisplay.html +865 -0
- data/docs/doc/Doing/WWIDEditor.html +466 -0
- data/docs/doc/Doing/WWIDFileTools.html +359 -0
- data/docs/doc/Doing/WWIDFilter.html +466 -0
- data/docs/doc/Doing/WWIDGuess.html +299 -0
- data/docs/doc/Doing/WWIDInteractive.html +752 -0
- data/docs/doc/Doing/WWIDModify.html +1078 -0
- data/docs/doc/Doing/WWIDTags.html +302 -0
- data/docs/doc/Doing/WWIDTimers.html +359 -0
- data/docs/doc/Doing/WWIDUtil.html +510 -0
- data/docs/doc/Doing.html +9 -6
- 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/Numeric.html +23 -78
- 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 +58 -633
- data/docs/doc/Symbol.html +9 -224
- data/docs/doc/Time.html +119 -13
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +348 -4
- 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 +1904 -592
- data/docs/doc/top-level-namespace.html +12 -4
- data/docs/index.md +1 -1
- data/doing.rdoc +67 -15
- data/lib/completion/_doing.zsh +6 -6
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +10 -3
- data/lib/doing/add_options.rb +39 -1
- data/lib/doing/array/array.rb +18 -12
- data/lib/doing/array/cleanup.rb +31 -0
- data/lib/doing/array/nested_hash.rb +1 -1
- data/lib/doing/array/tags.rb +6 -5
- data/lib/doing/changelog/changelog.rb +6 -0
- data/lib/doing/chronify/array.rb +65 -25
- data/lib/doing/chronify/chronify.rb +12 -0
- data/lib/doing/chronify/numeric.rb +3 -2
- data/lib/doing/chronify/string.rb +1 -1
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion/completion_string.rb +25 -0
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/configuration.rb +7 -3
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/good.rb +8 -0
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/item.rb +128 -0
- data/lib/doing/{item.rb → item/query.rb} +2 -353
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/items/filter.rb +67 -0
- data/lib/doing/items/items.rb +57 -0
- data/lib/doing/items/modify.rb +36 -0
- data/lib/doing/items/sections.rb +83 -0
- data/lib/doing/items/util.rb +74 -0
- data/lib/doing/normalize.rb +10 -2
- data/lib/doing/note.rb +1 -1
- data/lib/doing/pager.rb +9 -3
- data/lib/doing/plugin_manager.rb +33 -8
- data/lib/doing/plugins/export/markdown_export.rb +4 -2
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/prompt/choose.rb +118 -0
- data/lib/doing/prompt/fzf.rb +84 -0
- data/lib/doing/prompt/input.rb +129 -0
- data/lib/doing/prompt/prompt.rb +41 -0
- data/lib/doing/prompt/std.rb +32 -0
- data/lib/doing/prompt/yn.rb +64 -0
- data/lib/doing/section.rb +4 -0
- data/lib/doing/string/highlight.rb +1 -1
- data/lib/doing/string/query.rb +1 -1
- data/lib/doing/string/string.rb +18 -7
- data/lib/doing/string/tags.rb +14 -3
- data/lib/doing/string/transform.rb +7 -1
- data/lib/doing/string/truncate.rb +1 -1
- data/lib/doing/string/url.rb +1 -1
- data/lib/doing/time.rb +19 -1
- data/lib/doing/util.rb +12 -6
- data/lib/doing/util_backup.rb +62 -57
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +396 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +183 -0
- data/lib/doing/wwid/filter.rb +226 -0
- data/lib/doing/wwid/guess.rb +85 -0
- data/lib/doing/wwid/interactive.rb +377 -0
- data/lib/doing/wwid/modify.rb +617 -0
- data/lib/doing/wwid/tags.rb +51 -0
- data/lib/doing/wwid/timers.rb +342 -0
- data/lib/doing/wwid/wwid.rb +121 -0
- data/lib/doing/wwid/wwidutil.rb +101 -0
- data/lib/doing.rb +7 -7
- data/lib/helpers/threaded_tests.rb +1 -0
- metadata +94 -14
- data/lib/doing/changelog.rb +0 -6
- data/lib/doing/completion/string.rb +0 -17
- data/lib/doing/items.rb +0 -196
- data/lib/doing/prompt.rb +0 -330
- data/lib/doing/wwid.rb +0 -2398
@@ -0,0 +1,617 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class WWID
|
5
|
+
##
|
6
|
+
## Adds an entry
|
7
|
+
##
|
8
|
+
## @param title [String] The entry title
|
9
|
+
## @param section [String] The section to add to
|
10
|
+
## @param opt [Hash] Additional Options
|
11
|
+
##
|
12
|
+
## @option opt :date [Date] item start date
|
13
|
+
## @option opt :note [Note] item note (will be converted if value is String)
|
14
|
+
## @option opt :back [Date] backdate
|
15
|
+
## @option opt :timed [Boolean] new item is timed entry, marks previous entry as @done
|
16
|
+
## @option opt :done [Date] If set, adds a @done tag to new entry
|
17
|
+
##
|
18
|
+
def add_item(title, section = nil, opt)
|
19
|
+
opt ||= {}
|
20
|
+
section ||= Doing.setting('current_section')
|
21
|
+
@content.add_section(section, log: false)
|
22
|
+
opt[:back] ||= opt[:date] ? opt[:date] : Time.now
|
23
|
+
opt[:date] ||= Time.now
|
24
|
+
note = Note.new
|
25
|
+
opt[:timed] ||= false
|
26
|
+
|
27
|
+
note.add(opt[:note]) if opt[:note]
|
28
|
+
|
29
|
+
title = [title.strip.cap_first]
|
30
|
+
title = title.join(' ')
|
31
|
+
|
32
|
+
if Doing.auto_tag
|
33
|
+
title = autotag(title)
|
34
|
+
title.add_tags!(Doing.setting('default_tags')) if Doing.setting('default_tags').good?
|
35
|
+
end
|
36
|
+
|
37
|
+
title.compress!
|
38
|
+
entry = Item.new(opt[:back], title.strip, section)
|
39
|
+
|
40
|
+
if opt[:done] && entry.should_finish?
|
41
|
+
if entry.should_time?
|
42
|
+
entry.tag('done', value: opt[:done])
|
43
|
+
else
|
44
|
+
entry.tag('done')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
entry.note = note
|
49
|
+
|
50
|
+
items = @content.clone
|
51
|
+
if opt[:timed]
|
52
|
+
items.reverse!
|
53
|
+
items.each_with_index do |i, x|
|
54
|
+
next if i.title =~ / @done/
|
55
|
+
|
56
|
+
finish_date = verify_duration(i.date, opt[:back], title: i.title)
|
57
|
+
items[x].tag('done', value: finish_date.strftime('%F %R'))
|
58
|
+
break
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
Hooks.trigger :pre_entry_add, self, entry
|
63
|
+
|
64
|
+
@content.push(entry)
|
65
|
+
# logger.count(:added, level: :debug)
|
66
|
+
logger.info('New entry:', %(added "#{entry.date.relative_date}: #{entry.title}" to #{section}))
|
67
|
+
|
68
|
+
Hooks.trigger :post_entry_added, self, entry
|
69
|
+
entry
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reset start date to current time, optionally remove
|
73
|
+
# done tag (resume)
|
74
|
+
#
|
75
|
+
# @param item [Item] the item to reset/resume
|
76
|
+
# @param resume [Boolean] removing @done tag if true
|
77
|
+
#
|
78
|
+
def reset_item(item, date: nil, finish_date: nil, resume: false)
|
79
|
+
date ||= Time.now
|
80
|
+
item.date = date
|
81
|
+
if finish_date
|
82
|
+
item.tag('done', remove: true)
|
83
|
+
item.tag('done', value: finish_date.strftime('%F %R'))
|
84
|
+
else
|
85
|
+
item.tag('done', remove: true) if resume
|
86
|
+
end
|
87
|
+
logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
|
88
|
+
item
|
89
|
+
end
|
90
|
+
|
91
|
+
# Duplicate an item and add it as a new item
|
92
|
+
#
|
93
|
+
# @param item [Item] the item to duplicate
|
94
|
+
# @param opt [Hash] additional options
|
95
|
+
#
|
96
|
+
# @option opt :editor [Boolean] open new item in editor
|
97
|
+
# @option opt :date [String] set start date
|
98
|
+
# @option opt :in [String] add new item to section :in
|
99
|
+
# @option opt :note [Note] add note to new item
|
100
|
+
#
|
101
|
+
# @return nothing
|
102
|
+
#
|
103
|
+
def repeat_item(item, opt)
|
104
|
+
opt ||= {}
|
105
|
+
old_item = item.clone
|
106
|
+
if item.should_finish?
|
107
|
+
if item.should_time?
|
108
|
+
finish_date = verify_duration(item.date, Time.now, title: item.title)
|
109
|
+
item.title.tag!('done', value: finish_date.strftime('%F %R'))
|
110
|
+
else
|
111
|
+
item.title.tag!('done')
|
112
|
+
end
|
113
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
114
|
+
end
|
115
|
+
|
116
|
+
# Remove @done tag
|
117
|
+
title = item.title.sub(/\s*@done(\(.*?\))?/, '').chomp
|
118
|
+
section = opt[:in].nil? ? item.section : guess_section(opt[:in])
|
119
|
+
Doing.auto_tag = false
|
120
|
+
|
121
|
+
note = opt[:note] || Note.new
|
122
|
+
|
123
|
+
if opt[:editor]
|
124
|
+
start = opt[:date] ? opt[:date] : Time.now
|
125
|
+
to_edit = "#{start.strftime('%F %R')} | #{title}"
|
126
|
+
to_edit += "\n#{note.strip_lines.join("\n")}" unless note.empty?
|
127
|
+
new_item = fork_editor(to_edit)
|
128
|
+
date, title, note = format_input(new_item)
|
129
|
+
|
130
|
+
opt[:date] = date unless date.nil?
|
131
|
+
|
132
|
+
if title.nil? || title.empty?
|
133
|
+
logger.warn('Skipped:', 'No content provided')
|
134
|
+
return
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# @content.update_item(original, item)
|
139
|
+
add_item(title, section, { note: note, back: opt[:date], timed: false })
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
## Restart the last entry
|
144
|
+
##
|
145
|
+
## @param opt [Hash] Additional Options
|
146
|
+
##
|
147
|
+
def repeat_last(opt)
|
148
|
+
opt ||= {}
|
149
|
+
opt[:section] ||= 'all'
|
150
|
+
opt[:section] = guess_section(opt[:section])
|
151
|
+
opt[:note] ||= []
|
152
|
+
opt[:tag] ||= []
|
153
|
+
opt[:tag_bool] ||= :and
|
154
|
+
|
155
|
+
last = last_entry(opt)
|
156
|
+
if last.nil?
|
157
|
+
logger.warn('Skipped:', 'No previous entry found')
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
repeat_item(last, opt)
|
162
|
+
write(@doing_file)
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
##
|
167
|
+
## Tag the last entry or X entries
|
168
|
+
##
|
169
|
+
## @param opt [Hash] Additional Options (see
|
170
|
+
## #filter_items for filtering
|
171
|
+
## options)
|
172
|
+
##
|
173
|
+
## @see #filter_items
|
174
|
+
##
|
175
|
+
def tag_last(opt) # hooked
|
176
|
+
opt ||= {}
|
177
|
+
opt[:count] ||= 1
|
178
|
+
opt[:archive] ||= false
|
179
|
+
opt[:tags] ||= ['done']
|
180
|
+
opt[:sequential] ||= false
|
181
|
+
opt[:date] ||= false
|
182
|
+
opt[:remove] ||= false
|
183
|
+
opt[:update] ||= false
|
184
|
+
opt[:autotag] ||= false
|
185
|
+
opt[:back] ||= false
|
186
|
+
opt[:unfinished] ||= false
|
187
|
+
opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
|
188
|
+
|
189
|
+
items = filter_items(Items.new, opt: opt)
|
190
|
+
|
191
|
+
if opt[:interactive]
|
192
|
+
items = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, menu: true,
|
193
|
+
header: '',
|
194
|
+
prompt: 'Select entries to tag > ',
|
195
|
+
multiple: true,
|
196
|
+
sort: true,
|
197
|
+
show_if_single: true)
|
198
|
+
|
199
|
+
raise NoResults, 'no items selected' if items.empty?
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
raise NoResults, 'no items matched your search' if items.empty?
|
204
|
+
|
205
|
+
if opt[:tags].empty? && !opt[:autotag]
|
206
|
+
completions = opt[:remove] ? all_tags(items) : all_tags(@content)
|
207
|
+
if opt[:remove]
|
208
|
+
puts "#{yellow}Available tags: #{boldwhite}#{completions.map(&:add_at).join(', ')}"
|
209
|
+
else
|
210
|
+
puts "#{yellow}Use tab to complete known tags"
|
211
|
+
end
|
212
|
+
opt[:tags] = Doing::Prompt.read_line(prompt: "Enter tag(s) to #{opt[:remove] ? 'remove' : 'add'}",
|
213
|
+
completions: completions,
|
214
|
+
default_response: '').to_tags
|
215
|
+
raise UserCancelled, 'No tags provided' if opt[:tags].empty?
|
216
|
+
end
|
217
|
+
|
218
|
+
items.each do |item|
|
219
|
+
old_item = item.clone
|
220
|
+
added = []
|
221
|
+
removed = []
|
222
|
+
|
223
|
+
item.date = opt[:start_date] if opt[:start_date]
|
224
|
+
|
225
|
+
if opt[:autotag]
|
226
|
+
new_title = autotag(item.title) if Doing.auto_tag
|
227
|
+
if new_title == item.title
|
228
|
+
logger.count(:skipped, level: :debug, message: '%count unchaged %items')
|
229
|
+
# logger.debug('Autotag:', 'No changes')
|
230
|
+
else
|
231
|
+
logger.count(:added_tags)
|
232
|
+
logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
|
233
|
+
item.title = new_title
|
234
|
+
end
|
235
|
+
else
|
236
|
+
if opt[:done_date]
|
237
|
+
done_date = opt[:done_date]
|
238
|
+
elsif opt[:sequential]
|
239
|
+
next_entry = next_item(item)
|
240
|
+
|
241
|
+
done_date = if next_entry.nil?
|
242
|
+
Time.now
|
243
|
+
else
|
244
|
+
next_entry.date - 60
|
245
|
+
end
|
246
|
+
else
|
247
|
+
done_date = item.calculate_end_date(opt)
|
248
|
+
end
|
249
|
+
|
250
|
+
opt[:tags].each do |tag|
|
251
|
+
if tag == 'done' && !item.should_finish?
|
252
|
+
|
253
|
+
Doing.logger.debug('Skipped:', "Item in never_finish: #{item.title}")
|
254
|
+
logger.count(:skipped, level: :debug)
|
255
|
+
next
|
256
|
+
end
|
257
|
+
|
258
|
+
tag = tag.strip
|
259
|
+
|
260
|
+
if tag =~ /^(\S+)\((.*?)\)$/
|
261
|
+
m = Regexp.last_match
|
262
|
+
tag = m[1]
|
263
|
+
opt[:value] ||= m[2]
|
264
|
+
end
|
265
|
+
|
266
|
+
if tag =~ /^done$/ && opt[:date] && item.should_time?
|
267
|
+
max_elapsed = Doing.setting('interaction.confirm_longer_than', 0)
|
268
|
+
max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
|
269
|
+
elapsed = done_date - item.date
|
270
|
+
|
271
|
+
if max_elapsed.positive? && (elapsed > max_elapsed) && !opt[:took]
|
272
|
+
puts boldwhite(item.title)
|
273
|
+
human = elapsed.time_string(format: :natural)
|
274
|
+
res = Prompt.yn(yellow("Did this actually take #{human}"), default_response: true)
|
275
|
+
unless res
|
276
|
+
new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
|
277
|
+
raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed > 0
|
278
|
+
|
279
|
+
opt[:took] = new_elapsed
|
280
|
+
done_date = item.calculate_end_date(opt) if opt[:took]
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
if opt[:remove] || opt[:rename] || opt[:value]
|
286
|
+
rename_to = nil
|
287
|
+
if opt[:value]
|
288
|
+
rename_to = tag
|
289
|
+
elsif opt[:rename]
|
290
|
+
rename_to = tag
|
291
|
+
tag = opt[:rename]
|
292
|
+
end
|
293
|
+
old_title = item.title.dup
|
294
|
+
force = opt[:value].nil? ? false : true
|
295
|
+
item.title.tag!(tag, remove: opt[:remove], rename_to: rename_to, regex: opt[:regex], value: opt[:value], force: force)
|
296
|
+
if old_title != item.title
|
297
|
+
removed << tag
|
298
|
+
added << rename_to if rename_to
|
299
|
+
else
|
300
|
+
logger.count(:skipped, level: :debug)
|
301
|
+
end
|
302
|
+
else
|
303
|
+
old_title = item.title.dup
|
304
|
+
should_date = opt[:date] && item.should_time?
|
305
|
+
item.title.tag!('done', remove: true) if tag =~ /done/ && (!should_date || opt[:update])
|
306
|
+
item.title.tag!(tag, value: should_date ? done_date.strftime('%F %R') : nil)
|
307
|
+
added << tag if old_title != item.title
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
logger.log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
|
313
|
+
|
314
|
+
item.note.add(opt[:note]) if opt[:note]
|
315
|
+
|
316
|
+
if opt[:archive] && opt[:section] != 'Archive' && (opt[:count]).positive?
|
317
|
+
item.move_to('Archive', label: true)
|
318
|
+
elsif opt[:archive] && opt[:count].zero?
|
319
|
+
logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
|
320
|
+
end
|
321
|
+
|
322
|
+
item.expand_date_tags(Doing.setting('date_tags'))
|
323
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
324
|
+
end
|
325
|
+
|
326
|
+
write(@doing_file)
|
327
|
+
end
|
328
|
+
|
329
|
+
##
|
330
|
+
## Accepts one tag and the raw text of a new item if the
|
331
|
+
## passed tag is on any item, it's replaced with @done.
|
332
|
+
## if new_item is not nil, it's tagged with the passed
|
333
|
+
## tag and inserted. This is for use where only one
|
334
|
+
## instance of a given tag should exist (@meanwhile)
|
335
|
+
##
|
336
|
+
## @param target_tag [String] Tag to replace
|
337
|
+
## @param opt [Hash] Additional Options
|
338
|
+
##
|
339
|
+
## @option opt :section [String] target section
|
340
|
+
## @option opt :archive [Boolean] archive old item
|
341
|
+
## @option opt :back [Date] backdate new item
|
342
|
+
## @option opt :new_item [String] content to use for new item
|
343
|
+
## @option opt :note [Array] note content for new item
|
344
|
+
def stop_start(target_tag, opt)
|
345
|
+
opt ||= {}
|
346
|
+
tag = target_tag.dup
|
347
|
+
opt[:section] ||= Doing.setting('current_section')
|
348
|
+
opt[:archive] ||= false
|
349
|
+
opt[:back] ||= Time.now
|
350
|
+
opt[:new_item] ||= false
|
351
|
+
opt[:note] ||= false
|
352
|
+
|
353
|
+
opt[:section] = guess_section(opt[:section])
|
354
|
+
|
355
|
+
tag.sub!(/^@/, '')
|
356
|
+
|
357
|
+
found_items = 0
|
358
|
+
|
359
|
+
@content.each_with_index do |item, i|
|
360
|
+
old_item = i.clone
|
361
|
+
next unless item.section == opt[:section] || opt[:section] =~ /all/i
|
362
|
+
|
363
|
+
next unless item.title =~ /@#{tag}/
|
364
|
+
|
365
|
+
item.title.add_tags!([tag, 'done'], remove: true)
|
366
|
+
item.tag('done', value: opt[:back].strftime('%F %R'))
|
367
|
+
|
368
|
+
found_items += 1
|
369
|
+
|
370
|
+
if opt[:archive] && opt[:section] != 'Archive'
|
371
|
+
item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
|
372
|
+
item.move_to('Archive', label: false, log: false)
|
373
|
+
logger.count(:completed_archived)
|
374
|
+
logger.info('Completed/archived:', item.title)
|
375
|
+
else
|
376
|
+
logger.count(:completed)
|
377
|
+
logger.info('Completed:', item.title)
|
378
|
+
end
|
379
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
|
384
|
+
|
385
|
+
if opt[:new_item]
|
386
|
+
date, title, note = format_input(opt[:new_item])
|
387
|
+
opt[:back] = date unless date.nil?
|
388
|
+
note.add(opt[:note]) if opt[:note]
|
389
|
+
title.tag!(tag)
|
390
|
+
add_item(title.cap_first, opt[:section], { note: note, back: opt[:back] })
|
391
|
+
end
|
392
|
+
|
393
|
+
write(@doing_file)
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
## Delete a set of items from the main index
|
398
|
+
##
|
399
|
+
## @param items [Array] The items to delete
|
400
|
+
## @param force [Boolean] Force deletion without confirmation
|
401
|
+
##
|
402
|
+
def delete_items(items, force: false)
|
403
|
+
items.slice(0, 5).each { |i| puts i.to_pretty } unless force
|
404
|
+
puts softpurple("+ #{items.size - 5} additional #{'item'.to_p(items.size - 5)}") if items.size > 5 && !force
|
405
|
+
|
406
|
+
res = force ? true : Prompt.yn("Delete #{items.size} #{'item'.to_p(items.size)}?", default_response: 'y')
|
407
|
+
return unless res
|
408
|
+
|
409
|
+
items.each { |i| Hooks.trigger :post_entry_removed, self, @content.delete_item(i, single: items.count == 1) }
|
410
|
+
# write(@doing_file)
|
411
|
+
end
|
412
|
+
|
413
|
+
##
|
414
|
+
## Move entries from a section to Archive or other specified
|
415
|
+
## section
|
416
|
+
##
|
417
|
+
## @param section [String] The source section
|
418
|
+
## @param options [Hash] Options
|
419
|
+
##
|
420
|
+
def archive(section = Doing.setting('current_section'), options)
|
421
|
+
options ||= {}
|
422
|
+
count = options[:keep] || 0
|
423
|
+
destination = options[:destination] || 'Archive'
|
424
|
+
tags = options[:tags] || []
|
425
|
+
bool = options[:bool] || :and
|
426
|
+
|
427
|
+
section = choose_section if section.nil? || section =~ /choose/i
|
428
|
+
archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
|
429
|
+
section = guess_section(section) unless archive_all
|
430
|
+
|
431
|
+
@content.add_section(destination, log: true)
|
432
|
+
# add_section(Section.new('Archive')) if destination =~ /^archive$/i && !@content.section?('Archive')
|
433
|
+
|
434
|
+
destination = guess_section(destination)
|
435
|
+
|
436
|
+
if @content.section?(destination) && (@content.section?(section) || archive_all)
|
437
|
+
do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before], after: options[:after], from: options[:from] })
|
438
|
+
write(doing_file)
|
439
|
+
else
|
440
|
+
raise InvalidArgument, 'Either source or destination does not exist'
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
## Uses 'autotag' configuration to turn keywords into tags for time tracking.
|
446
|
+
## Does not repeat tags in a title, and only converts the first instance of an
|
447
|
+
## untagged keyword
|
448
|
+
##
|
449
|
+
## @param string [String] The text to tag
|
450
|
+
##
|
451
|
+
def autotag(string)
|
452
|
+
return unless string
|
453
|
+
return string unless Doing.auto_tag
|
454
|
+
|
455
|
+
original = string.dup
|
456
|
+
text = string.dup
|
457
|
+
|
458
|
+
current_tags = text.scan(/@\w+/).map { |t| t.sub(/^@/, '') }
|
459
|
+
tagged = {
|
460
|
+
whitelisted: [],
|
461
|
+
synonyms: [],
|
462
|
+
transformed: [],
|
463
|
+
replaced: []
|
464
|
+
}
|
465
|
+
|
466
|
+
Doing.setting('autotag.whitelist').each do |tag|
|
467
|
+
next if text =~ /@#{tag}\b/i
|
468
|
+
|
469
|
+
text.sub!(/(?<= |\A)(#{tag.strip})(?= |\Z)/i) do |m|
|
470
|
+
m.downcase! unless tag =~ /[A-Z]/
|
471
|
+
tagged[:whitelisted].push(m)
|
472
|
+
"@#{m}"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
Doing.setting('autotag.synonyms').each do |tag, v|
|
477
|
+
v.each do |word|
|
478
|
+
word = word.wildcard_to_rx
|
479
|
+
next unless text =~ /\b#{word}\b/i
|
480
|
+
|
481
|
+
unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
|
482
|
+
tagged[:synonyms].push(tag)
|
483
|
+
tagged[:synonyms] = tagged[:synonyms].uniq
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
if Doing.setting('autotag.transform')
|
489
|
+
Doing.setting('autotag.transform').each do |tag|
|
490
|
+
next unless tag =~ /\S+:\S+/
|
491
|
+
|
492
|
+
if tag =~ /::/
|
493
|
+
rx, r = tag.split(/::/)
|
494
|
+
else
|
495
|
+
rx, r = tag.split(/:/)
|
496
|
+
end
|
497
|
+
|
498
|
+
flag_rx = %r{/([r]+)$}
|
499
|
+
if r =~ flag_rx
|
500
|
+
flags = r.match(flag_rx)[1].split(//)
|
501
|
+
r.sub!(flag_rx, '')
|
502
|
+
end
|
503
|
+
r.gsub!(/\$/, '\\')
|
504
|
+
rx.sub!(/^@?/, '@')
|
505
|
+
regex = Regexp.new("(?<= |\\A)#{rx}(?= |\\Z)")
|
506
|
+
|
507
|
+
text.sub!(regex) do
|
508
|
+
m = Regexp.last_match
|
509
|
+
new_tag = r
|
510
|
+
|
511
|
+
m.to_a.slice(1, m.length - 1).each_with_index do |v, idx|
|
512
|
+
next if v.nil?
|
513
|
+
|
514
|
+
new_tag.gsub!("\\#{idx + 1}", v)
|
515
|
+
end
|
516
|
+
# Replace original tag if /r
|
517
|
+
if flags&.include?('r')
|
518
|
+
tagged[:replaced].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
|
519
|
+
new_tag.split(/ /).map { |t| t.sub(/^@?/, '@') }.join(' ')
|
520
|
+
else
|
521
|
+
tagged[:transformed].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
|
522
|
+
tagged[:transformed] = tagged[:transformed].uniq
|
523
|
+
m[0]
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
|
530
|
+
logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
|
531
|
+
logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
|
532
|
+
logger.debug('Autotag:', "transform replaced: #{tagged[:replaced].log_tags}") unless tagged[:replaced].empty?
|
533
|
+
|
534
|
+
tail_tags = tagged[:synonyms].concat(tagged[:transformed])
|
535
|
+
tail_tags.sort!
|
536
|
+
tail_tags.uniq!
|
537
|
+
|
538
|
+
text.add_tags!(tail_tags) unless tail_tags.empty?
|
539
|
+
|
540
|
+
if text == original
|
541
|
+
logger.debug('Autotag:', "no change to \"#{text.strip}\"")
|
542
|
+
else
|
543
|
+
new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
|
544
|
+
logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text.strip}\"")
|
545
|
+
logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
|
546
|
+
end
|
547
|
+
|
548
|
+
text.dedup_tags
|
549
|
+
end
|
550
|
+
|
551
|
+
private
|
552
|
+
|
553
|
+
##
|
554
|
+
## Helper function, performs the actual archiving
|
555
|
+
##
|
556
|
+
## @param section [String] The source section
|
557
|
+
## @param destination [String] The destination
|
558
|
+
## section
|
559
|
+
## @param opt [Hash] Additional Options
|
560
|
+
## @api private
|
561
|
+
def do_archive(section, destination, opt)
|
562
|
+
opt ||= {}
|
563
|
+
count = opt[:count] || 0
|
564
|
+
tags = opt[:tags] || []
|
565
|
+
bool = opt[:bool] || :and
|
566
|
+
label = opt[:label] || true
|
567
|
+
|
568
|
+
section = guess_section(section)
|
569
|
+
destination = guess_section(destination)
|
570
|
+
|
571
|
+
section_items = @content.in_section(section)
|
572
|
+
max = section_items.count - count.to_i
|
573
|
+
|
574
|
+
opt[:after] = opt[:from][0] if opt[:from]
|
575
|
+
opt[:before] = opt[:from][1] if opt[:from]
|
576
|
+
|
577
|
+
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
|
578
|
+
|
579
|
+
if opt[:before].is_a?(String) && opt[:before] =~ time_rx
|
580
|
+
opt[:before] = opt[:before].chronify(guess: :end, future: false)
|
581
|
+
end
|
582
|
+
|
583
|
+
if opt[:after].is_a?(String) && opt[:after] =~ time_rx
|
584
|
+
opt[:after] = opt[:after].chronify(guess: :begin, future: false)
|
585
|
+
end
|
586
|
+
|
587
|
+
counter = 0
|
588
|
+
|
589
|
+
@content.map do |item|
|
590
|
+
break if counter >= max
|
591
|
+
|
592
|
+
next if item.section.downcase == destination.downcase
|
593
|
+
|
594
|
+
next if item.section.downcase != section.downcase && section != /^all$/i
|
595
|
+
|
596
|
+
next if (opt[:before] && item.date > opt[:before]) || (opt[:after] && item.date < opt[:after])
|
597
|
+
|
598
|
+
next if (!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s))
|
599
|
+
|
600
|
+
counter += 1
|
601
|
+
old_item = item.clone
|
602
|
+
item.move_to(destination, label: label, log: false)
|
603
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
604
|
+
item
|
605
|
+
end
|
606
|
+
|
607
|
+
if counter.positive?
|
608
|
+
logger.count(destination == 'Archive' ? :archived : :moved,
|
609
|
+
level: :info,
|
610
|
+
count: counter,
|
611
|
+
message: "%count %items from #{section} to #{destination}")
|
612
|
+
else
|
613
|
+
logger.info('Skipped:', 'No items were moved')
|
614
|
+
end
|
615
|
+
end
|
616
|
+
end
|
617
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class WWID
|
5
|
+
##
|
6
|
+
## List all tags that exist on given items
|
7
|
+
##
|
8
|
+
## @param items [Array] array of Item
|
9
|
+
## @param opt [Hash] additional options
|
10
|
+
## @param counts [Boolean] Include tag counts in
|
11
|
+
## results
|
12
|
+
##
|
13
|
+
## @return [Hash] if counts is true, returns a hash with {
|
14
|
+
## tag: count }.
|
15
|
+
## @return [Array] If counts is false, returns a simple
|
16
|
+
## array of tags.
|
17
|
+
##
|
18
|
+
def all_tags(items, opt: {}, counts: false)
|
19
|
+
if counts
|
20
|
+
all_tags = {}
|
21
|
+
items.each do |item|
|
22
|
+
item.tags.each do |tag|
|
23
|
+
if all_tags.key?(tag.downcase)
|
24
|
+
all_tags[tag.downcase] += 1
|
25
|
+
else
|
26
|
+
all_tags[tag.downcase] = 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
all_tags.sort_by { |_, count| count }
|
32
|
+
else
|
33
|
+
all_tags = []
|
34
|
+
items.each { |item| all_tags.concat(item.tags.map(&:downcase)).uniq! }
|
35
|
+
all_tags.sort
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def tag_groups(items, opt: {})
|
40
|
+
all_items = filter_items(items, opt: opt)
|
41
|
+
tags = all_tags(all_items, opt: {})
|
42
|
+
groups = {}
|
43
|
+
tags.each do |tag|
|
44
|
+
groups[tag] ||= []
|
45
|
+
groups[tag] = filter_items(all_items, opt: { tag: tag, tag_bool: :or })
|
46
|
+
end
|
47
|
+
|
48
|
+
groups
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|