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
data/lib/doing/util.rb
CHANGED
@@ -47,13 +47,13 @@ module Doing
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
# Non-destructive version of deep_merge_hashes!
|
51
|
-
#
|
50
|
+
# Non-destructive version of deep_merge_hashes!
|
51
|
+
# @see {deep_merge_hashes!}
|
52
52
|
#
|
53
53
|
# @return the merged hashes.
|
54
54
|
#
|
55
|
-
# @param [Hash]
|
56
|
-
# @param [Hash]
|
55
|
+
# @param master_hash [Hash] The master hash
|
56
|
+
# @param other_hash [Hash] The other hash
|
57
57
|
#
|
58
58
|
def deep_merge_hashes(master_hash, other_hash)
|
59
59
|
deep_merge_hashes!(master_hash.clone, other_hash)
|
@@ -61,13 +61,19 @@ module Doing
|
|
61
61
|
|
62
62
|
# Merges a master hash with another hash, recursively.
|
63
63
|
#
|
64
|
-
#
|
65
|
-
#
|
64
|
+
# @param target [Hash] the "parent" hash whose
|
65
|
+
# values will be overridden
|
66
|
+
# @param overwrite [Hash] the other hash whose
|
67
|
+
# values will be persisted after
|
68
|
+
# the merge
|
66
69
|
#
|
67
70
|
# This code was lovingly stolen from some random gem:
|
68
71
|
# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
|
69
72
|
#
|
70
73
|
# Thanks to whoever made it.
|
74
|
+
#
|
75
|
+
# @return [Hash] merged hashes
|
76
|
+
#
|
71
77
|
def deep_merge_hashes!(target, overwrite)
|
72
78
|
merge_values(target, overwrite)
|
73
79
|
merge_default_proc(target, overwrite)
|
data/lib/doing/util_backup.rb
CHANGED
@@ -24,19 +24,6 @@ module Doing
|
|
24
24
|
clear_redo(filename)
|
25
25
|
end
|
26
26
|
|
27
|
-
##
|
28
|
-
## Delete all redo files
|
29
|
-
##
|
30
|
-
## @param limit Maximum number of backups to retain
|
31
|
-
##
|
32
|
-
def clear_redo(filename)
|
33
|
-
filename ||= Doing.setting('doing_file')
|
34
|
-
backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
|
35
|
-
backups.each do |file|
|
36
|
-
FileUtils.rm(File.join(backup_dir, file))
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
27
|
##
|
41
28
|
## Retrieve the most recent backup
|
42
29
|
##
|
@@ -62,10 +49,11 @@ module Doing
|
|
62
49
|
filename ||= Doing.setting('doing_file')
|
63
50
|
|
64
51
|
backup_file = last_backup(filename, count: count)
|
65
|
-
raise
|
52
|
+
raise HistoryLimitError, 'End of undo history' if backup_file.nil?
|
66
53
|
|
67
54
|
save_undone(filename)
|
68
|
-
|
55
|
+
move_backup(backup_file, filename)
|
56
|
+
|
69
57
|
prune_backups_after(File.basename(backup_file))
|
70
58
|
Doing.logger.warn('File update:', "restored from #{backup_file}")
|
71
59
|
Doing.logger.benchmark(:restore_backup, :finish)
|
@@ -78,7 +66,7 @@ module Doing
|
|
78
66
|
##
|
79
67
|
def redo_backup(filename = nil, count: 1)
|
80
68
|
filename ||= Doing.setting('doing_file')
|
81
|
-
|
69
|
+
|
82
70
|
undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
|
83
71
|
total = undones.count
|
84
72
|
count = total if count > total
|
@@ -86,11 +74,11 @@ module Doing
|
|
86
74
|
skipped = undones.slice!(0, count)
|
87
75
|
undone = skipped.pop
|
88
76
|
|
89
|
-
raise
|
77
|
+
raise HistoryLimitError, 'End of redo history' if undone.nil?
|
90
78
|
|
91
79
|
redo_file = File.join(backup_dir, undone)
|
92
80
|
|
93
|
-
|
81
|
+
move_backup(redo_file, filename)
|
94
82
|
|
95
83
|
skipped.each do |f|
|
96
84
|
FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
|
@@ -100,14 +88,6 @@ module Doing
|
|
100
88
|
Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
|
101
89
|
end
|
102
90
|
|
103
|
-
def clear_undone(filename = nil)
|
104
|
-
filename ||= Doing.setting('doing_file')
|
105
|
-
# redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
|
106
|
-
Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
|
107
|
-
FileUtils.rm(File.join(backup_dir, f))
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
91
|
##
|
112
92
|
## Select from recent undos. If a filename is
|
113
93
|
## provided, only backups of that filename will be used.
|
@@ -118,8 +98,7 @@ module Doing
|
|
118
98
|
filename ||= Doing.setting('doing_file')
|
119
99
|
|
120
100
|
undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
|
121
|
-
|
122
|
-
raise DoingRuntimeError, 'End of redo history' if undones.empty?
|
101
|
+
raise HistoryLimitError, 'End of redo history' if undones.empty?
|
123
102
|
|
124
103
|
total = undones.count
|
125
104
|
options = undones.each_with_object([]) do |file, arr|
|
@@ -128,8 +107,7 @@ module Doing
|
|
128
107
|
|
129
108
|
arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
|
130
109
|
end
|
131
|
-
|
132
|
-
raise DoingRuntimeError, 'No backup files to load' if options.empty?
|
110
|
+
raise MissingBackupFile, 'No backup files to load' if options.empty?
|
133
111
|
|
134
112
|
backup_file = show_menu(options, filename)
|
135
113
|
idx = undones.index(File.basename(backup_file))
|
@@ -138,7 +116,7 @@ module Doing
|
|
138
116
|
|
139
117
|
redo_file = File.join(backup_dir, undone)
|
140
118
|
|
141
|
-
|
119
|
+
move_backup(redo_file, filename)
|
142
120
|
|
143
121
|
skipped.each do |f|
|
144
122
|
FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
|
@@ -163,15 +141,51 @@ module Doing
|
|
163
141
|
arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
|
164
142
|
end
|
165
143
|
|
166
|
-
raise
|
144
|
+
raise MissingBackupFile, 'No backup files to load' if options.empty?
|
167
145
|
|
168
146
|
backup_file = show_menu(options, filename)
|
169
147
|
Util.write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
|
170
|
-
|
148
|
+
move_backup(backup_file, filename)
|
171
149
|
prune_backups_after(File.basename(backup_file))
|
172
150
|
Doing.logger.warn('File update:', "restored from #{backup_file}")
|
173
151
|
end
|
174
152
|
|
153
|
+
##
|
154
|
+
## Writes a copy of the content to a dated backup file
|
155
|
+
## in a hidden directory
|
156
|
+
##
|
157
|
+
## @param filename [String] The filename
|
158
|
+
##
|
159
|
+
def write_backup(filename = nil)
|
160
|
+
Doing.logger.benchmark(:_write_backup, :start)
|
161
|
+
filename ||= Doing.setting('doing_file')
|
162
|
+
|
163
|
+
unless File.exist?(filename)
|
164
|
+
Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
|
165
|
+
return
|
166
|
+
end
|
167
|
+
|
168
|
+
backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
|
169
|
+
# compressed = Zlib::Deflate.deflate(content)
|
170
|
+
# Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
|
171
|
+
# gz.write(IO.read(filename))
|
172
|
+
# end
|
173
|
+
|
174
|
+
FileUtils.cp(filename, backup_file)
|
175
|
+
|
176
|
+
prune_backups(filename, Doing.setting('history_size').to_i)
|
177
|
+
clear_undone(filename)
|
178
|
+
Doing.logger.benchmark(:_write_backup, :finish)
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def move_backup(source, dest)
|
184
|
+
Hooks.trigger :pre_write, WWID.new, dest
|
185
|
+
FileUtils.mv(source, dest)
|
186
|
+
Hooks.trigger :post_write, dest
|
187
|
+
end
|
188
|
+
|
175
189
|
def show_menu(options, filename)
|
176
190
|
if TTY::Which.which('colordiff')
|
177
191
|
preview = 'colordiff -U 1'
|
@@ -211,36 +225,27 @@ module Doing
|
|
211
225
|
result.strip.split(/\t/).last
|
212
226
|
end
|
213
227
|
|
228
|
+
def clear_undone(filename = nil)
|
229
|
+
filename ||= Doing.setting('doing_file')
|
230
|
+
# redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
|
231
|
+
Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
|
232
|
+
FileUtils.rm(File.join(backup_dir, f))
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
214
236
|
##
|
215
|
-
##
|
216
|
-
## in a hidden directory
|
237
|
+
## Delete all redo files
|
217
238
|
##
|
218
|
-
## @param
|
239
|
+
## @param filename [String] The filename
|
219
240
|
##
|
220
|
-
def
|
221
|
-
Doing.logger.benchmark(:_write_backup, :start)
|
241
|
+
def clear_redo(filename)
|
222
242
|
filename ||= Doing.setting('doing_file')
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
return
|
243
|
+
backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
|
244
|
+
backups.each do |file|
|
245
|
+
FileUtils.rm(File.join(backup_dir, file))
|
227
246
|
end
|
228
|
-
|
229
|
-
backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
|
230
|
-
# compressed = Zlib::Deflate.deflate(content)
|
231
|
-
# Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
|
232
|
-
# gz.write(IO.read(filename))
|
233
|
-
# end
|
234
|
-
|
235
|
-
FileUtils.cp(filename, backup_file)
|
236
|
-
|
237
|
-
prune_backups(filename, Doing.setting('history_size').to_i)
|
238
|
-
clear_undone(filename)
|
239
|
-
Doing.logger.benchmark(:_write_backup, :finish)
|
240
247
|
end
|
241
248
|
|
242
|
-
private
|
243
|
-
|
244
249
|
def timestamp_filename
|
245
250
|
Time.now.strftime('%Y-%m-%d_%H.%M.%S')
|
246
251
|
end
|
@@ -281,7 +286,7 @@ module Doing
|
|
281
286
|
def create_backup_dir
|
282
287
|
dir = File.expand_path(Doing.setting('backup_dir')) || File.join(user_home, '.doing_backup')
|
283
288
|
if File.exist?(dir) && !File.directory?(dir)
|
284
|
-
raise
|
289
|
+
raise DoingNoTraceError.new("#{dir} is not a directory", topic: 'History:', exit_code: 27)
|
285
290
|
|
286
291
|
end
|
287
292
|
|
data/lib/doing/version.rb
CHANGED
@@ -0,0 +1,396 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
class WWID
|
5
|
+
##
|
6
|
+
## Display contents of a section based on options
|
7
|
+
##
|
8
|
+
## @param opt [Hash] Additional Options
|
9
|
+
##
|
10
|
+
def list_section(opt, items: Items.new)
|
11
|
+
logger.benchmark(:list_section, :start)
|
12
|
+
opt[:config_template] ||= 'default'
|
13
|
+
|
14
|
+
tpl_cfg = Doing.setting(['templates', opt[:config_template]])
|
15
|
+
|
16
|
+
cfg = if opt[:view_template]
|
17
|
+
Doing.setting(['views', opt[:view_template]]).deep_merge(tpl_cfg, { extend_existing_arrays: true, sort_merged_arrays: true })
|
18
|
+
else
|
19
|
+
tpl_cfg
|
20
|
+
end
|
21
|
+
|
22
|
+
cfg.deep_merge({
|
23
|
+
'wrap_width' => Doing.setting('wrap_width') || 0,
|
24
|
+
'date_format' => Doing.setting('default_date_format'),
|
25
|
+
'order' => Doing.setting('order') || :asc,
|
26
|
+
'tags_color' => Doing.setting('tags_color'),
|
27
|
+
'duration' => Doing.setting('duration'),
|
28
|
+
'interval_format' => Doing.setting('interval_format')
|
29
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
30
|
+
|
31
|
+
opt[:duration] ||= cfg['duration'] || false
|
32
|
+
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
33
|
+
opt[:count] ||= 0
|
34
|
+
opt[:age] ||= :newest
|
35
|
+
opt[:age] = opt[:age].normalize_age
|
36
|
+
opt[:format] ||= cfg['date_format']
|
37
|
+
opt[:order] ||= cfg['order'] || :asc
|
38
|
+
opt[:tag_order] ||= :asc
|
39
|
+
opt[:tags_color] = cfg['tags_color'] || false if opt[:tags_color].nil?
|
40
|
+
opt[:template] ||= cfg['template']
|
41
|
+
opt[:sort_tags] ||= opt[:tag_sort]
|
42
|
+
|
43
|
+
# opt[:highlight] ||= true
|
44
|
+
title = ''
|
45
|
+
is_single = true
|
46
|
+
if opt[:section].nil?
|
47
|
+
opt[:section] = choose_section
|
48
|
+
title = opt[:section]
|
49
|
+
elsif opt[:section].instance_of?(String)
|
50
|
+
title = if opt[:section] =~ /^all$/i
|
51
|
+
if opt[:page_title]
|
52
|
+
opt[:page_title]
|
53
|
+
elsif opt[:tag_filter] && opt[:tag_filter]['bool'].normalize_bool != :not
|
54
|
+
opt[:tag_filter]['tags'].map { |tag| "@#{tag}" }.join(' + ')
|
55
|
+
else
|
56
|
+
'doing'
|
57
|
+
end
|
58
|
+
else
|
59
|
+
guess_section(opt[:section])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
items = filter_items(items, opt: opt)
|
64
|
+
|
65
|
+
items.reverse! unless opt[:order].normalize_order == :desc
|
66
|
+
|
67
|
+
if opt[:delete]
|
68
|
+
delete_items(items, force: opt[:force])
|
69
|
+
|
70
|
+
write(@doing_file)
|
71
|
+
return
|
72
|
+
elsif opt[:editor]
|
73
|
+
edit_items(items)
|
74
|
+
|
75
|
+
write(@doing_file)
|
76
|
+
return
|
77
|
+
elsif opt[:interactive]
|
78
|
+
opt[:menu] = !opt[:force]
|
79
|
+
opt[:query] = '' # opt[:search]
|
80
|
+
opt[:multiple] = true
|
81
|
+
selected = Prompt.choose_from_items(items.reverse, include_section: opt[:section] =~ /^all$/i, **opt)
|
82
|
+
|
83
|
+
raise NoResults, 'no items selected' if selected.nil? || selected.empty?
|
84
|
+
|
85
|
+
act_on(selected, opt)
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
opt[:output] ||= 'template'
|
90
|
+
opt[:wrap_width] ||= Doing.setting('templates.default.wrap_width', 0)
|
91
|
+
|
92
|
+
logger.benchmark(:list_section, :finish)
|
93
|
+
output(items, title, is_single, opt)
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
## Display entries within a date range
|
98
|
+
##
|
99
|
+
## @param dates [Array] [start, end]
|
100
|
+
## @param section [String] The section
|
101
|
+
## @param times (Bool) Show times
|
102
|
+
## @param output [String] Output format
|
103
|
+
## @param opt [Hash] Additional Options
|
104
|
+
##
|
105
|
+
def list_date(dates, section, times = nil, output = nil, opt)
|
106
|
+
opt ||= {}
|
107
|
+
opt[:totals] ||= false
|
108
|
+
opt[:sort_tags] ||= false
|
109
|
+
section = guess_section(section)
|
110
|
+
# :date_filter expects an array with start and end date
|
111
|
+
dates = dates.split_date_range if dates.instance_of?(String)
|
112
|
+
|
113
|
+
opt[:section] = section
|
114
|
+
opt[:count] = 0
|
115
|
+
opt[:order] = :asc
|
116
|
+
opt[:date_filter] = dates
|
117
|
+
opt[:times] = times
|
118
|
+
opt[:output] = output
|
119
|
+
|
120
|
+
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
|
121
|
+
if opt[:from] && opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
|
122
|
+
opt[:time_filter] = opt[:from]
|
123
|
+
end
|
124
|
+
|
125
|
+
list_section(opt)
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
## Show all entries from the current day
|
130
|
+
##
|
131
|
+
## @param times [Boolean] show times
|
132
|
+
## @param output [String] output format
|
133
|
+
## @param opt [Hash] Options
|
134
|
+
##
|
135
|
+
def today(times = true, output = nil, opt)
|
136
|
+
opt ||= {}
|
137
|
+
opt[:totals] ||= false
|
138
|
+
opt[:sort_tags] ||= false
|
139
|
+
|
140
|
+
cfg = Doing.setting('templates').deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
141
|
+
'wrap_width' => Doing.setting('wrap_width') || 0,
|
142
|
+
'date_format' => Doing.setting('default_date_format'),
|
143
|
+
'order' => Doing.setting('order') || :asc,
|
144
|
+
'tags_color' => Doing.setting('tags_color'),
|
145
|
+
'duration' => Doing.setting('duration'),
|
146
|
+
'interval_format' => Doing.setting('interval_format')
|
147
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
148
|
+
|
149
|
+
template = opt[:template] || cfg['template']
|
150
|
+
|
151
|
+
opt[:duration] ||= cfg['duration'] || false
|
152
|
+
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
153
|
+
|
154
|
+
options = {
|
155
|
+
after: opt[:after],
|
156
|
+
before: opt[:before],
|
157
|
+
count: 0,
|
158
|
+
duration: opt[:duration],
|
159
|
+
from: opt[:from],
|
160
|
+
format: cfg['date_format'],
|
161
|
+
interval_format: opt[:interval_format],
|
162
|
+
only_timed: opt[:only_timed],
|
163
|
+
order: cfg['order'] || :asc,
|
164
|
+
output: output,
|
165
|
+
section: opt[:section],
|
166
|
+
sort_tags: opt[:sort_tags],
|
167
|
+
template: template,
|
168
|
+
times: times,
|
169
|
+
today: true,
|
170
|
+
totals: opt[:totals],
|
171
|
+
wrap_width: cfg['wrap_width'],
|
172
|
+
tags_color: cfg['tags_color'],
|
173
|
+
config_template: opt[:config_template]
|
174
|
+
}
|
175
|
+
list_section(options)
|
176
|
+
end
|
177
|
+
|
178
|
+
##
|
179
|
+
## Show entries from the previous day
|
180
|
+
##
|
181
|
+
## @param section [String] The section
|
182
|
+
## @param times (Bool) Show times
|
183
|
+
## @param output [String] Output format
|
184
|
+
## @param opt [Hash] Additional Options
|
185
|
+
##
|
186
|
+
def yesterday(section, times = nil, output = nil, opt)
|
187
|
+
opt ||= {}
|
188
|
+
opt[:totals] ||= false
|
189
|
+
opt[:sort_tags] ||= false
|
190
|
+
opt[:config_template] ||= 'today'
|
191
|
+
opt[:yesterday] = true
|
192
|
+
|
193
|
+
section = guess_section(section)
|
194
|
+
y = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d')
|
195
|
+
opt[:after] = "#{y} #{opt[:after]}" if opt[:after]
|
196
|
+
opt[:before] = "#{y} #{opt[:before]}" if opt[:before]
|
197
|
+
|
198
|
+
opt[:output] = output
|
199
|
+
opt[:section] = section
|
200
|
+
opt[:times] = times
|
201
|
+
opt[:count] = 0
|
202
|
+
|
203
|
+
list_section(opt)
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
## Show recent entries
|
208
|
+
##
|
209
|
+
## @param count [Integer] The number to show
|
210
|
+
## @param section [String] The section to show from, default Currently
|
211
|
+
## @param opt [Hash] Additional Options
|
212
|
+
##
|
213
|
+
def recent(count = 10, section = nil, opt)
|
214
|
+
opt ||= {}
|
215
|
+
times = opt[:t] || true
|
216
|
+
opt[:totals] ||= false
|
217
|
+
opt[:sort_tags] ||= false
|
218
|
+
|
219
|
+
cfg = Doing.setting('templates.recent').deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
220
|
+
'wrap_width' => Doing.setting('wrap_width') || 0,
|
221
|
+
'date_format' => Doing.setting('default_date_format'),
|
222
|
+
'order' => Doing.setting('order') || :asc,
|
223
|
+
'tags_color' => Doing.setting('tags_color'),
|
224
|
+
'duration' => Doing.setting('duration'),
|
225
|
+
'interval_format' => Doing.setting('interval_format')
|
226
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
227
|
+
opt[:duration] ||= cfg['duration'] || false
|
228
|
+
opt[:interval_format] ||= cfg['interval_format'] || 'text'
|
229
|
+
|
230
|
+
section ||= Doing.setting('current_section')
|
231
|
+
section = guess_section(section)
|
232
|
+
|
233
|
+
opt[:section] = section
|
234
|
+
opt[:wrap_width] = cfg['wrap_width']
|
235
|
+
opt[:count] = count
|
236
|
+
opt[:format] = cfg['date_format']
|
237
|
+
opt[:template] = opt[:template] || cfg['template']
|
238
|
+
opt[:order] = :asc
|
239
|
+
opt[:times] = times
|
240
|
+
|
241
|
+
list_section(opt)
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
## Show the last entry
|
246
|
+
##
|
247
|
+
## @param times (Bool) Show times
|
248
|
+
## @param section [String] Section to pull from, default Currently
|
249
|
+
##
|
250
|
+
def last(times: true, section: nil, options: {})
|
251
|
+
section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
|
252
|
+
cfg = Doing.setting(['templates', options[:config_template]]).deep_merge(Doing.setting('templates.default'), { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
|
253
|
+
'wrap_width' => Doing.setting('wrap_width', 0),
|
254
|
+
'date_format' => Doing.setting('default_date_format'),
|
255
|
+
'order' => Doing.setting('order', :asc),
|
256
|
+
'tags_color' => Doing.setting('tags_color'),
|
257
|
+
'duration' => Doing.setting('duration'),
|
258
|
+
'interval_format' => Doing.setting('interval_format')
|
259
|
+
}, { extend_existing_arrays: true, sort_merged_arrays: true })
|
260
|
+
options[:duration] ||= cfg['duration'] || false
|
261
|
+
options[:interval_format] ||= cfg['interval_format'] || 'text'
|
262
|
+
|
263
|
+
opts = {
|
264
|
+
case: options[:case],
|
265
|
+
config_template: options[:config_template] || 'last',
|
266
|
+
count: 1,
|
267
|
+
delete: options[:delete],
|
268
|
+
duration: options[:duration],
|
269
|
+
format: cfg['date_format'],
|
270
|
+
interval_format: options[:interval_format],
|
271
|
+
not: options[:negate],
|
272
|
+
output: options[:output],
|
273
|
+
section: section,
|
274
|
+
template: options[:template] || cfg['template'],
|
275
|
+
times: times,
|
276
|
+
val: options[:val],
|
277
|
+
wrap_width: cfg['wrap_width']
|
278
|
+
}
|
279
|
+
|
280
|
+
if options[:tag]
|
281
|
+
opts[:tag_filter] = {
|
282
|
+
'tags' => options[:tag],
|
283
|
+
'bool' => options[:tag_bool]
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
opts[:search] = options[:search] if options[:search]
|
288
|
+
|
289
|
+
list_section(opts)
|
290
|
+
end
|
291
|
+
|
292
|
+
##
|
293
|
+
## Return the content of the last note for a given section
|
294
|
+
##
|
295
|
+
## @param section [String] The section to retrieve from, default
|
296
|
+
## All
|
297
|
+
##
|
298
|
+
def last_note(section = 'All')
|
299
|
+
section = guess_section(section)
|
300
|
+
|
301
|
+
last_item = last_entry({ section: section })
|
302
|
+
|
303
|
+
raise NoEntryError, 'No entry found' unless last_item
|
304
|
+
|
305
|
+
logger.log_now(:info, 'Edit note:', last_item.title)
|
306
|
+
|
307
|
+
note = last_item.note&.to_s || ''
|
308
|
+
"#{last_item.title}\n# EDIT BELOW THIS LINE ------------\n#{note}"
|
309
|
+
end
|
310
|
+
|
311
|
+
##
|
312
|
+
## Get the last entry
|
313
|
+
##
|
314
|
+
## @param opt [Hash] Additional Options
|
315
|
+
##
|
316
|
+
def last_entry(opt)
|
317
|
+
opt ||= {}
|
318
|
+
opt[:tag_bool] ||= :and
|
319
|
+
opt[:section] ||= Doing.setting('current_section')
|
320
|
+
|
321
|
+
items = filter_items(Items.new, opt: opt)
|
322
|
+
|
323
|
+
logger.debug('Filtered:', "Parameters matched #{items.count} entries")
|
324
|
+
|
325
|
+
if opt[:interactive]
|
326
|
+
last_entry = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i,
|
327
|
+
menu: true,
|
328
|
+
header: '',
|
329
|
+
prompt: 'Select an entry > ',
|
330
|
+
multiple: false,
|
331
|
+
sort: false,
|
332
|
+
show_if_single: true
|
333
|
+
)
|
334
|
+
else
|
335
|
+
last_entry = items.max_by { |item| item.date }
|
336
|
+
end
|
337
|
+
|
338
|
+
last_entry
|
339
|
+
end
|
340
|
+
|
341
|
+
private
|
342
|
+
|
343
|
+
##
|
344
|
+
## Generate output using available export plugins
|
345
|
+
##
|
346
|
+
## @param items [Array] The items
|
347
|
+
## @param title [String] Page title
|
348
|
+
## @param is_single [Boolean] Indicates if single
|
349
|
+
## section
|
350
|
+
## @param opt [Hash] Additional options
|
351
|
+
##
|
352
|
+
## @return [String] formatted output based on opt[:output]
|
353
|
+
## template trigger
|
354
|
+
## @api private
|
355
|
+
def output(items, title, is_single, opt)
|
356
|
+
logger.benchmark(:output, :start)
|
357
|
+
opt ||= {}
|
358
|
+
out = nil
|
359
|
+
|
360
|
+
raise InvalidArgument, 'Unknown output format' unless opt[:output] =~ Plugins.plugin_regex(type: :export)
|
361
|
+
|
362
|
+
export_options = { page_title: title, is_single: is_single, options: opt }
|
363
|
+
|
364
|
+
Hooks.trigger :pre_export, self, opt[:output], items
|
365
|
+
|
366
|
+
Plugins.plugins[:export].each do |_, options|
|
367
|
+
next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
|
368
|
+
|
369
|
+
out = options[:class].render(self, items, variables: export_options)
|
370
|
+
break
|
371
|
+
end
|
372
|
+
|
373
|
+
logger.debug('Output:', "#{items.count} #{items.count == 1 ? 'item' : 'items'} shown")
|
374
|
+
logger.benchmark(:output, :finish)
|
375
|
+
out
|
376
|
+
end
|
377
|
+
|
378
|
+
##
|
379
|
+
## Get next item in the index
|
380
|
+
##
|
381
|
+
## @param item [Item] target item
|
382
|
+
## @param options [Hash] additional options
|
383
|
+
## @see #filter_items
|
384
|
+
##
|
385
|
+
## @return [Item] the next chronological item in the index
|
386
|
+
##
|
387
|
+
def next_item(item, options = {})
|
388
|
+
options ||= {}
|
389
|
+
items = filter_items(Items.new, opt: options)
|
390
|
+
|
391
|
+
idx = items.index(item)
|
392
|
+
|
393
|
+
idx.positive? ? items[idx - 1] : nil
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|